Compare commits

..

1 Commits

Author SHA1 Message Date
A. Jesse Jiryu Davis
f25301dc9f MOTOR-131 enable SSL tests with Python 2.6
Update test certificates from PyMongo's to work around
http://bugs.python.org/issue13034
2017-04-01 15:05:20 -04:00
182 changed files with 10955 additions and 18667 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,12 @@
set -o xtrace # Write all commands first to stderr
set -o errexit # Exit the script with error if any of the commands fail
# Copy Motor's test certificates over driver-evergreen-tools'
cp ${PROJECT_DIRECTORY}/test/certificates/* ${DRIVERS_TOOLS}/.evergreen/x509gen/
# Replace MongoOrchestration's client certificate.
cp ${PROJECT_DIRECTORY}/test/certificates/client.pem ${MONGO_ORCHESTRATION_HOME}/lib/client.pem
if [ -w /etc/hosts ]; then
SUDO=""
else

View File

@ -2,18 +2,12 @@
# Don't trace to avoid secrets showing up in the logs
set -o errexit
set +x
echo "Running enterprise authentication tests"
source ./secrets-export.sh
export DB_USER="bob"
export DB_PASSWORD="pwd123"
# BUILD-3830
touch ${PROJECT_DIRECTORY}/.evergreen/krb5.conf.empty
export KRB5_CONFIG=${PROJECT_DIRECTORY}/.evergreen/krb5.conf.empty
echo "Writing keytab"
echo ${KEYTAB_BASE64} | base64 -d > ${PROJECT_DIRECTORY}/.evergreen/drivers.keytab
echo "Running kinit"
@ -25,6 +19,7 @@ export GSSAPI_PORT=${SASL_PORT}
export GSSAPI_PRINCIPAL=${PRINCIPAL}
# Pass needed env variables to the test environment.
export TOX_ENV="enterprise-synchro"
export TOX_TESTENV_PASSENV=*
bash ${PROJECT_DIRECTORY}/.evergreen/run-tox.sh
# --sitepackages allows use of pykerberos without a test dep.
/opt/python/3.6/bin/python3 -m tox -e "$TOX_ENV" --sitepackages -- -x test.test_auth

View File

@ -1,26 +1,15 @@
#!/bin/bash
#!/bin/sh
set -o xtrace # Write all commands first to stderr
set -o errexit # Exit the script with error if any of the commands fail
# Supported/used environment variables:
# AUTH Set to enable authentication. Defaults to "noauth"
# SSL Set to enable SSL. Defaults to "nossl"
# TOX_ENV Tox environment name, e.g. "synchro", required.
# PYTHON_BINARY Path to python, required.
# TOX_ENV Tox environment name, e.g. "tornado4-py36"
AUTH=${AUTH:-noauth}
SSL=${SSL:-nossl}
if [ -z $PYTHON_BINARY ]; then
echo "PYTHON_BINARY is undefined!"
exit 1
fi
if [ -z $TOX_ENV ]; then
echo "TOX_ENV is undefined!"
exit 1
fi
if [ "$AUTH" != "noauth" ]; then
export DB_USER="bob"
export DB_PASSWORD="pwd123"
@ -31,48 +20,5 @@ if [ "$SSL" != "nossl" ]; then
export CA_PEM="$DRIVERS_TOOLS/.evergreen/x509gen/ca.pem"
fi
if [ -f secrets-export.sh ]; then
source secrets-export.sh
fi
# Usage:
# createvirtualenv /path/to/python /output/path/for/venv
# * param1: Python binary to use for the virtualenv
# * param2: Path to the virtualenv to create
createvirtualenv () {
PYTHON=$1
VENVPATH=$2
if $PYTHON -m virtualenv --version; then
VIRTUALENV="$PYTHON -m virtualenv"
elif $PYTHON -m venv -h > /dev/null; then
# System virtualenv might not be compatible with the python3 on our path
VIRTUALENV="$PYTHON -m venv"
else
echo "Cannot test without virtualenv"
exit 1
fi
# Workaround for bug in older versions of virtualenv.
$VIRTUALENV $VENVPATH || $PYTHON -m venv $VENVPATH
if [ "Windows_NT" = "$OS" ]; then
# Workaround https://bugs.python.org/issue32451:
# mongovenv/Scripts/activate: line 3: $'\r': command not found
dos2unix $VENVPATH/Scripts/activate || true
. $VENVPATH/Scripts/activate
else
. $VENVPATH/bin/activate
fi
python -m pip install -q --upgrade pip
python -m pip install -q --upgrade tox
}
# Set up a virtualenv and install tox.
createvirtualenv "$PYTHON_BINARY" toxenv
trap "deactivate; rm -rf toxenv" EXIT HUP
python -m pip install tox
run_tox() {
python -m tox -m $TOX_ENV "$@"
}
run_tox "${@:1}"
# Run the tests, and store the results in Evergreen compatible XUnit XML
${TOX_BINARY} -e ${TOX_ENV}

View File

@ -1,2 +0,0 @@
# Initial pre-commit reformat
1e62b868ea58afeb42b3d0346e33776561c16ab6

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @mongodb/dbx-python

View File

@ -1,16 +0,0 @@
version: 2
updates:
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
# Python
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"

View File

@ -1,5 +0,0 @@
# List of reviewers for auto-assignment of reviews.
caseyclements
blink1073
Jibola
NoahStapp

View File

@ -1,76 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master", "*" ]
pull_request:
branches: [ "master", "*" ]
schedule:
- cron: '35 23 * * 5'
workflow_call:
inputs:
ref:
required: true
type: string
jobs:
analyze:
name: Analyze ${{ matrix.language }}
runs-on: ubuntu-latest
timeout-minutes: 360
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: python
- language: actions
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.x
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4
with:
languages: ${{ matrix.language }}
build-mode: none
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
queries: security-extended
config: |
paths-ignore:
- 'test/**'
- shell: bash
if: matrix.language == 'python'
run: |
pip install -e .
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4
with:
category: "/language:${{matrix.language}}"

View File

@ -1,43 +0,0 @@
name: Python Dist
concurrency:
group: dist-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
workflow_call:
inputs:
ref:
required: true
type: string
pull_request:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+.post[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+[a-b][0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
jobs:
build:
name: "Build Dist"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Install dependencies
run: pip install build
- name: Create packages
run: python -m build .
- name: Store package artifacts
uses: actions/upload-artifact@v7
with:
name: all-dist-${{ github.run_id }}
path: "dist/*"

View File

@ -1,115 +0,0 @@
name: Release
on:
workflow_dispatch:
inputs:
following_version:
description: "The post (dev) version to set"
dry_run:
description: "Dry Run?"
default: false
type: boolean
schedule:
- cron: '30 5 * * *'
env:
# Changes per repo
PRODUCT_NAME: Motor
# Changes per branch
EVERGREEN_PROJECT: motor
# Constant
# inputs will be empty on a scheduled run. so, we only set dry_run
# to 'false' when the input is set to 'false'.
DRY_RUN: ${{ ! contains(inputs.dry_run, 'false') }}
FOLLOWING_VERSION: ${{ inputs.following_version || '' }}
defaults:
run:
shell: bash -eux {0}
jobs:
pre-publish:
environment: release
if: github.repository_owner == 'mongodb' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
outputs:
version: ${{ steps.pre-publish.outputs.version }}
steps:
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
with:
app_id: ${{ vars.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: mongodb-labs/drivers-github-tools/setup@v3
with:
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
aws_region_name: ${{ vars.AWS_REGION_NAME }}
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
- uses: mongodb-labs/drivers-github-tools/python/pre-publish@v3
id: pre-publish
with:
dry_run: ${{ env.DRY_RUN }}
build-dist:
needs: [pre-publish]
uses: ./.github/workflows/dist.yml
with:
ref: ${{ needs.pre-publish.outputs.version }}
static-scan:
needs: [pre-publish]
uses: ./.github/workflows/codeql.yml
with:
ref: ${{ needs.pre-publish.outputs.version }}
publish:
needs: [build-dist, static-scan]
name: Upload release to PyPI
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v8
with:
name: all-dist-${{ github.run_id }}
path: dist/
- name: Publish package distributions to TestPyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true
attestations: ${{ env.DRY_RUN }}
- name: Publish package distributions to PyPI
if: startsWith(env.DRY_RUN, 'false')
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
post-publish:
needs: [publish]
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
contents: write
attestations: write
security-events: write
steps:
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
with:
app_id: ${{ vars.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: mongodb-labs/drivers-github-tools/setup@v3
with:
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
aws_region_name: ${{ vars.AWS_REGION_NAME }}
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
- uses: mongodb-labs/drivers-github-tools/python/post-publish@v3
with:
following_version: ${{ env.FOLLOWING_VERSION }}
product_name: ${{ env.PRODUCT_NAME }}
evergreen_project: ${{ env.EVERGREEN_PROJECT }}
token: ${{ github.token }}
dry_run: ${{ env.DRY_RUN }}

View File

@ -1,129 +0,0 @@
name: Python Tests
on:
push:
pull_request:
concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash -eux {0}
jobs:
build:
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.12", "3.14"]
fail-fast: false
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
allow-prereleases: true
- id: setup-mongodb
uses: mongodb-labs/drivers-evergreen-tools@master
with:
version: "8.0"
topology: replica_set
- name: Install Python dependencies
run: |
python -m pip install -U pip tox
- name: Run tests
run: |
tox -m test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.10'
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
- name: Install Python dependencies
run: |
python -m pip install -U pip tox
- name: Run linters
run: |
tox -m lint-manual
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.10'
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
- name: Install Python dependencies
run: |
python -m pip install -U pip tox
- name: Run docs
run: tox -m docs
- name: Run linkcheck
run: tox -m linkcheck
- name: Start MongoDB
uses: supercharge/mongodb-github-action@315db7fe45ac2880b7758f1933e6e5d59afd5e94 # 1.12.1
with:
mongodb-version: 5.0
- name: Run doctest
run: tox -m doctest
release:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.10'
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
- name: Install Python dependencies
run: |
python -m pip install -U pip
- name: Run the release script
run: |
bash release.sh
typing:
name: Typing Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
allow-prereleases: true
- name: Install dependencies
run: |
python -m pip install -U pip tox
- name: Run mypy
run: |
tox -m typecheck-mypy

View File

@ -1,21 +0,0 @@
name: GitHub Actions Security Analysis with zizmor 🌈
on:
push:
branches: ["master"]
pull_request:
branches: ["**"]
jobs:
zizmor:
name: zizmor latest via Cargo
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d

7
.github/zizmor.yml vendored
View File

@ -1,7 +0,0 @@
rules:
unpinned-uses:
config:
policies:
actions/*: ref-pin
mongodb-labs/drivers-github-tools/*: ref-pin
mongodb-labs/drivers-evergreen-tools: ref-pin

3
.gitignore vendored
View File

@ -12,6 +12,3 @@ setup.cfg
doc/_build/
.idea/
xunit-results
xunit-synchro-results
.eggs
toxenv

View File

@ -1,89 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-toml
- id: check-json
- id: check-yaml
exclude: template.yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: WHEEL
exclude_types: [json]
- id: forbid-new-submodules
- id: trailing-whitespace
exclude: .patch
exclude_types: [json]
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.3
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
- id: ruff-format
- repo: https://github.com/adamchainz/blacken-docs
rev: "1.16.0"
hooks:
- id: blacken-docs
additional_dependencies:
- black==22.3.0
- repo: https://github.com/pre-commit/pygrep-hooks
rev: "v1.10.0"
hooks:
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
- repo: https://github.com/rstcheck/rstcheck
rev: v6.2.0
hooks:
- id: rstcheck
additional_dependencies: [sphinx]
args: ["--ignore-directives=doctest,testsetup,todo,automodule,mongodoc,autodoc,testcleanup,autoclass","--ignore-substitutions=release", "--report-level=error"]
exclude: '^doc/migrate-to-motor-3.rst'
# We use the Python version instead of the original version which seems to require Docker
# https://github.com/koalaman/shellcheck-precommit
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
name: shellcheck
args: ["--severity=warning"]
stages: [manual]
- repo: https://github.com/PyCQA/doc8
rev: v1.1.1
hooks:
- id: doc8
args: ["--ignore=D001"] # ignore line length
stages: [manual]
- repo: https://github.com/sirosen/check-jsonschema
rev: 0.29.0
hooks:
- id: check-github-workflows
- id: check-github-actions
- id: check-dependabot
- repo: https://github.com/ariebovenberg/slotscheck
rev: v0.17.0
hooks:
- id: slotscheck
files: \.py$
exclude: "^(doc|test)/"
stages: [manual]
args: ["--no-strict-imports"]
- repo: https://github.com/codespell-project/codespell
rev: "v2.2.6"
hooks:
- id: codespell
args: ["-L", "fle"]

View File

@ -1,23 +0,0 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the doc/ directory with Sphinx
sphinx:
configuration: doc/conf.py
# Set the version of Python and requirements required to build the docs.
python:
install:
# Install motor itself.
- method: pip
path: .
- requirements: requirements/docs.txt
build:
os: ubuntu-22.04
tools:
python: "3.11"

View File

@ -1,68 +0,0 @@
# Contributing to Motor
Contributions are encouraged. Please read these guidelines before
sending a pull request.
## Bugfixes and New Features
Before starting to write code, look for existing tickets or create one
in [Jira](https://jira.mongodb.org/browse/MOTOR) for your specific issue
or feature request.
## Running Tests
Install a recent version of MongoDB and run it on the default port from
a clean data directory. Pass "--setParameter enableTestCommands=1" to
mongod to enable testing MotorCursor's `max_time_ms` method.
Control how the tests connect to MongoDB with these environment
variables:
- `DB_IP`: Defaults to "localhost", can be a domain name or IP
- `DB_PORT`: Defaults to 27017
- `DB_USER`, `DB_PASSWORD`: To test with authentication, create an
admin user and set these environment variables to the username and
password
- `CERT_DIR`: Path with alternate client.pem and ca.pem for testing.
Otherwise the suite uses those in test/certificates/.
Install [tox](https://testrun.org/tox/) and run it from the command line
in the repository directory. You will need a variety of Python
interpreters installed. For a minimal test, ensure you have your desired
Python version on your path, and run:
```bash
tox -m test
```
The doctests pass with Python 3.10+ and a MongoDB 5.0 instance running on
port 27017:
```bash
tox -m doctest
```
## Running Linters
Motor uses [pre-commit](https://pypi.org/project/pre-commit/) for
managing linting of the codebase. `pre-commit` performs various checks
on all files in Motor and uses tools that help follow a consistent code
style within the codebase.
To set up `pre-commit` locally, run:
```bash
pip install pre-commit # or brew install pre-commit for global install.
pre-commit install
```
To run `pre-commit` manually, run:
```bash
tox -m lint
```
## General Guidelines
- Avoid backward breaking changes if at all possible.
- Write inline documentation for new classes and methods.
- Add yourself to doc/contributors.rst :)

50
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,50 @@
Contributing to Motor
=====================
Contributions are encouraged. Please read these guidelines before sending a
pull request.
Bugfixes and New Features
-------------------------
Before starting to write code, look for existing tickets or create one in `Jira
<https://jira.mongodb.org/browse/MOTOR>`_ for your specific issue or feature
request.
Running Tests
-------------
Install a recent version of MongoDB and run it on the default port from a clean
data directory. Pass "--setParameter enableTestCommands=1" to mongod to enable
testing MotorCursor's ``max_time_ms`` method.
Control how the tests connect to MongoDB with these environment variables:
- ``DB_IP``: Defaults to "localhost", can be a domain name or IP
- ``DB_PORT``: Defaults to 27017
- ``DB_USER``: If auth is enabled the test suite creates an admin user by
default, or logs in to the admin database with the username provided
- ``DB_PASSWORD``: If auth is enabled the test suite creates an admin user by
default, or logs in to the admin database with the username provided
- ``CERT_DIR``: Path with alternate client.pem and ca.pem for testing.
Otherwise the suite uses those in test/certificates/.
Install `tox`_ and run it from the command line in the repository directory.
You will need a variety of Python interpreters installed. For a minimal test,
ensure you have Python 2.6 and 3.5, and run::
> tox -e tornado4-py26-min,tornado4-py35-min
The doctests pass with Python 3.5 and a MongoDB 3.2 instance running on
port 27017:
> tox -e py3-sphinx-doctest
.. _tox: https://testrun.org/tox/
General Guidelines
------------------
- Avoid backward breaking changes if at all possible.
- Write inline documentation for new classes and methods.
- Add yourself to doc/contributors.rst :)

View File

@ -199,3 +199,4 @@
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.

8
MANIFEST.in Normal file
View File

@ -0,0 +1,8 @@
include README.rst
include LICENSE
recursive-include doc *.rst
recursive-include doc *.py
recursive-include test *.py
recursive-include doc *.conf
recursive-include doc *.css
recursive-include doc *.js

209
README.md
View File

@ -1,209 +0,0 @@
# Motor
[![PyPI Version](https://img.shields.io/pypi/v/motor)](https://pypi.org/project/motor)
[![Python Versions](https://img.shields.io/pypi/pyversions/motor)](https://pypi.org/project/motor)
[![Monthly Downloads](https://static.pepy.tech/badge/motor/month)](https://pepy.tech/project/motor)
[![Documentation Status](https://readthedocs.org/projects/motor/badge/?version=stable)](http://motor.readthedocs.io/en/stable/?badge=stable)
![image](https://raw.github.com/mongodb/motor/master/doc/_static/motor.png)
> [!WARNING]
> As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
> No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
> After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
> We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
> For help transitioning, see the Migrate to PyMongo Async guide: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/.
## About
Motor is a full-featured, non-blocking [MongoDB](http://mongodb.org/)
driver for Python [asyncio](https://docs.python.org/3/library/asyncio.html) and
[Tornado](http://tornadoweb.org/) applications. Motor presents a coroutine-based API
for non-blocking access to MongoDB.
> "We use Motor in high throughput environments, processing tens of
> thousands of requests per second. It allows us to take full advantage
> of modern hardware, ensuring we utilise the entire capacity of our
> purchased CPUs. This helps us be more efficient with computing power,
> compute spend and minimises the environmental impact of our
> infrastructure as a result."
>
> --*David Mytton, Server Density*
>
> "We develop easy-to-use sensors and sensor systems with open source
> software to ensure every innovator, from school child to laboratory
> researcher, has the same opportunity to create. We integrate Motor
> into our software to guarantee massively scalable sensor systems for
> everyone."
>
> --*Ryan Smith, inXus Interactive*
## Support / Feedback
For issues with, questions about, or feedback for PyMongo, please look
into our [support channels](https://support.mongodb.com/welcome). Please
do not email any of the Motor developers directly with issues or
questions - you're more likely to get an answer on the
[StackOverflow](https://stackoverflow.com/questions/tagged/mongodb)
(using a "mongodb" tag).
## Bugs / Feature Requests
Think you've found a bug? Want to see a new feature in Motor? Please
open a case in our issue management tool, JIRA:
- [Create an account and login](https://jira.mongodb.org).
- Navigate to [the MOTOR
project](https://jira.mongodb.org/browse/MOTOR).
- Click **Create Issue** - Please provide as much information as
possible about the issue type and how to reproduce it.
Bug reports in JIRA for all driver projects (i.e. MOTOR, CSHARP, JAVA)
and the Core Server (i.e. SERVER) project are **public**.
### How To Ask For Help
Please include all of the following information when opening an issue:
- Detailed steps to reproduce the problem, including full traceback, if
possible.
- The exact python version used, with patch level:
```bash
python -c "import sys; print(sys.version)"
```
- The exact version of Motor used, with patch level:
```bash
python -c "import motor; print(motor.version)"
```
- The exact version of PyMongo used, with patch level:
```bash
python -c "import pymongo; print(pymongo.version); print(pymongo.has_c())"
```
- The exact Tornado version, if you are using Tornado:
```bash
python -c "import tornado; print(tornado.version)"
```
- The operating system and version (e.g. RedHat Enterprise Linux 6.4,
OSX 10.9.5, ...)
### Security Vulnerabilities
If you've identified a security vulnerability in a driver or any other
MongoDB project, please report it according to the [instructions
here](https://mongodb.com/docs/manual/tutorial/create-a-vulnerability-report).
## Installation
Motor can be installed with [pip](http://pypi.python.org/pypi/pip):
```bash
pip install motor
```
## Dependencies
Motor works in all the environments officially supported by Tornado or
by asyncio. It requires:
- Unix (including macOS) or Windows.
- [PyMongo](http://pypi.python.org/pypi/pymongo/) >=4.9,<5
- Python 3.10+
Optional dependencies:
Motor supports same optional dependencies as PyMongo. Required
dependencies can be installed along with Motor.
GSSAPI authentication requires `gssapi` extra dependency. The correct
dependency can be installed automatically along with Motor:
```bash
pip install "motor[gssapi]"
```
similarly,
MONGODB-AWS authentication requires `aws` extra dependency:
```bash
pip install "motor[aws]"
```
Support for mongodb+srv:// URIs requires `srv` extra dependency:
```bash
pip install "motor[srv]"
```
OCSP requires `ocsp` extra dependency:
```bash
pip install "motor[ocsp]"
```
Wire protocol compression with snappy requires `snappy` extra
dependency:
```bash
pip install "motor[snappy]"
```
Wire protocol compression with zstandard requires `zstd` extra
dependency:
```bash
pip install "motor[zstd]"
```
Client-Side Field Level Encryption requires `encryption` extra
dependency:
```bash
pip install "motor[encryption]"
```
You can install all dependencies automatically with the following
command:
```bash
pip install "motor[gssapi,aws,ocsp,snappy,srv,zstd,encryption]"
```
See
[requirements](https://motor.readthedocs.io/en/stable/requirements.html)
for details about compatibility.
## Examples
See the [examples on
ReadTheDocs](https://motor.readthedocs.io/en/stable/examples/index.html).
## Documentation
Motor's documentation is on
[ReadTheDocs](https://motor.readthedocs.io/en/stable/).
Build the documentation with Python 3.10+. Install
[sphinx](http://sphinx.pocoo.org/), [Tornado](http://tornadoweb.org/),
and [aiohttp](https://github.com/aio-libs/aiohttp), and do
`cd doc; make html`.
## Learning Resources
- MongoDB Learn - [Python
courses](https://learn.mongodb.com/catalog?labels=%5B%22Language%22%5D&values=%5B%22Python%22%5D).
- [Python Articles on Developer
Center](https://www.mongodb.com/developer/languages/python/).
## Testing
Run `python setup.py test`. Tests are located in the `test/` directory.

120
README.rst Normal file
View File

@ -0,0 +1,120 @@
=====
Motor
=====
.. image:: https://raw.github.com/mongodb/motor/master/doc/_static/motor.png
:Info: Motor is a full-featured, non-blocking MongoDB_ driver for Python
Tornado_ and asyncio_ applications.
:Author: A\. Jesse Jiryu Davis
About
=====
Motor presents a callback- or Future-based API for non-blocking access
to MongoDB. The source is `on GitHub <https://github.com/mongodb/motor>`_
and the docs are on ReadTheDocs_.
"We use Motor in high throughput environments, processing tens of thousands
of requests per second. It allows us to take full advantage of modern
hardware, ensuring we utilise the entire capacity of our purchased CPUs.
This helps us be more efficient with computing power, compute spend and
minimises the environmental impact of our infrastructure as a result."
--*David Mytton, Server Density*
"We develop easy-to-use sensors and sensor systems with open source
software to ensure every innovator, from school child to laboratory
researcher, has the same opportunity to create. We integrate Motor into our
software to guarantee massively scalable sensor systems for everyone."
--*Ryan Smith, inXus Interactive*
Installation
============
$ pip install motor
Dependencies
============
Motor works in all the environments officially supported by Tornado or by
asyncio. It requires:
* Unix, including Mac OS X. Windows is not supported.
* PyMongo_ 3.4 or later.
* Python 2.6 or later.
* `futures`_ on Python 2.6.
* `backports.pbkdf2`_ for faster authentication with MongoDB 3.0+,
especially on Python older than 2.7.8, or on Python 3 before Python 3.4.
See `requirements <https://motor.readthedocs.io/en/stable/requirements.html>`_
for details about compatibility.
How To Ask For Help
===================
Issues with, questions about, or feedback for Motor should be sent to the
`mongodb-user list on Google Groups`_.
For confirmed issues or feature requests,
open a case in `Jira <http://jira.mongodb.org>`_ in the "MOTOR" project.
Please include all of the following information:
- Detailed steps to reproduce the problem, including your code and a full
traceback, if possible.
- What you expected to happen, and what actually happened.
- The exact python version used, with patch level::
$ python -c "import sys; print(sys.version)"
- The exact version of PyMongo used:
$ python -c "import pymongo; print(pymongo.version); print(pymongo.has_c())"
- The exact Tornado version, if you are using Tornado::
$ python -c "import tornado; print(tornado.version)"
- The operating system and version (e.g. RedHat Enterprise Linux 6.4, OSX 10.9.5, ...)
Documentation
=============
Motor's documentation is on ReadTheDocs_.
Build the documentation with Python 3.5. Install sphinx, Tornado, and aiohttp,
and do ``cd doc; make html``.
Examples
========
See the `examples on ReadTheDocs <https://motor.readthedocs.io/en/latest/examples/index.html>`_.
Testing
=======
Run ``python setup.py test``.
Tests are located in the ``test/`` directory.
In Python 2.6, unittest2_ is automatically installed.
.. _PyMongo: http://pypi.python.org/pypi/pymongo/
.. _MongoDB: http://mongodb.org/
.. _Tornado: http://tornadoweb.org/
.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _futures: https://pypi.python.org/pypi/futures
.. _backports.pbkdf2: https://pypi.python.org/pypi/backports.pbkdf2/
.. _ReadTheDocs: https://motor.readthedocs.io/
.. _mongodb-user list on Google Groups:
https://groups.google.com/forum/?fromgroups#!forum/mongodb-user
.. _sphinx: http://sphinx.pocoo.org/
.. _unittest2: https://pypi.python.org/pypi/unittest2

View File

@ -1,71 +0,0 @@
# Motor Releases
## Versioning
Motor's version numbers follow [semantic
versioning](http://semver.org/): each version number is structured
"major.minor.patch". Patch releases fix bugs, minor releases add
features (and may fix bugs), and major releases include API changes that
break backwards compatibility (and may add features and fix bugs).
In between releases we add .devN to the version number to denote the
version under development. So if we just released 2.3.0, then the
current dev version might be 2.3.1.dev0 or 2.4.0.dev0. When we make the
next release we replace all instances of 2.x.x.devN in the docs with the
new version number.
<https://www.python.org/dev/peps/pep-0440/>
## Release Process
Motor ships a [pure Python
wheel](https://packaging.python.org/guides/distributing-packages-using-setuptools/#pure-python-wheels)
and a [source
distribution](https://packaging.python.org/guides/distributing-packages-using-setuptools/#source-distributions).
1. Motor is tested on Evergreen. Ensure that the latest commit is
passing CI as expected:
<https://evergreen.mongodb.com/waterfall/motor>.
2. Check JIRA to ensure all the tickets in this version have been
completed.
3. Add release notes to `doc/changelog.rst`. Generally just
summarize/clarify the git log, but you might add some more long form
notes for big changes.
4. Replace the `devN` version number w/ the new version number (see
note above in [Versioning](#versioning)) in `motor/_version.py`.
Commit the change and tag the release. Immediately bump the version
number to `dev0` in a new commit:
$ # Bump to release version number
$ git commit -a -m "BUMP <release version number>"
$ git tag -a "<release version number>" -m "BUMP <release version number>"
$ # Bump to dev version number
$ git commit -a -m "BUMP <dev version number>"
$ git push
$ git push --tags
5. Bump the version number to `<next version>.dev0` in
`motor/_version.py`, commit, then push.
6. Authorize the deployment for the tagged version on the release
GitHub Action and wait for it to successfully publish to PyPI.
7. Make sure the new version appears on
<https://motor.readthedocs.io/>. If the new version does not show up
automatically, trigger a rebuild of "latest":
<https://readthedocs.org/projects/motor/builds/>
8. Publish the release version in Jira and add a brief description
about the reason for the release or the main feature.
9. Announce the release on:
<https://www.mongodb.com/community/forums/c/announcements/driver-releases>
10. Create a GitHub Release for the tag using
<https://github.com/mongodb/motor/releases/new>. The title should be
"Motor X.Y.Z", and the description should contain a link to the
release notes on the the community forum, e.g. "Release notes:
mongodb.com/community/forums/t/motor-2-5-1-released/120313."

View File

@ -0,0 +1 @@

View File

@ -1,12 +1,6 @@
:mod:`motor.aiohttp` - Integrate Motor with the aiohttp web framework
=====================================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.aiohttp
.. automodule:: motor.aiohttp

View File

@ -1,12 +1,6 @@
asyncio GridFS Classes
======================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_asyncio
Store blobs of data in `GridFS <http://dochub.mongodb.org/core/gridfs>`_.
@ -14,8 +8,421 @@ Store blobs of data in `GridFS <http://dochub.mongodb.org/core/gridfs>`_.
.. seealso:: :ref:`Differences between PyMongo's and Motor's GridFS APIs
<gridfs-differences>`.
.. autoclass:: AsyncIOMotorGridFSBucket
.. class:: AsyncIOMotorGridFSBucket
Create a new instance of :class:`AsyncIOMotorGridFSBucket`.
Raises :exc:`TypeError` if `database` is not an instance of
:class:`AsyncIOMotorDatabase`.
Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern`
is not acknowledged.
:Parameters:
- `database`: database to use.
- `bucket_name` (optional): The name of the bucket. Defaults to 'fs'.
- `chunk_size_bytes` (optional): The chunk size in bytes. Defaults
to 255KB.
- `write_concern` (optional): The
:class:`~pymongo.write_concern.WriteConcern` to use. If ``None``
(the default) db.write_concern is used.
- `read_preference` (optional): The read preference to use. If
``None`` (the default) db.read_preference is used.
.. mongodoc:: gridfs
.. coroutinemethod:: delete(self, file_id)
Delete a file's metadata and data chunks from a GridFS bucket::
async def delete():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
# Get _id of file to delete
file_id = await fs.upload_from_stream("test_file",
b"data I want to store!")
await fs.delete(file_id)
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be deleted.
.. coroutinemethod:: download_to_stream(self, file_id, destination)
Downloads the contents of the stored file specified by file_id and
writes the contents to `destination`::
async def download():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
# Get _id of file to read
file_id = await fs.upload_from_stream("test_file",
b"data I want to store!")
# Get file to write to
file = open('myfile','wb+')
await fs.download_to_stream(file_id, file)
file.seek(0)
contents = file.read()
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be downloaded.
- `destination`: a file-like object implementing :meth:`write`.
.. coroutinemethod:: download_to_stream_by_name(self, filename, destination, revision=-1)
Write the contents of `filename` (with optional `revision`) to
`destination`.
For example::
async def download_by_name():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
# Get file to write to
file = open('myfile','wb')
await fs.download_to_stream_by_name("test_file", file)
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `filename`: The name of the file to read from.
- `destination`: A file-like object that implements :meth:`write`.
- `revision` (optional): Which revision (documents with the same
filename and different uploadDate) of the file to retrieve.
Defaults to -1 (the most recent revision).
:Note: Revision numbers are defined as follows:
- 0 = the original stored file
- 1 = the first revision
- 2 = the second revision
- etc...
- -2 = the second most recent revision
- -1 = the most recent revision
.. method:: find(self, *args, **kwargs)
Find and return the files collection documents that match ``filter``.
Returns a cursor that iterates across files matching
arbitrary queries on the files collection. Can be combined
with other modifiers for additional control.
For example::
async def find():
cursor = fs.find({"filename": "lisa.txt"},
no_cursor_timeout=True)
async for grid_data in cursor:
data = grid_data.read()
iterates through all versions of "lisa.txt" stored in GridFS.
Setting no_cursor_timeout may be important to
prevent the cursor from timing out during long multi-file processing
work.
As another example, the call::
most_recent_three = fs.find().sort("uploadDate", -1).limit(3)
returns a cursor to the three most recently uploaded files in GridFS.
Follows a similar interface to :meth:`~AsyncIOMotorCollection.find`
in :class:`AsyncIOMotorCollection`.
:Parameters:
- `filter`: Search query.
- `batch_size` (optional): The number of documents to return per
batch.
- `limit` (optional): The maximum number of documents to return.
- `no_cursor_timeout` (optional): The server normally times out idle
cursors after an inactivity period (10 minutes) to prevent excess
memory use. Set this option to True prevent that.
- `skip` (optional): The number of documents to skip before
returning.
- `sort` (optional): The order by which to sort results. Defaults to
None.
.. coroutinemethod:: open_download_stream(self, file_id)
Opens a stream to read the contents of the stored file specified by file_id::
async def download_stream():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
# get _id of file to read.
file_id = await fs.upload_from_stream("test_file",
b"data I want to store!")
grid_out = await fs.open_download_stream(file_id)
contents = await grid_out.read()
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be downloaded.
Returns a :class:`AsyncIOMotorGridOut`.
.. coroutinemethod:: open_download_stream_by_name(self, filename, revision=-1)
Opens a stream to read the contents of `filename` and optional `revision`::
async def download_by_name():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
# get _id of file to read.
file_id = await fs.upload_from_stream("test_file",
b"data I want to store!")
grid_out = await fs.open_download_stream_by_name(file_id)
contents = await grid_out.read()
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` filename is not a string.
:Parameters:
- `filename`: The name of the file to read from.
- `revision` (optional): Which revision (documents with the same
filename and different uploadDate) of the file to retrieve.
Defaults to -1 (the most recent revision).
Returns a :class:`AsyncIOMotorGridOut`.
:Note: Revision numbers are defined as follows:
- 0 = the original stored file
- 1 = the first revision
- 2 = the second revision
- etc...
- -2 = the second most recent revision
- -1 = the most recent revision
.. method:: open_upload_stream(self, filename, chunk_size_bytes=None, metadata=None)
Opens a stream for writing.
Specify the filename, and add any additional information in the metadata
field of the file document or modify the chunk size::
async def upload():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
grid_in, file_id = fs.open_upload_stream(
"test_file", chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
await grid_in.write(b"data I want to store!")
await grid_in.close() # uploaded on close
Returns an instance of :class:`AsyncIOMotorGridIn`.
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
In a Python 3.5 native coroutine, the "async with" statement calls
:meth:`~AsyncIOMotorGridIn.close` automatically::
async def upload():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
async with await fs.new_file() as gridin:
await gridin.write(b'First part\n')
await gridin.write(b'Second part')
# gridin is now closed automatically.
:Parameters:
- `filename`: The name of the file to upload.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes in :class:`AsyncIOMotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
.. method:: open_upload_stream_with_id(self, file_id, filename, chunk_size_bytes=None, metadata=None)
Opens a stream for writing.
Specify the filed_id and filename, and add any additional information in
the metadata field of the file document, or modify the chunk size::
async def upload():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
grid_in, file_id = fs.open_upload_stream_with_id(
ObjectId(),
"test_file",
chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
await grid_in.write(b"data I want to store!")
await grid_in.close() # uploaded on close
Returns an instance of :class:`AsyncIOMotorGridIn`.
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `file_id`: The id to use for this file. The id must not have
already been used for another file.
- `filename`: The name of the file to upload.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes in :class:`AsyncIOMotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
.. coroutinemethod:: rename(self, file_id, new_filename)
Renames the stored file with the specified file_id.
For example::
async def rename():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
# get _id of file to read.
file_id = await fs.upload_from_stream("test_file",
b"data I want to store!")
await fs.rename(file_id, "new_test_name")
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be renamed.
- `new_filename`: The new name of the file.
.. coroutinemethod:: upload_from_stream(self, filename, source, chunk_size_bytes=None, metadata=None)
Uploads a user file to a GridFS bucket.
Reads the contents of the user file from `source` and uploads
it to the file `filename`. Source can be a string or file-like object.
For example::
async def upload_from_stream():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
file_id = await fs.upload_from_stream(
"test_file",
b"data I want to store!",
chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `filename`: The name of the file to upload.
- `source`: The source stream of the content to be uploaded. Must be
a file-like object that implements :meth:`read` or a string.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes of :class:`AsyncIOMotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
Returns the _id of the uploaded file.
.. coroutinemethod:: upload_from_stream_with_id(self, file_id, filename, source, chunk_size_bytes=None, metadata=None)
Uploads a user file to a GridFS bucket with a custom file id.
Reads the contents of the user file from `source` and uploads
it to the file `filename`. Source can be a string or file-like object.
For example::
async def upload_from_stream_with_id():
my_db = AsyncIOMotorClient().test
fs = AsyncIOMotorGridFSBucket(my_db)
file_id = await fs.upload_from_stream_with_id(
ObjectId(),
"test_file",
b"data I want to store!",
chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `file_id`: The id to use for this file. The id must not have
already been used for another file.
- `filename`: The name of the file to upload.
- `source`: The source stream of the content to be uploaded. Must be
a file-like object that implements :meth:`read` or a string.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes of :class:`AsyncIOMotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
.. autoclass:: AsyncIOMotorGridFS
:members:
:exclude-members: find_one, put
.. coroutinemethod:: find_one(self, filter=None, *args, **kwargs)
Get a single file from gridfs.
All arguments to :meth:`find` are also valid arguments for
:meth:`find_one`, although any `limit` argument will be
ignored. Returns a single :class:`AsyncIOMotorGridOut`,
or ``None`` if no matching file is found. For example::
file = await fs.find_one({"filename": "lisa.txt"})
:Parameters:
- `filter` (optional): a dictionary specifying
the query to be performing OR any other type to be used as
the value for a query for ``"_id"`` in the file collection.
- `*args` (optional): any additional positional arguments are
the same as the arguments to :meth:`find`.
- `**kwargs` (optional): any additional keyword arguments
are the same as the arguments to :meth:`find`.
.. coroutinemethod:: put(self, data, **kwargs)
Put data in GridFS as a new file.
Equivalent to doing::
try:
f = await fs.new_file(**kwargs)
await f.write(data)
finally:
await f.close()
`data` can be a :class:`bytes` instance or a file-like object providing a :meth:`read` method.
If an `encoding` keyword argument is passed, `data` can also be a
:class:`str`, which will be encoded as `encoding` before being written. Any keyword arguments
will be passed through to the created file - see
:class:`AsyncIOMotorGridIn` for possible arguments. Returns the
``"_id"`` of the created file.
If the ``"_id"`` of the file is manually specified, it must
not already exist in GridFS. Otherwise
:class:`~gridfs.errors.FileExists` is raised.
:Parameters:
- `data`: data to be written as a file.
- `**kwargs` (optional): keyword arguments for file creation
.. autoclass:: AsyncIOMotorGridIn
:members:

View File

@ -1,13 +0,0 @@
:class:`~motor.motor_asyncio.AsyncIOMotorChangeStream`
======================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_asyncio
.. autoclass:: AsyncIOMotorChangeStream
:members:

View File

@ -1,12 +1,6 @@
:class:`~motor.motor_asyncio.AsyncIOMotorClient` -- Connection to MongoDB
=========================================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. autoclass:: motor.motor_asyncio.AsyncIOMotorClient
:members:

View File

@ -1,13 +0,0 @@
:class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption`
==========================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_asyncio
.. autoclass:: AsyncIOMotorClientEncryption
:members:

View File

@ -1,11 +0,0 @@
:class:`~motor.motor_asyncio.AsyncIOMotorClientSession` -- Sequence of operations
=================================================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. autoclass:: motor.motor_asyncio.AsyncIOMotorClientSession
:members:

View File

@ -1,16 +1,11 @@
:class:`~motor.motor_asyncio.AsyncIOMotorCollection`
====================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_asyncio
.. autoclass:: AsyncIOMotorCollection
:members:
:exclude-members: create_index, inline_map_reduce
.. describe:: c[name] || c.name
@ -23,3 +18,110 @@
The :class:`AsyncIOMotorDatabase` that this
:class:`AsyncIOMotorCollection` is a part of.
.. coroutinemethod:: create_index(self, keys, **kwargs)
Creates an index on this collection.
Takes either a single key or a list of (key, direction) pairs.
The key(s) must be an instance of :class:`basestring`
(:class:`str` in python 3), and the direction(s) must be one of
(:data:`~pymongo.ASCENDING`, :data:`~pymongo.DESCENDING`,
:data:`~pymongo.GEO2D`, :data:`~pymongo.GEOHAYSTACK`,
:data:`~pymongo.GEOSPHERE`, :data:`~pymongo.HASHED`,
:data:`~pymongo.TEXT`).
To create a single key ascending index on the key ``'mike'`` we just
use a string argument::
await my_collection.create_index("mike")
For a compound index on ``'mike'`` descending and ``'eliot'``
ascending we need to use a list of tuples::
await my_collection.create_index([("mike", pymongo.DESCENDING),
("eliot", pymongo.ASCENDING)])
All optional index creation parameters should be passed as
keyword arguments to this method. For example::
await my_collection.create_index([("mike", pymongo.DESCENDING)],
background=True)
Valid options include, but are not limited to:
- `name`: custom name to use for this index - if none is
given, a name will be generated.
- `unique`: if ``True`` creates a uniqueness constraint on the index.
- `background`: if ``True`` this index should be created in the
background.
- `sparse`: if ``True``, omit from the index any documents that lack
the indexed field.
- `bucketSize`: for use with geoHaystack indexes.
Number of documents to group together within a certain proximity
to a given longitude and latitude.
- `min`: minimum value for keys in a :data:`~pymongo.GEO2D`
index.
- `max`: maximum value for keys in a :data:`~pymongo.GEO2D`
index.
- `expireAfterSeconds`: <int> Used to create an expiring (TTL)
collection. MongoDB will automatically delete documents from
this collection after <int> seconds. The indexed field must
be a UTC datetime or the data will not expire.
- `partialFilterExpression`: A document that specifies a filter for
a partial index.
- `collation` (optional): An instance of
:class:`~pymongo.collation.Collation`. This option is only supported
on MongoDB 3.4 and above.
See the MongoDB documentation for a full list of supported options by
server version.
.. warning:: `dropDups` is not supported by MongoDB 3.0 or newer. The
option is silently ignored by the server and unique index builds
using the option will fail if a duplicate value is detected.
.. note:: `partialFilterExpression` requires server version **>= 3.2**
.. note:: The :attr:`~pymongo.collection.Collection.write_concern` of
this collection is automatically applied to this operation when using
MongoDB >= 3.4.
:Parameters:
- `keys`: a single key or a list of (key, direction)
pairs specifying the index to create
- `**kwargs` (optional): any additional index creation
options (see the above list) should be passed as keyword
arguments
.. mongodoc:: indexes
.. coroutinemethod:: inline_map_reduce(self, map, reduce, full_response=False, **kwargs)
Perform an inline map/reduce operation on this collection.
Perform the map/reduce operation on the server in RAM. A result
collection is not created. The result set is returned as a list
of documents.
If `full_response` is ``False`` (default) returns the
result documents in a list. Otherwise, returns the full
response from the server to the `map reduce command`_.
The :meth:`inline_map_reduce` method obeys the :attr:`read_preference`
of this :class:`Collection`.
:Parameters:
- `map`: map function (as a JavaScript string)
- `reduce`: reduce function (as a JavaScript string)
- `full_response` (optional): if ``True``, return full response to
this command - otherwise just return the result collection
- `**kwargs` (optional): additional arguments to the
`map reduce command`_ may be passed as keyword arguments to this
helper method, e.g.::
await db.test.inline_map_reduce(map, reduce, limit=2)
.. _map reduce command: http://docs.mongodb.org/manual/reference/command/mapReduce/
.. mongodoc:: mapreduce

View File

@ -0,0 +1,7 @@
:class:`~motor.motor_asyncio.AsyncIOMotorCursor`
================================================
.. currentmodule:: motor.motor_asyncio
.. autoclass:: AsyncIOMotorCursor
:members:

View File

@ -1,12 +1,6 @@
:class:`~motor.motor_asyncio.AsyncIOMotorDatabase`
==================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_asyncio
.. autoclass:: AsyncIOMotorDatabase

View File

@ -1,23 +0,0 @@
:class:`~motor.motor_asyncio.AsyncIOMotorCursor`
================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_asyncio
.. autoclass:: AsyncIOMotorCursor
:members:
:inherited-members:
:class:`~motor.motor_asyncio.AsyncIOMotorCommandCursor`
=======================================================
.. currentmodule:: motor.motor_asyncio
.. autoclass:: AsyncIOMotorCommandCursor
:members:
:inherited-members:

View File

@ -1,21 +1,12 @@
Motor asyncio API
=================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. toctree::
asyncio_motor_client
asyncio_motor_client_session
asyncio_motor_database
asyncio_motor_collection
asyncio_motor_change_stream
asyncio_motor_client_encryption
cursors
asyncio_motor_cursor
asyncio_gridfs
aiohttp
@ -23,3 +14,4 @@ Motor asyncio API
This page describes using Motor with asyncio. For Tornado integration, see
:doc:`../api-tornado/index`.

View File

@ -1,23 +0,0 @@
:class:`~motor.motor_tornado.MotorCursor`
=========================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorCursor
:members:
:inherited-members:
:class:`~motor.motor_tornado.MotorCommandCursor`
================================================
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorCommandCursor
:members:
:inherited-members:

View File

@ -1,12 +1,6 @@
Motor GridFS Classes
====================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
Store blobs of data in `GridFS <http://dochub.mongodb.org/core/gridfs>`_.
@ -16,8 +10,470 @@ Store blobs of data in `GridFS <http://dochub.mongodb.org/core/gridfs>`_.
.. seealso:: :doc:`web`
.. autoclass:: MotorGridFSBucket
.. class:: MotorGridFSBucket
Create a new instance of :class:`MotorGridFSBucket`.
Raises :exc:`TypeError` if `database` is not an instance of
:class:`MotorDatabase`.
Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern`
is not acknowledged.
:Parameters:
- `database`: database to use.
- `bucket_name` (optional): The name of the bucket. Defaults to 'fs'.
- `chunk_size_bytes` (optional): The chunk size in bytes. Defaults
to 255KB.
- `write_concern` (optional): The
:class:`~pymongo.write_concern.WriteConcern` to use. If ``None``
(the default) db.write_concern is used.
- `read_preference` (optional): The read preference to use. If
``None`` (the default) db.read_preference is used.
.. mongodoc:: gridfs
.. coroutinemethod:: delete(self, file_id, callback=None)
Delete a file's metadata and data chunks from a GridFS bucket::
@gen.coroutine
def delete():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
# Get _id of file to delete
file_id = yield fs.upload_from_stream("test_file",
b"data I want to store!")
yield fs.delete(file_id)
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be deleted.
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future.
.. coroutinemethod:: download_to_stream(self, file_id, destination, callback=None)
Downloads the contents of the stored file specified by file_id and
writes the contents to `destination`::
@gen.coroutine
def download():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
# Get _id of file to read
file_id = yield fs.upload_from_stream("test_file",
b"data I want to store!")
# Get file to write to
file = open('myfile','wb+')
yield fs.download_to_stream(file_id, file)
file.seek(0)
contents = file.read()
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be downloaded.
- `destination`: a file-like object implementing :meth:`write`.
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future.
.. coroutinemethod:: download_to_stream_by_name(self, filename, destination, revision=-1, callback=None)
Write the contents of `filename` (with optional `revision`) to
`destination`.
For example::
@gen.coroutine
def download_by_name():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
# Get file to write to
file = open('myfile','wb')
yield fs.download_to_stream_by_name("test_file", file)
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `filename`: The name of the file to read from.
- `destination`: A file-like object that implements :meth:`write`.
- `revision` (optional): Which revision (documents with the same
filename and different uploadDate) of the file to retrieve.
Defaults to -1 (the most recent revision).
:Note: Revision numbers are defined as follows:
- 0 = the original stored file
- 1 = the first revision
- 2 = the second revision
- etc...
- -2 = the second most recent revision
- -1 = the most recent revision
.. method:: find(self, *args, **kwargs)
Find and return the files collection documents that match ``filter``.
Returns a cursor that iterates across files matching
arbitrary queries on the files collection. Can be combined
with other modifiers for additional control.
For example::
@gen.coroutine
def find():
cursor = fs.find({"filename": "lisa.txt"},
no_cursor_timeout=True)
while (yield cursor.fetch_next):
grid_data = cursor.next_object()
data = grid_data.read()
iterates through all versions of "lisa.txt" stored in GridFS.
Setting no_cursor_timeout may be important to
prevent the cursor from timing out during long multi-file processing
work.
As another example, the call::
most_recent_three = fs.find().sort("uploadDate", -1).limit(3)
returns a cursor to the three most recently uploaded files in GridFS.
Follows a similar interface to :meth:`~MotorCollection.find`
in :class:`MotorCollection`.
:Parameters:
- `filter`: Search query.
- `batch_size` (optional): The number of documents to return per
batch.
- `limit` (optional): The maximum number of documents to return.
- `no_cursor_timeout` (optional): The server normally times out idle
cursors after an inactivity period (10 minutes) to prevent excess
memory use. Set this option to True prevent that.
- `skip` (optional): The number of documents to skip before
returning.
- `sort` (optional): The order by which to sort results. Defaults to
None.
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future.
.. coroutinemethod:: open_download_stream(self, file_id, callback=None)
Opens a stream to read the contents of the stored file specified by file_id::
@gen.coroutine
def download_stream():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
# get _id of file to read.
file_id = yield fs.upload_from_stream("test_file",
b"data I want to store!")
grid_out = yield fs.open_download_stream(file_id)
contents = yield grid_out.read()
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be downloaded.
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future that resolves
to a :class:`MotorGridOut`.
.. coroutinemethod:: open_download_stream_by_name(self, filename, revision=-1, callback=None)
Opens a stream to read the contents of `filename` and optional `revision`::
@gen.coroutine
def download_by_name():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
# get _id of file to read.
file_id = yield fs.upload_from_stream("test_file",
b"data I want to store!")
grid_out = yield fs.open_download_stream_by_name(file_id)
contents = yield grid_out.read()
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` filename is not a string.
:Parameters:
- `filename`: The name of the file to read from.
- `revision` (optional): Which revision (documents with the same
filename and different uploadDate) of the file to retrieve.
Defaults to -1 (the most recent revision).
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future that resolves
to a :class:`MotorGridOut`.
:Note: Revision numbers are defined as follows:
- 0 = the original stored file
- 1 = the first revision
- 2 = the second revision
- etc...
- -2 = the second most recent revision
- -1 = the most recent revision
.. method:: open_upload_stream(self, filename, chunk_size_bytes=None, metadata=None)
Opens a stream for writing.
Specify the filename, and add any additional information in the metadata
field of the file document or modify the chunk size::
@gen.coroutine
def upload():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
grid_in, file_id = fs.open_upload_stream(
"test_file", chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
yield grid_in.write(b"data I want to store!")
yield grid_in.close() # uploaded on close
Returns an instance of :class:`MotorGridIn`.
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
In a Python 3.5 native coroutine, the "async with" statement calls
:meth:`~MotorGridIn.close` automatically::
async def upload():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
async with await fs.new_file() as gridin:
await gridin.write(b'First part\n')
await gridin.write(b'Second part')
# gridin is now closed automatically.
:Parameters:
- `filename`: The name of the file to upload.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes in :class:`MotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
.. method:: open_upload_stream_with_id(self, file_id, filename, chunk_size_bytes=None, metadata=None)
Opens a stream for writing.
Specify the filed_id and filename, and add any additional information in
the metadata field of the file document, or modify the chunk size::
@gen.coroutine
def upload():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
grid_in, file_id = fs.open_upload_stream_with_id(
ObjectId(),
"test_file",
chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
yield grid_in.write(b"data I want to store!")
yield grid_in.close() # uploaded on close
Returns an instance of :class:`MotorGridIn`.
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `file_id`: The id to use for this file. The id must not have
already been used for another file.
- `filename`: The name of the file to upload.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes in :class:`MotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
.. coroutinemethod:: rename(self, file_id, new_filename, callback=None)
Renames the stored file with the specified file_id.
For example::
@gen.coroutine
def rename():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
# get _id of file to read.
file_id = yield fs.upload_from_stream("test_file",
b"data I want to store!")
yield fs.rename(file_id, "new_test_name")
Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists.
:Parameters:
- `file_id`: The _id of the file to be renamed.
- `new_filename`: The new name of the file.
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future.
.. coroutinemethod:: upload_from_stream(self, filename, source, chunk_size_bytes=None, metadata=None, callback=None)
Uploads a user file to a GridFS bucket.
Reads the contents of the user file from `source` and uploads
it to the file `filename`. Source can be a string or file-like object.
For example::
@gen.coroutine
def upload_from_stream():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
file_id = yield fs.upload_from_stream(
"test_file",
b"data I want to store!",
chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `filename`: The name of the file to upload.
- `source`: The source stream of the content to be uploaded. Must be
a file-like object that implements :meth:`read` or a string.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes of :class:`MotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future that resolves
to the _id of the uploaded file.
.. coroutinemethod:: upload_from_stream_with_id(self, file_id, filename, source, chunk_size_bytes=None, metadata=None, callback=None)
Uploads a user file to a GridFS bucket with a custom file id.
Reads the contents of the user file from `source` and uploads
it to the file `filename`. Source can be a string or file-like object.
For example::
@gen.coroutine
def upload_from_stream_with_id():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
file_id = yield fs.upload_from_stream_with_id(
ObjectId(),
"test_file",
b"data I want to store!",
chunk_size_bytes=4,
metadata={"contentType": "text/plain"})
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
:Parameters:
- `file_id`: The id to use for this file. The id must not have
already been used for another file.
- `filename`: The name of the file to upload.
- `source`: The source stream of the content to be uploaded. Must be
a file-like object that implements :meth:`read` or a string.
- `chunk_size_bytes` (options): The number of bytes per chunk of this
file. Defaults to the chunk_size_bytes of :class:`MotorGridFSBucket`.
- `metadata` (optional): User data for the 'metadata' field of the
files collection document. If not provided the metadata field will
be omitted from the files collection document.
- `callback`: (optional): function taking (result, error), executed
when operation completes
If a callback is passed, returns None, else returns a Future.
.. autoclass:: MotorGridFS
:members:
:exclude-members: find_one, put
.. coroutinemethod:: find_one(self, filter=None, *args, callback=None, **kwargs)
Get a single file from gridfs.
All arguments to :meth:`find` are also valid arguments for
:meth:`find_one`, although any `limit` argument will be
ignored. Returns a single :class:`MotorGridOut`,
or ``None`` if no matching file is found. For example::
file = yield fs.find_one({"filename": "lisa.txt"})
:Parameters:
- `filter` (optional): a dictionary specifying
the query to be performing OR any other type to be used as
the value for a query for ``"_id"`` in the file collection.
- `*args` (optional): any additional positional arguments are
the same as the arguments to :meth:`find`.
- `callback`: (optional): function taking (result, error), executed
when operation completes
- `**kwargs` (optional): any additional keyword arguments
are the same as the arguments to :meth:`find`.
If a callback is passed, returns None, else returns a Future.
.. coroutinemethod:: put(self, data, callback=None, **kwargs)
Put data in GridFS as a new file.
Equivalent to doing::
try:
f = yield fs.new_file(**kwargs)
yield f.write(data)
finally:
yield f.close()
`data` can be a :class:`bytes` instance or a file-like object providing a :meth:`read` method.
If an `encoding` keyword argument is passed, `data` can also be a
:class:`unicode` (:class:`str` in python 3) instance, which will
be encoded as `encoding` before being written. Any keyword arguments
will be passed through to the created file - see
:class:`MotorGridIn` for possible arguments. Returns the
``"_id"`` of the created file.
If the ``"_id"`` of the file is manually specified, it must
not already exist in GridFS. Otherwise
:class:`~gridfs.errors.FileExists` is raised.
:Parameters:
- `data`: data to be written as a file.
- `callback`: (optional): function taking (result, error), executed
when operation completes
- `**kwargs` (optional): keyword arguments for file creation
If a callback is passed, returns None, else returns a Future.
.. autoclass:: MotorGridIn
:members:

View File

@ -1,21 +1,12 @@
Motor Tornado API
=================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. toctree::
motor_client
motor_client_session
motor_database
motor_collection
motor_change_stream
motor_client_encryption
cursors
motor_cursor
gridfs
web

View File

@ -1,13 +0,0 @@
:class:`~motor.motor_tornado.MotorChangeStream`
===============================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorChangeStream
:members:

View File

@ -1,12 +1,6 @@
:class:`~motor.motor_tornado.MotorClient` -- Connection to MongoDB
==================================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorClient

View File

@ -1,13 +0,0 @@
:class:`~motor.motor_tornado.MotorClientEncryption`
===================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorClientEncryption
:members:

View File

@ -1,13 +0,0 @@
:class:`~motor.motor_tornado.MotorClientSession` -- Sequence of operations
==========================================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
.. autoclass:: motor.motor_tornado.MotorClientSession
:members:

View File

@ -1,16 +1,11 @@
:class:`~motor.motor_tornado.MotorCollection`
=============================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorCollection
:members:
:exclude-members: create_index, inline_map_reduce
.. describe:: c[name] || c.name
@ -23,3 +18,118 @@
The :class:`MotorDatabase` that this
:class:`MotorCollection` is a part of.
.. coroutinemethod:: create_index(self, keys, callback=None, **kwargs)
Creates an index on this collection.
Takes either a single key or a list of (key, direction) pairs.
The key(s) must be an instance of :class:`basestring`
(:class:`str` in python 3), and the direction(s) must be one of
(:data:`~pymongo.ASCENDING`, :data:`~pymongo.DESCENDING`,
:data:`~pymongo.GEO2D`, :data:`~pymongo.GEOHAYSTACK`,
:data:`~pymongo.GEOSPHERE`, :data:`~pymongo.HASHED`,
:data:`~pymongo.TEXT`).
To create a single key ascending index on the key ``'mike'`` we just
use a string argument::
yield my_collection.create_index("mike")
For a compound index on ``'mike'`` descending and ``'eliot'``
ascending we need to use a list of tuples::
yield my_collection.create_index([("mike", pymongo.DESCENDING),
("eliot", pymongo.ASCENDING)])
All optional index creation parameters should be passed as
keyword arguments to this method. For example::
yield my_collection.create_index([("mike", pymongo.DESCENDING)],
background=True)
Valid options include, but are not limited to:
- `name`: custom name to use for this index - if none is
given, a name will be generated.
- `unique`: if ``True`` creates a uniqueness constraint on the index.
- `background`: if ``True`` this index should be created in the
background.
- `sparse`: if ``True``, omit from the index any documents that lack
the indexed field.
- `bucketSize`: for use with geoHaystack indexes.
Number of documents to group together within a certain proximity
to a given longitude and latitude.
- `min`: minimum value for keys in a :data:`~pymongo.GEO2D`
index.
- `max`: maximum value for keys in a :data:`~pymongo.GEO2D`
index.
- `expireAfterSeconds`: <int> Used to create an expiring (TTL)
collection. MongoDB will automatically delete documents from
this collection after <int> seconds. The indexed field must
be a UTC datetime or the data will not expire.
- `partialFilterExpression`: A document that specifies a filter for
a partial index.
- `collation` (optional): An instance of
:class:`~pymongo.collation.Collation`. This option is only supported
on MongoDB 3.4 and above.
See the MongoDB documentation for a full list of supported options by
server version.
.. warning:: `dropDups` is not supported by MongoDB 3.0 or newer. The
option is silently ignored by the server and unique index builds
using the option will fail if a duplicate value is detected.
.. note:: `partialFilterExpression` requires server version **>= 3.2**
.. note:: The :attr:`~pymongo.collection.Collection.write_concern` of
this collection is automatically applied to this operation when using
MongoDB >= 3.4.
:Parameters:
- `keys`: a single key or a list of (key, direction)
pairs specifying the index to create
- `callback`: (optional): function taking (result, error), executed
when operation completes
- `**kwargs` (optional): any additional index creation
options (see the above list) should be passed as keyword
arguments
If a callback is passed, returns None, else returns a Future.
.. mongodoc:: indexes
.. coroutinemethod:: inline_map_reduce(self, map, reduce, full_response=False, callback=None, **kwargs)
Perform an inline map/reduce operation on this collection.
Perform the map/reduce operation on the server in RAM. A result
collection is not created. The result set is returned as a list
of documents.
If `full_response` is ``False`` (default) returns the
result documents in a list. Otherwise, returns the full
response from the server to the `map reduce command`_.
The :meth:`inline_map_reduce` method obeys the :attr:`read_preference`
of this :class:`Collection`.
:Parameters:
- `map`: map function (as a JavaScript string)
- `reduce`: reduce function (as a JavaScript string)
- `full_response` (optional): if ``True``, return full response to
this command - otherwise just return the result collection
- `callback`: (optional): function taking (result, error), executed
when operation completes
- `**kwargs` (optional): additional arguments to the
`map reduce command`_ may be passed as keyword arguments to this
helper method, e.g.::
yield db.test.inline_map_reduce(map, reduce, limit=2)
If a callback is passed, returns None, else returns a Future.
.. _map reduce command: http://docs.mongodb.org/manual/reference/command/mapReduce/
.. mongodoc:: mapreduce

View File

@ -0,0 +1,7 @@
:class:`~motor.motor_tornado.MotorCursor`
=========================================
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorCursor
:members:

View File

@ -1,12 +1,6 @@
:class:`~motor.motor_tornado.MotorDatabase`
===========================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
.. autoclass:: MotorDatabase

View File

@ -1,12 +1,6 @@
:mod:`motor.web` - Integrate Motor with the Tornado web framework
=================================================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.web
.. automodule:: motor.web

View File

@ -3,691 +3,9 @@ Changelog
.. currentmodule:: motor.motor_tornado
Motor 3.8.0
-----------
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
- Add support for Python 3.14.
- Drop support for Python 3.9.
Motor 3.7.1
-----------
The 3.7.1 release contains only documentation changes.
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
Motor 3.7.0
-----------
- Add support for PyMongo 4.10.
- Drop support for Python 3.8.
- Drop support for MongoDB 3.6.
Motor 3.6.1
-----------
- Add return type to to_list method in stub file.
- Fix ability to install pymongo from source while testing.
Motor 3.6.0
-----------
- Add support for MongoDB 8.0 and PyMongo 4.9.
- The length parameter in :meth:`MotorCursor.to_list` is now optional.
.. note::
This is the last planned minor version of Motor. We are sunsetting Motor in favor of native
asyncio support in PyMongo 4.9+. We will continue to provide security releases and bug fixes for
Motor, but it will not gain new features.
Motor 3.5.1
-----------
- Fix runtime behavior of Motor generic class typing, e.g. ``client: AsyncIOMotorClient[Dict[str, Any]]``.
Motor 3.5.0
-----------
- Drop support for Python 3.7.
- Switch to using Hatchling as a build backend and remove ``setup.py``.
- Add Secure Software Development Life Cycle automation to release process.
GitHub Releases for pymongocrypt now include a Software Bill of Materials, and signature
files corresponding to the distribution files released on PyPI.
Motor 3.4.0
-----------
- Type hint bug fixes and improvements. Added typings to classes in ``motor_tornado`` and
``motor_asyncio``.
Motor 3.3.2
-----------
- Fix incorrect type hints for the following:
:meth:`MotorCursor.to_list`,
:meth:`MotorCollection.name`,
:meth:`MotorDatabase.get_collection`,
:meth:`MotorClientSession.with_transaction`
- Fix a bug that caused application-supplied DriverInfo to be overwritten.
Motor 3.3.1
-----------
- Fix a bug in the type hint for :meth:`MotorCursor.to_list`.
Motor 3.3.0
-----------
- Add support for PyMongo 4.4+.
- Add support for Python 3.12.
- Add inline type hints for public APIs.
- Added new helper methods for Atlas Search Index (requires MongoDB Server 7.0+):
:meth:`~motor.motor_tornado.MotorCollection.list_search_indexes`,
:meth:`~motor.motor_tornado.MotorCollection.create_search_index`,
:meth:`~motor.motor_tornado.MotorCollection.create_search_indexes`,
:meth:`~motor.motor_tornado.MotorCollection.drop_search_index`,
:meth:`~motor.motor_tornado.MotorCollection.update_search_index`
- Added :meth:`~motor.motor_tornado.MotorDatabase.cursor_command`
and :meth:`~motor.motor_tornado.MotorCommandCursor.try_next` to support
executing an arbitrary command that returns a cursor.
Motor 3.2.0
-----------
- Add support for MongoDB 7.0 and PyMongo 4.4+.
- Add support for Queryable Encryption helpers :meth:`~motor.core.MotorClientEncryption.create_encrypted_collection` and
:meth:`~motor.core.MotorClientEncryption.encrypt_expression`.
Backwards-breaking changes may be made before the final release.
- pymongocrypt 1.6.0 or later is now required for Client Side Field Level
Encryption (CSFLE) and Queryable Encryption (QE) support. MongoDB Server 7.0
introduced a backwards breaking change to the QE protocol. Users taking
advantage of the QE must now upgrade to MongoDB 7.0+ and Motor 3.2+.
Motor 3.1.1
-----------
Motor 3.1.1 adds support for Python 3.11 and fixes a bug that caused an
``ImportError`` in Python 3.11.0.
Motor 3.1
---------
Motor 3.1 adds support for PyMongo 4.2+ and the Queryable Encryption beta
with MongoDB 6.0.
Note that backwards-breaking changes may be made before the final release.
New features:
- Support for MongoDB 6.0.
- Added the following key management APIs to :class:`~motor.core.MotorClientEncryption`:
:meth:`~motor.core.MotorClientEncryption.get_key`
:meth:`~motor.core.MotorClientEncryption.get_keys`
:meth:`~motor.core.MotorClientEncryption.delete_key`
:meth:`~motor.core.MotorClientEncryption.add_key_alt_name`
:meth:`~motor.core.MotorClientEncryption.get_key_by_alt_name`
:meth:`~motor.core.MotorClientEncryption.remove_key_alt_name`
:meth:`~motor.core.MotorClientEncryption.rewrap_many_data_key`
- Change streams support for user-facing PIT pre- and post-images using
the new ``full_document_before_change`` argument to
:meth:`~motor.core.MotorClient.watch` and :meth:`~motor.core.MotorCollection.watch`.
- Allow cursor to be used in async with-statement.
The new Queryable Encryption changes that are in beta are:
- The ``encrypted_fields`` argument to the
:class:`~motor.motor_tornado.MotorCollection` constructor, and the
:meth:`~motor.motor_tornado.MotorDatabase.create_collection`
and :meth:`~motor.motor_tornado.MotorDatabase.drop_collection` methods.
- The ``query_type`` and ``contention_factor`` arguments to
:meth:`motor.motor_asyncio.AsyncIOMotorClientEncryption.encrypt` and
:meth:`motor.motor_tornado.MotorClientEncryption.encrypt`.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 3.1 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _Motor 3.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=33421
Motor 3.0
---------
Motor 3.0 adds support for PyMongo 4.0+. It inherits a number
of improvements and breaking API changes from PyMongo 4.0+.
See :doc:`migrate-to-motor-3` for more information.
Breaking Changes
~~~~~~~~~~~~~~~~
- Requires PyMongo 4.0+.
- Removed support for Python 3.5 and 3.6. Python 3.7+ is now required.
- Removed the ``socketKeepAlive`` keyword argument to
:class:`~motor.motor_tornado.MotorClient`.
- Removed :meth:`motor.motor_tornado.MotorClient.fsync`,
:meth:`motor.motor_tornado.MotorClient.unlock`, and
:attr:`motor.motor_tornado.MotorClient.is_locked`.
- Removed :attr:`motor.motor_tornado.MotorClient.max_bson_size`.
- Removed :attr:`motor.motor_tornado.MotorClient.max_message_size`.
- Removed :attr:`motor.motor_tornado.MotorClient.max_write_batch_size`.
- Removed :attr:`motor.motor_tornado.MotorClient.event_listeners`.
- Removed :attr:`motor.motor_tornado.MotorClient.max_pool_size`.
- Removed :attr:`motor.motor_tornado.MotorClient.max_idle_time_ms`.
- Removed :attr:`motor.motor_tornado.MotorClient.local_threshold_ms`.
- Removed :attr:`motor.motor_tornado.MotorClient.server_selection_timeout`.
- Removed :attr:`motor.motor_tornado.MotorClient.retry_writes`.
- Removed :attr:`motor.motor_tornado.MotorClient.retry_reads`.
- Removed support for database profiler helpers
:meth:`~motor.motor_tornado.MotorDatabase.profiling_level`,
:meth:`~motor.motor_tornado.MotorDatabase.set_profiling_level`,
and :meth:`~motor.motor_tornado.MotorDatabase.profiling_info`. Instead, users
should run the profile command with the
:meth:`~motor.motor_tornado.MotorDatabase.command` helper directly.
- Removed :attr:`pymongo.OFF`, :attr:`pymongo.SLOW_ONLY`, and
:attr:`pymongo.ALL`.
- Removed :meth:`motor.motor_tornado.MotorCollection.map_reduce` and
:meth:`motor.motor_tornado.MotorCollection.inline_map_reduce`.
- Removed the ``useCursor`` option for
:meth:`~motor.motor_tornado.MotorCollection.aggregate`.
- Removed :mod:`pymongo.son_manipulator`,
:meth:`motor.motor_tornado.MotorDatabase.add_son_manipulator`,
:attr:`motor.motor_tornado.MotorDatabase.outgoing_copying_manipulators`,
:attr:`motor.motor_tornado.MotorDatabase.outgoing_manipulators`,
:attr:`motor.motor_tornado.MotorDatabase.incoming_copying_manipulators`, and
:attr:`motor.motor_tornado.MotorDatabase.incoming_manipulators`.
- Removed the ``manipulate`` and ``modifiers`` parameters from
:meth:`~motor.motor_tornado.MotorCollection.find`,
:meth:`~motor.motor_tornado.MotorCollection.find_one`,
:meth:`~motor.motor_tornado.MotorCollection.find_raw_batches`, and
:meth:`~motor.motor_tornado.MotorCursor`.
- ``directConnection`` URI option and keyword argument to :class:`~motor.motor_tornado.MotorClient`
defaults to ``False`` instead of ``None``, allowing for the automatic
discovery of replica sets. This means that if you
want a direct connection to a single server you must pass
``directConnection=True`` as a URI option or keyword argument.
- The ``hint`` option is now required when using ``min`` or ``max`` queries
with :meth:`~motor.motor_tornado.MotorCollection.find`.
- When providing a "mongodb+srv://" URI to
:class:`~motor.motor_tornado.MotorClient` constructor you can now use the
``srvServiceName`` URI option to specify your own SRV service name.
- :class:`~motor.motor_tornado.MotorCollection` and :class:`motor.motor_tornado.MotorDatabase`
now raises an error upon evaluating as a Boolean, please use the
syntax ``if collection is not None:`` or ``if database is not None:`` as
opposed to
the previous syntax which was simply ``if collection:`` or ``if database:``.
You must now explicitly compare with None.
- :class:`~motor.motor_tornado.MotorClient` cannot execute any operations
after being closed. The previous behavior would simply reconnect. However,
now you must create a new instance.
- Empty projections (eg {} or []) for
:meth:`~motor.motor_tornado.MotorCollection.find`, and
:meth:`~motor.motor_tornado.MotorCollection.find_one`
are passed to the server as-is rather than the previous behavior which
substituted in a projection of ``{"_id": 1}``. This means that an empty
projection will now return the entire document, not just the ``"_id"`` field.
- :class:`~motor.motor_tornado.MotorClient` now raises a
:exc:`~pymongo.errors.ConfigurationError` when more than one URI is passed
into the ``hosts`` argument.
- :class:`~motor.motor_tornado.MotorClient`` now raises an
:exc:`~pymongo.errors.InvalidURI` exception
when it encounters unescaped percent signs in username and password when
parsing MongoDB URIs.
- Comparing two :class:`~motor.motor_tornado.MotorClient` instances now
uses a set of immutable properties rather than
:attr:`~motor.motor_tornado.MotorClient.address` which can change.
- Removed the ``disable_md5`` parameter for :class:`~pymongo.GridFSBucket` and
:class:`~pymongo.GridFS`. See :ref:`removed-gridfs-checksum` for details.
- PyMongoCrypt 1.2.0 or later is now required for client side field level
encryption support.
Notable improvements
~~~~~~~~~~~~~~~~~~~~
- Enhanced connection pooling to create connections more efficiently and
avoid connection storms.
- Added the ``maxConnecting`` URI and
:class:`~motor.motor_tornado.MotorClient` keyword argument.
- :class:`~motor.motor_tornado.MotorClient` now accepts a URI and keyword
argument ``srvMaxHosts`` that limits the number of mongos-like hosts a client
will connect to. More specifically, when a mongodb+srv:// connection string
resolves to more than ``srvMaxHosts`` number of hosts, the client will randomly
choose a ``srvMaxHosts`` sized subset of hosts.
- Added :attr:`motor.motor_tornado.MotorClient.options` for read-only access
to a client's configuration options.
- Added support for the ``comment`` parameter to all helpers. For example see
:meth:`~motor.motor_tornado.MotorCollection.insert_one`.
- Added support for the ``let`` parameter to
:meth:`~motor.motor_tornado.MotorCollection.update_one`,
:meth:`~motor.motor_tornado.MotorCollection.update_many`,
:meth:`~motor.motor_tornado.MotorCollection.delete_one`,
:meth:`~motor.motor_tornado.MotorCollection.delete_many`,
:meth:`~motor.motor_tornado.MotorCollection.replace_one`,
:meth:`~motor.motor_tornado.MotorCollection.aggregate`,
:meth:`~motor.motor_tornado.MotorCollection.find_one_and_delete`,
:meth:`~motor.motor_tornado.MotorCollection.find_one_and_replace`,
:meth:`~motor.motor_tornado.MotorCollection.find_one_and_update`,
:meth:`~motor.motor_tornado.MotorCollection.find`,
:meth:`~motor.motor_tornado.MotorCollection.find_one`,
and :meth:`~motor.motor_tornado.MotorCollection.bulk_write`.
``let`` is a map of parameter names and values.
Parameters can then be accessed as variables in an aggregate expression
context.
- :meth:`~motor.motor_tornado.MotorCollection.aggregate` now supports
$merge and $out executing on secondaries on MongoDB >=5.0.
aggregate() now always obeys the collection's :attr:`read_preference` on
MongoDB >= 5.0.
- :meth:`gridfs.grid_file.GridOut.seek` now returns the new position in the file, to
conform to the behavior of :meth:`io.IOBase.seek`.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 3.0 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _Motor 3.0 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=29710
Motor 2.5.1
-----------
Motor 2.5.1 fixes a bug where :meth:`MotorCursor.to_list` could return more
than ``length`` documents.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 2.5.1 release notes in JIRA`_ for the complete list of resolved
issues in this release.
.. _Motor 2.5.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=31791
Motor 2.5
---------
Motor 2.5 adds support for MongoDB 5.0. It depends on PyMongo 3.12 or
later.
New features:
- Added support for MongoDB 5.0.
- Support for MongoDB Stable API, see :class:`~pymongo.server_api.ServerApi`.
- Support for snapshot reads on secondaries via the new ``snapshot`` option to
:meth:`~motor.motor_asyncio.AsyncIOMotorClient.start_session`.
- Support for Azure and GCP KMS providers for client side field level
encryption. See the examples in :doc:`examples/encryption`.
- Support AWS authentication with temporary credentials when connecting to KMS
in client side field level encryption.
- Support for connecting to load balanced MongoDB clusters via the new
``loadBalanced`` URI option.
- Support for creating timeseries collections via the ``timeseries`` and
``expireAfterSeconds`` arguments to
:meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.create_collection`.
- Added :attr:`motor.motor_asyncio.AsyncIOMotorClient.topology_description`.
- Added hash support to :class:`motor.motor_asyncio.AsyncIOMotorClient`,
:class:`motor.motor_asyncio.AsyncIOMotorDatabase`, and
:class:`motor.motor_asyncio.AsyncIOMotorCollection` classes.
- Added session and read concern support to
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_raw_batches`
and :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.aggregate_raw_batches`.
Deprecations:
- Deprecated support for Python 3.5.
- Deprecated :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.profiling_info`,
:meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.profiling_level`, and
:meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.set_profiling_level`.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 2.5 release notes in JIRA`_ for the complete list of resolved
issues in this release.
.. _Motor 2.5 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=30895
Motor 2.4
---------
Motor 2.4 adds support for client-side field-level encryption
and Python 3.9.
New Features:
- Added the :class:`motor.motor_asyncio.AsyncIOMotorClientEncryption` class,
with the same interface as the corresponding PyMongo class.
See :doc:`examples/encryption` for examples.
- Added support for Python 3.9
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 2.4 release notes in JIRA`_ for the complete list of resolved
issues in this release.
.. _Motor 2.4 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=29749
Motor 2.3.1
-----------
Motor 2.3.1 fixes two bugs related to change streams.
Bug-fixes:
- The :meth:`motor.motor_asyncio.AsyncIOMotorCollection.watch`,
:meth:`motor.motor_asyncio.AsyncIOMotorDatabase.watch`, and
:meth:`motor.motor_asyncio.AsyncIOMotorClient.watch` methods now properly
support passing :class:`~motor.motor_asyncio.AsyncIOMotorClientSession` via
the ``session`` argument.
- Avoid exhausting Motor's worker thread pool when many change streams are
being iterated simultaneously.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 2.3.1 release notes in JIRA`_ for the complete list of resolved
issues in this release.
.. _Motor 2.3.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=30136
Motor 2.3
---------
Motor 2.3 adds support for contextvars.
New features:
- Added supported for the contextvars module. Specifically, it is now possible
to access context variables inside
:class:`~pymongo.monitoring.CommandListener` callbacks.
Bug-fixes:
- Fixed a bug that prohibited users from subclassing the
:class:`motor.motor_asyncio.AsyncIOMotorClient`,
:class:`motor.motor_asyncio.AsyncIOMotorDatabase`, and
:class:`motor.motor_asyncio.AsyncIOMotorCollection` classes.
- Updated the documentation to indicate full support for Windows.
Previously, the documentation stated that Windows support was
experimental.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 2.3 release notes in JIRA`_ for the complete list of resolved
issues in this release.
.. _Motor 2.3 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=29836
Motor 2.2
---------
Motor 2.2 adds support for MongoDB 4.4 features. It depends on PyMongo 3.11 or
later. Motor continues to support MongoDB 3.0 and later. Motor 2.2 also drops
support for Python 2.7 and Python 3.4.
New features:
- Added the ``AsyncIOMotorCursor`` method
:meth:`~motor.motor_asyncio.AsyncIOMotorCursor.next` that advances the
cursor one document at a time, similar to to the
``AsyncIOMotorChangeStream`` method
:meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream.next`.
- Added index-hinting support to the
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.replace_one`,
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.update_one`,
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.update_many`,
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.delete_one`,
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.delete_many`,
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_one_and_replace`,
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_one_and_update`, and
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_one_and_delete`
methods.
- Added support for the ``allow_disk_use`` parameter to
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find`.
- Modified the :meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream` class'
async context manager such that the change stream cursor is now created
during the call to ``async with``. Previously, the cursor was only created
when the application iterated the
:meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream` object which could
result in the application missing some changes.
- Motor now advertises the framework used by the application to
the MongoDB server as ``asyncio`` or ``Tornado``. Previously, no framework
information was reported if the application used ``asyncio``.
Bug-fixes:
- Fixed a bug that caused calls to the
:meth:`~motor.motor_asyncio.AsyncIOMotorGridOut.open()` method to raise
:exc:`AttributeError`.
- Fixed a bug that sometimes caused :meth:`~asyncio.Future.set_result` to be
called on a cancelled :meth:`~asyncio.Future` when iterating a
:meth:`~motor.motor_asyncio.AsyncIOMotorCommandCursor`.
Deprecations:
- Deprecated ``AsyncIOMotorCursor`` method
:meth:`~motor.motor_asyncio.AsyncIOMotorCursor.next_object` and
property :attr:`~motor.motor_asyncio.AsyncIOMotorCursor.fetch_next`.
Applications should use ``async for`` to iterate over cursors instead.
- Deprecated the :meth:`~motor.motor_asyncio.AsyncIOMotorClient.fsync`
method. Applications should run the
`fsync command <https://mongodb.com/docs/manual/reference/command/fsync/>`_
directly with :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command`
instead.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 2.2 release notes in JIRA`_ for the complete list of resolved
issues in this release.
.. _Motor 2.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=24884
Motor 2.1
---------
Motor 2.1 adds support for MongoDB 4.2 features. It depends on PyMongo 3.10 or
later. Motor continues to support MongoDB 3.0 and later. Motor 2.1 also adds
support for Python 3.8.
Motor now offers experimental support for Windows when it is using the asyncio
event loop. This means it supports Windows exclusively with Python 3, either
integrating with asyncio directly or with Tornado 5 or later: starting in
version 5, Tornado uses the asyncio event loop on Python 3 by default.
Additional changes:
- Support for MongoDB 4.2 sharded transactions. Sharded transactions have
the same API as replica set transactions.
- New method :meth:`~motor.motor_asyncio.AsyncIOMotorClientSession.with_transaction`
to support conveniently running a transaction in a session with automatic
retries and at-most-once semantics.
- Added the ``max_commit_time_ms`` parameter to
:meth:`~motor.motor_asyncio.AsyncIOMotorClientSession.start_transaction`.
- The ``retryWrites`` URI option now defaults to ``True``. Supported write
operations that fail with a retryable error will automatically be retried one
time, with at-most-once semantics.
- Support for retryable reads and the ``retryReads`` URI option which is
enabled by default. See the :class:`~pymongo.mongo_client.MongoClient`
documentation for details. Now that supported operations are retried
automatically and transparently, users should consider adjusting any custom
retry logic to prevent an application from inadvertently retrying for too
long.
- Support zstandard for wire protocol compression.
- Support for periodically polling DNS SRV records to update the mongos proxy
list without having to change client configuration.
- New method :meth:`motor.motor_asyncio.AsyncIOMotorDatabase.aggregate` to
support running database level aggregations.
- Change stream enhancements for MongoDB 4.2:
- Resume tokens can now be accessed from a ``AsyncIOMotorChangeStream`` cursor
using the :attr:`~motor.motor_asyncio.AsyncIOMotorChangeStream.resume_token`
attribute.
- New ``AsyncIOMotorChangeStream`` method
:meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream.try_next` and
attribute :attr:`~motor.motor_asyncio.AsyncIOMotorChangeStream.alive`.
- New parameter ``start_after`` for change stream
:meth:`motor.motor_asyncio.AsyncIOMotorCollection.watch`,
:meth:`motor.motor_asyncio.AsyncIOMotorDatabase.watch`, and
:meth:`motor.motor_asyncio.AsyncIOMotorClient.watch` methods.
- New parameters ``bucket_name``, ``chunk_size_bytes``, ``write_concern``, and
``read_preference`` for :class:`motor.motor_asyncio.AsyncIOMotorGridFSBucket`.
Issues Resolved
~~~~~~~~~~~~~~~
See the `Motor 2.1 release notes in JIRA`_ for the complete list of resolved
issues in this release.
.. _Motor 2.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=20187
Motor 2.0
---------
Motor 2.0 drops support for MongoDB 2.6 and adds supports MongoDB 4.0 features,
including multi-document transactions, and change stream notifications on entire
databases or entire MongoDB servers. It adds support for Python 3.7. This
version of Motor requires PyMongo 3.7 or later.
This is a major release that removes previously deprecated APIs.
To support multi-document transactions, Motor had to make breaking changes to
the session API and release a major version bump. Since this is a major release
it also deletes many helper methods and APIs that had been deprecated over the
time since Motor 1.0, most notably the old CRUD methods ``insert``, ``update``,
``remove``, and ``save``, and the original callback-based API. Read the
:doc:`migrate-to-motor-2` carefully to upgrade your existing Motor application.
Documentation is updated to warn about obsolete TLS versions, see
:doc:`configuration`. Motor is now tested on Travis in addition to MongoDB's
`Evergreen <https://github.com/evergreen-ci/evergreen>`_ system.
Added support for `aiohttp`_ 3.0 and later, and dropped older aiohttp versions.
The aiohttp integration now requires Python 3.5+.
The ``MotorDatabase.add_user`` and ``MotorDatabase.remove_user`` methods are
deleted. Manage user accounts with four database commands: createUser_,
usersInfo_, updateUser_, and dropUser_. You can run any database command with
the :meth:`MotorDatabase.command` method.
.. _createUser: https://mongodb.com/docs/manual/reference/command/createUser/
.. _usersInfo: https://mongodb.com/docs/manual/reference/command/usersInfo/
.. _updateUser: https://mongodb.com/docs/manual/reference/command/updateUser/
.. _dropUser: https://mongodb.com/docs/manual/reference/command/createUser/
The deprecated GridFS classes ``MotorGridFS`` and ``AsyncIOMotorGridFS`` are
deleted in favor of :class:`~motor.motor_tornado.MotorGridFSBucket` and
:class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket`, which conform to driver
specs for GridFS.
Additional changes:
- New methods for retrieving batches of raw BSON:
- :meth:`MotorCollection.find_raw_batches`
- :meth:`MotorCollection.aggregate_raw_batches`
- Motor adds its name, version, and Tornado's version (if appropriate) to the
client data logged by the MongoDB server when Motor connects, in addition to
the data added by PyMongo.
- Calling :meth:`~MotorCommandCursor.batch_size` on a cursor returned from
:meth:`~MotorCollection.aggregate` no longer raises ``AttributeError``.
Motor 1.3.1
-----------
Fix a Python 3.7 compatibility bug caused by importing "async", which is a
keyword in Python 3.7. Drop support for Python 3.4.3 and older.
Motor 1.3.0
-----------
Deprecate Motor's old callback-based async API in preparation for removing it in
Motor 2.0. Raise ``DeprecationWarning`` whenever a callback is passed.
See the :doc:`migrate-to-motor-2`.
Motor 1.2.5
-----------
Fix a Python 3.7 compatibility bug caused by importing "async", which is a
keyword in Python 3.7. Drop support for Python 3.4.3 and older.
Motor 1.2.4
-----------
Fix a Python 3.7 compatibility bug in the :class:`MotorChangeStream` class
returned by :meth:`MotorCollection.watch`. It is now possible to use change
streams in ``async for`` loops in Python 3.7.
Motor 1.2.3
-----------
Compatibility with latest Sphinx and document how to use the latest TLS
protocols.
Motor 1.2.2
-----------
Motor 1.2.0 requires PyMongo 3.6 or later. The dependency was properly
documented, but not enforced in ``setup.py``. PyMongo 3.6 is now an install-time
requirement; thanks to Shane Harvey for the fix.
Motor 1.2.1
-----------
An asyncio application that created a Change Stream with
:meth:`MotorCollection.watch` and shut down while the Change Stream was open
would print several errors. I have rewritten :meth:`MotorChangeStream.next`
and some Motor internals to allow clean shutdown with asyncio.
Motor 1.2
---------
Motor 1.2 drops support for MongoDB 2.4 and adds support for MongoDB 3.6
features. It depends on PyMongo 3.6 or later. Motor continues to support MongoDB
2.6 and later.
Dropped support for Python 2.6 and 3.3. Motor continues to support Python 2.7,
and 3.4+.
Dropped support for Tornado 3. A recent version of Tornado 4 is required.
Dropped support for the `Python 3.5.0 and Python 3.5.1 "async for" protocol
<https://python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions>`_.
Motor allows "async for" with cursors in Python 3.5.2 and later.
See the :ref:`Compatibility Matrix <compatibility-matrix>` for the relationships
among Motor, Python, Tornado, and MongoDB versions.
Added support for `aiohttp`_ 2.0 and later, and dropped older aiohttp versions.
Highlights include:
- New method :meth:`MotorCollection.watch` to acquire a Change Stream on a
collection.
- New Session API to support causal consistency, see
:meth:`MotorClient.start_session`.
- Support for array_filters in
:meth:`~MotorCollection.update_one`,
:meth:`~MotorCollection.update_many`,
:meth:`~MotorCollection.find_one_and_update`,
:meth:`~MotorCollection.bulk_write`.
- :meth:`MotorClient.list_databases` and :meth:`MotorClient.list_database_names`.
- Support for mongodb+srv:// URIs. See
:class:`~pymongo.mongo_client.MongoClient` for details.
- Support for retryable writes and the ``retryWrites`` URI option. See
:class:`~pymongo.mongo_client.MongoClient` for details.
The maximum number of workers in the thread pool can be overridden with an
environment variable, see :doc:`configuration`.
@ -696,8 +14,6 @@ and read_concern arguments. This is rarely needed; you typically create a
:class:`MotorCollection` from a :class:`MotorDatabase`, not by calling its
constructor directly.
Deleted obsolete class ``motor.Op``.
Motor 1.1
---------
@ -709,9 +25,9 @@ Highlights include:
- Complete support for MongoDB 3.4:
- Unicode aware string comparison using collations.
- Unicode aware string comparison using collations. See :ref:`PyMongo's examples for collation <collation-on-operation>`.
- :class:`MotorCursor` and :class:`MotorGridOutCursor` have a new attribute :meth:`~MotorCursor.collation`.
- Support for the new :class:`~pymongo.decimal128.Decimal128` BSON type.
- Support for the new :class:`~bson.decimal128.Decimal128` BSON type.
- A new maxStalenessSeconds read preference option.
- A username is no longer required for the MONGODB-X509 authentication
mechanism when connected to MongoDB >= 3.4.
@ -740,9 +56,9 @@ Highlights include:
verification.
- TLS compression is now explicitly disabled when possible.
- The Server Name Indication (SNI) TLS extension is used when possible.
- PyMongo's ``bson`` module provides finer control over JSON encoding/decoding
with :class:`~pymongo.json_util.JSONOptions`.
- Allow :class:`~pymongo.code.Code` objects to have a scope of ``None``,
- PyMongo's `bson` module provides finer control over JSON encoding/decoding
with :class:`~bson.json_util.JSONOptions`.
- Allow :class:`~bson.code.Code` objects to have a scope of ``None``,
signifying no scope. Also allow encoding Code objects with an empty scope
(i.e. ``{}``).
@ -763,12 +79,13 @@ Motor 1.0
---------
Motor now depends on PyMongo 3.3 and later. The move from PyMongo 2 to 3 brings
a large number of API changes, read the `the PyMongo 3 changelog`_ carefully.
a large number of API changes, read :doc:`migrate-to-motor-1` and
`the PyMongo 3 changelog`_ carefully.
.. _the PyMongo 3 changelog: https://pymongo.readthedocs.io/en/4.0/changelog.html#changes-in-version-3-0
.. _the PyMongo 3 changelog: http://api.mongodb.com/python/current/changelog.html#changes-in-version-3-0
:class:`MotorReplicaSetClient` is removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In Motor 1.0, :class:`MotorClient` is the only class. Connect to a replica set with
a "replicaSet" URI option or parameter::
@ -782,7 +99,8 @@ New features
New classes :class:`~motor.motor_tornado.MotorGridFSBucket` and :class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket`
conform to the `GridFS API Spec <https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.rst>`_
for MongoDB drivers. These classes supersede the old
``MotorGridFS`` and ``AsyncIOMotorGridFS``. See `GridFS`_ changes below,
:class:`~motor.motor_tornado.MotorGridFS` and
:class:`~motor.motor_asyncio.AsyncIOMotorGridFS`. See `GridFS`_ changes below,
especially note the **breaking change** in
:class:`~motor.motor_web.GridFSHandler`.
@ -812,8 +130,8 @@ Unix domain socket paths must be quoted with :func:`urllib.parse.quote_plus` (or
.. code-block:: python
path = "/tmp/mongodb-27017.sock"
MotorClient("mongodb://%s" % urllib.parse.quote_plus(path))
path = '/tmp/mongodb-27017.sock'
MotorClient('mongodb://%s' % urllib.parse.quote_plus(path))
:class:`~motor.motor_tornado.MotorCollection` changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -862,9 +180,9 @@ The following find/find_one options have been removed:
- await_data (use the new ``cursor_type`` option instead)
- exhaust (use the new ``cursor_type`` option instead)
- as_class (use :meth:`~motor.motor_tornado.MotorCollection.with_options` with
:class:`~pymongo.codec_options.CodecOptions` instead)
:class:`~bson.codec_options.CodecOptions` instead)
- compile_re (BSON regular expressions are always decoded to
:class:`~pymongo.regex.Regex`)
:class:`~bson.regex.Regex`)
The following find/find_one options are deprecated:
@ -894,10 +212,9 @@ Removed:
GridFS
~~~~~~
The old GridFS classes ``MotorGridFS`` and
``AsyncIOMotorGridFS`` are deprecated in favor of
:class:`~motor.motor_tornado.MotorGridFSBucket` and
:class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket`,
The old GridFS classes :class:`~motor.motor_tornado.MotorGridFS` and
:class:`~motor.motor_asyncio.AsyncIOMotorGridFS` are deprecated in favor of
:class:`~motor.motor_tornado.MotorGridFSBucket` and :class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket`,
which comply with MongoDB's cross-language driver spec for GridFS.
The old classes are still supported, but will be removed in Motor 2.0.
@ -932,8 +249,7 @@ In a Python 3.5 native coroutine, the "async with" statement calls
async def upload():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
async with await fs.open_upload_stream(
"test_file", metadata={"contentType": "text/plain"}) as gridin:
async with await fs.new_file() as gridin:
await gridin.write(b'First part\n')
await gridin.write(b'Second part')
@ -974,7 +290,8 @@ This version updates the PyMongo dependency from 2.8.0 to 2.9.x, and wraps
PyMongo 2.9's new APIs.
Most of Motor 1.0's API is now implemented, and APIs that will be removed in
Motor 1.0 are now deprecated and raise warnings.
Motor 1.0 are now deprecated and raise warnings. See the
:doc:`/migrate-to-motor-1` to prepare your code for Motor 1.0.
:class:`MotorClient` changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -1089,20 +406,20 @@ Motor 0.6
This is a bugfix release. Fixing these bugs has introduced tiny API changes that
may affect some programs.
``motor_asyncio`` and ``motor_tornado`` submodules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`motor_asyncio` and `motor_tornado` submodules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These modules have been moved from:
- ``motor_asyncio.py``
- ``motor_tornado.py``
- `motor_asyncio.py`
- `motor_tornado.py`
To:
- ``motor_asyncio/__init__.py``
- ``motor_tornado/__init__.py``
- `motor_asyncio/__init__.py`
- `motor_tornado/__init__.py`
Motor had to make this change in order to omit the ``motor_asyncio`` submodule
Motor had to make this change in order to omit the `motor_asyncio` submodule
entirely and avoid a spurious :exc:`SyntaxError` being printed when installing in
Python 2. The change should be invisible to application code.
@ -1128,7 +445,7 @@ accessible, Motor collections now allow dict-style access, the same as Motor
clients and databases always have::
# New in Motor 0.6
subcollection = collection['_subcollection']
subcollection = collection['_subcollection']
These changes solve problems with iPython code completion and the Python 3
:class:`ABC` abstract base class.
@ -1156,18 +473,18 @@ explanation.)
.. _commit message dc19418c: https://github.com/mongodb/motor/commit/dc19418c
``async`` and ``await``
~~~~~~~~~~~~~~~~~~~~~~~
`async` and `await`
~~~~~~~~~~~~~~~~~~~
Motor now supports Python 3.5 native coroutines, written with the ``async`` and
``await`` syntax::
Motor now supports Python 3.5 native coroutines, written with the `async` and
`await` syntax::
async def f():
await collection.insert({'_id': 1})
Cursors from :meth:`~MotorCollection.find`, :meth:`~MotorCollection.aggregate`, or
:meth:`~MotorGridFS.find` can be iterated elegantly and very efficiently in native
coroutines with ``async for``::
coroutines with `async for`::
async def f():
async for doc in collection.find():
@ -1179,7 +496,7 @@ coroutines with ``async for``::
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:meth:`MotorCollection.aggregate` now returns a cursor by default, and the cursor
is returned immediately without a ``yield``. The old syntax is no longer
is returned immediately without a `yield`. The old syntax is no longer
supported::
# Motor 0.4 and older, no longer supported.
@ -1217,7 +534,7 @@ Deprecations
Motor 0.5 deprecates a large number of APIs that will be removed in version 1.0:
``MotorClient``:
`MotorClient`:
- `~MotorClient.host`
- `~MotorClient.port`
- `~MotorClient.document_class`
@ -1228,7 +545,7 @@ Motor 0.5 deprecates a large number of APIs that will be removed in version 1.0:
- `~MotorClient.disconnect`
- `~MotorClient.alive`
``MotorReplicaSetClient``:
`MotorReplicaSetClient`:
- `~MotorReplicaSetClient.document_class`
- `~MotorReplicaSetClient.tz_aware`
- `~MotorReplicaSetClient.secondary_acceptable_latency_ms`
@ -1236,12 +553,12 @@ Motor 0.5 deprecates a large number of APIs that will be removed in version 1.0:
- `~MotorReplicaSetClient.uuid_subtype`
- `~MotorReplicaSetClient.alive`
``MotorDatabase``:
`MotorDatabase`:
- `~MotorDatabase.secondary_acceptable_latency_ms`
- `~MotorDatabase.tag_sets`
- `~MotorDatabase.uuid_subtype`
``MotorCollection``:
`MotorCollection`:
- `~MotorCollection.secondary_acceptable_latency_ms`
- `~MotorCollection.tag_sets`
- `~MotorCollection.uuid_subtype`
@ -1265,13 +582,13 @@ Then instead, write::
The negative limit ensures the server closes the cursor after one result,
saving Motor the work of closing it. See `cursor.limit
<https://mongodb.com/docs/v3.0/reference/method/cursor.limit/>`_.
<http://docs.mongodb.org/v3.0/reference/method/cursor.limit/>`_.
SSL hostname validation error
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When you use Motor with Tornado and SSL hostname validation fails, Motor used
to raise a :exc:`~pymongo.errors.ConnectionFailure` with a useful message like "hostname 'X'
to raise a :exc:`~pymongo.errors.ConnectionFailure` with a useful messsage like "hostname 'X'
doesn't match 'Y'". The message is now empty and Tornado logs a warning
instead.
@ -1301,7 +618,8 @@ therefore inheriting
`PyMongo 2.7.2's bug fixes <https://jira.mongodb.org/browse/PYTHON/fixforversion/14005>`_
and
`PyMongo 2.8's bug fixes <https://jira.mongodb.org/browse/PYTHON/fixforversion/14223>`_
and features.
and `features
<http://api.mongodb.org/python/current/changelog.html#changes-in-version-2-8>`_.
Fixes `a connection-pool timeout when waitQueueMultipleMS is set
<https://jira.mongodb.org/browse/MOTOR-62>`_ and `two bugs in replica set
@ -1312,9 +630,13 @@ used it, see `MOTOR-56 <https://jira.mongodb.org/browse/MOTOR-56>`_.
You can still use the :meth:`MotorDatabase.command` method directly.
The only scenario not supported is copying a database from one host to
another, if the remote host requires authentication.
For this, use PyMongo's ``copy_database`` method, or, since PyMongo's
For this, use PyMongo's `copy_database`_ method, or, since PyMongo's
``copy_database`` will be removed in a future release too, use the mongo shell.
.. _copy_database: http://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient.copy_database
.. seealso:: `The "copydb" command <http://docs.mongodb.org/manual/reference/command/copydb/>`_.
Motor 0.3.3
-----------
@ -1400,7 +722,7 @@ If it's important to test that MongoDB is available before continuing
your application's startup, use ``IOLoop.run_sync``::
loop = tornado.ioloop.IOLoop.current()
client = motor.motor_tornado.MotorClient(host, port)
client = motor.MotorClient(host, port)
try:
loop.run_sync(client.open)
except pymongo.errors.ConnectionFailure:
@ -1586,7 +908,7 @@ returns a Future instead of accepting a callback.
.. _Greenlet: http://pypi.python.org/pypi/greenlet/
.. _asynchronous resolver interface: http://www.tornadoweb.org/en/stable/netutil.html#tornado.netutil.Resolver
.. _pycares: https://pypi.python.org/pypi/pycares
.. _fsyncLock: https://mongodb.com/docs/manual/reference/method/db.fsyncLock/
.. _fsyncLock: http://docs.mongodb.org/manual/reference/method/db.fsyncLock/
New Features
~~~~~~~~~~~~
@ -1605,7 +927,7 @@ PyMongo 2.6 and 2.7:
with :meth:`~MotorCursor.max_time_ms`.
* A new :meth:`MotorGridFS.find` method for querying GridFS.
.. _authentication examples: https://pymongo.readthedocs.io/en/stable/examples/authentication.html
.. _authentication examples: http://api.mongodb.org/python/current/examples/authentication.html
Bugfixes
~~~~~~~~

View File

@ -1,44 +1,37 @@
# -*- coding: utf-8 -*-
#
# Motor documentation build configuration file
#
# This file is execfile()d with the current directory set to its containing dir.
import os
import sys
from importlib.metadata import metadata
sys.path[0:0] = [os.path.abspath("..")]
import sys, os
sys.path[0:0] = [os.path.abspath('..')]
import motor # noqa: E402
from pymongo import version as pymongo_version
import motor
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.coverage",
"sphinx.ext.todo",
"doc.mongo_extensions",
"doc.motor_extensions",
"sphinx.ext.intersphinx",
"doc.coroutine_annotation",
]
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage',
'sphinx.ext.todo', 'doc.mongo_extensions', 'doc.motor_extensions',
'sphinx.ext.intersphinx', 'doc.coroutine_annotation']
primary_domain = "py"
primary_domain = 'py'
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = ".rst"
source_suffix = '.rst'
# The root toctree document.
root_doc = "index"
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = "Motor"
copyright = "2016-present MongoDB, Inc."
project = u'Motor'
copyright = u'2016 MongoDB, Inc.'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@ -54,42 +47,33 @@ unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ["_build"]
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None
#default_role = None
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
#show_authors = False
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# Links to release notes in jira give 401 error: unauthorized. MOTOR-1476
linkcheck_ignore = [
r"http://localhost:\d+",
r"https://jira\.mongodb\.org/secure/ReleaseNote\.jspa.*",
]
# Allow for flaky links.
linkcheck_retries = 3
#modindex_common_prefix = []
# -- Options for extensions ----------------------------------------------------
autoclass_content = "init"
autoclass_content = 'init'
doctest_path = [os.path.abspath("..")]
doctest_path = [os.path.abspath('..')]
# Don't test examples pulled from PyMongo's docstrings just because they start
# with '>>>'
doctest_test_doctest_blocks = ""
doctest_test_doctest_blocks = ''
doctest_global_setup = """
import pprint
@ -101,24 +85,7 @@ from tornado.ioloop import IOLoop
import pymongo
from pymongo.mongo_client import MongoClient
sync_client = MongoClient()
hello = sync_client.admin.command('hello')
server_info = sync_client.server_info()
if 'setName' in hello:
raise Exception(
"Run doctests with standalone MongoDB 5.0 server, not a replica set")
if hello.get('msg') == 'isdbgrid':
raise Exception(
"Run doctests with standalone MongoDB 5.0 server, not mongos")
if server_info['versionArray'][:2] != [5, 0]:
raise Exception(
"Run doctests with standalone MongoDB 5.0 server, not %s" % (
server_info['version'], ))
sync_client.drop_database("doctest_test")
db = sync_client.doctest_test
@ -128,108 +95,102 @@ from motor import MotorClient
# -- Options for HTML output ---------------------------------------------------
html_copy_source = False
# Theme gratefully vendored from CPython source.
html_theme = "pydoctheme"
html_theme_path = ["."]
html_theme_options = {'collapsiblesidebar': True}
html_static_path = ['static']
try:
import furo # noqa: F401
html_theme = "furo"
except ImportError:
# Theme gratefully vendored from CPython source.
html_theme = "pydoctheme"
html_theme_path = ["."]
html_theme_options = {"collapsiblesidebar": True}
html_static_path = ["static"]
html_sidebars = {
"index": ["globaltoc.html", "searchbox.html"],
}
html_sidebars = {
'index': ['globaltoc.html', 'searchbox.html'],
}
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
#html_static_path = ['_static']
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
#html_additional_pages = {}
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = ''
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = "Motor" + release.replace(".", "_")
htmlhelp_basename = 'Motor' + release.replace('.', '_')
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
# latex_paper_size = 'letter'
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
# latex_font_size = '10pt'
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
("index", "Motor.tex", "Motor Documentation", "A. Jesse Jiryu Davis", "manual"),
('index', 'Motor.tex', u'Motor Documentation',
u'A. Jesse Jiryu Davis', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
# latex_preamble = ''
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
# latex_appendices = []
#latex_appendices = []
# If false, no module index is generated.
# latex_use_modindex = True
#latex_use_modindex = True
autodoc_default_options = {
"inherited-members": True,
"member-order": "groupwise",
}
autodoc_default_flags = ['inherited-members']
autodoc_member_order = 'groupwise'
pymongo_version = metadata("pymongo")["version"]
pymongo_inventory = ("https://pymongo.readthedocs.io/en/%s/" % pymongo_version, None)
pymongo_inventory = ('http://api.mongodb.com/python/%s/' % pymongo_version,
None)
intersphinx_mapping = {
"pymongo": pymongo_inventory,
"aiohttp": ("http://aiohttp.readthedocs.io/en/stable/", None),
"tornado": ("http://www.tornadoweb.org/en/stable/", None),
"python": ("https://docs.python.org/3/", None),
'bson': pymongo_inventory,
'gridfs': pymongo_inventory,
'pymongo': pymongo_inventory,
'aiohttp': ('http://aiohttp.readthedocs.io/en/stable/', None),
'tornado': ('http://www.tornadoweb.org/en/stable/', None),
'python': ('https://docs.python.org/3/', None),
}

View File

@ -1,69 +1,9 @@
Configuration
=============
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
TLS Protocol Version
''''''''''''''''''''
Industry best practices, and some regulations, require the use
of TLS 1.1 or newer. Though no application changes are required for
Motor to make use of the newest protocols, some operating systems or
versions may not provide an OpenSSL version new enough to support them.
Users of macOS older than 10.13 (High Sierra) will need to install Python
from `python.org`_, `homebrew`_, `macports`_, or another similar source.
Users of Linux or other non-macOS Unix can check their OpenSSL version like
this::
$ openssl version
If the version number is less than 1.0.1 support for TLS 1.1 or newer is not
available. Contact your operating system vendor for a solution or upgrade to
a newer distribution.
You can check your Python interpreter by installing the `requests`_ module
and executing the following command::
python -c "import requests; print(requests.get('https://www.howsmyssl.com/a/check', verify=False).json()['tls_version'])"
You should see "TLS 1.X" where X is >= 1.
You can read more about TLS versions and their security implications in this `cheat sheet`_.
.. _python.org: https://www.python.org/downloads/
.. _homebrew: https://brew.sh/
.. _macports: https://www.macports.org/
.. _requests: https://pypi.python.org/pypi/requests
.. _cheat sheet: https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html#only-support-strong-protocols
Thread Pool Size
''''''''''''''''
Motor uses the Python standard library's :class:`~concurrent.futures.ThreadPoolExecutor` to defer network
operations to threads. By default, the executor uses at most five threads per CPU core on your
system; to override the default set the environment variable ``MOTOR_MAX_WORKERS``.
Some additional threads are used for monitoring servers and background tasks, so the total
count of threads in your process will be greater.
Network Compression
'''''''''''''''''''
Like PyMongo, Motor supports network compression where network traffic between
the client and MongoDB server are compressed.
Keyword arguments to the Motor clients match those in MongoClient, documented
`here <https://pymongo.readthedocs.io/en/stable/examples/network_compression.html>`_.
By default no compression is used. If you wish to use wire compression,
you will have to install one of the optional dependencies.
Snappy requires `python-snappy <https://pypi.org/project/python-snappy>`_
and zstandard requires `zstandard <https://pypi.org/project/zstandard>`_::
$ python3 -m pip install "motor[snappy, zstd]"

View File

@ -10,12 +10,3 @@ The following is a list of people who have contributed to
- Rémi Jolin
- Andrew Svetlov
- Nikolay Novik
- Prashant Mital
- Shane Harvey
- Bulat Khasanov
- William Zhou
- Tushar Singh
- Steven Silvester
- Julius Park
- Doeke Buursma
- Scott Luu

View File

@ -1,28 +1,29 @@
"""Gratefully adapted from aiohttp, provides coroutine support to autodoc."""
from sphinx.domains.python import PyModulelevel, PyClassmember
from sphinx import addnodes
from sphinx.domains.python import PyFunction, PyMethod
class PyCoroutineMixin:
class PyCoroutineMixin(object):
def handle_signature(self, sig, signode):
ret = super().handle_signature(sig, signode)
signode.insert(0, addnodes.desc_annotation("coroutine ", "coroutine "))
ret = super(PyCoroutineMixin, self).handle_signature(sig, signode)
signode.insert(0, addnodes.desc_annotation('coroutine ', 'coroutine '))
return ret
class PyCoroutineFunction(PyCoroutineMixin, PyFunction):
class PyCoroutineFunction(PyCoroutineMixin, PyModulelevel):
def run(self):
self.name = "py:function"
return PyFunction.run(self)
self.name = 'py:function'
return PyModulelevel.run(self)
class PyCoroutineMethod(PyCoroutineMixin, PyMethod):
class PyCoroutineMethod(PyCoroutineMixin, PyClassmember):
def run(self):
self.name = "py:method"
return PyMethod.run(self)
self.name = 'py:method'
return PyClassmember.run(self)
def setup(app):
app.add_directive_to_domain("py", "coroutinefunction", PyCoroutineFunction)
app.add_directive_to_domain("py", "coroutinemethod", PyCoroutineMethod)
return {"version": "1.0", "parallel_read_safe": True}
app.add_directive_to_domain('py', 'coroutinefunction', PyCoroutineFunction)
app.add_directive_to_domain('py', 'coroutinemethod', PyCoroutineMethod)
return {'version': '1.0', 'parallel_read_safe': True}

View File

@ -2,58 +2,47 @@
Developer Guide
===============
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
Some explanations for those who would like to contribute to Motor development.
Compatibility
-------------
Motor supports the asyncio module in the standard library of Python 3.5.3 and
later.
Motor also works with Tornado 5.0 and later along with all the Python versions
it supports.
Motor is single-source compatible with all supported Python versions, although
there are some tricks for Python 3. There is some code for the ``async``
and ``await`` features of Python 3.5+ that is conditionally compiled with ``eval``
in ``core.py``.
Each new Motor feature release depends on the latest PyMongo minor version release
or newer, up to the next PyMongo major version release. For example, if 3.10
is the latest available PyMongo version when Motor 2.1 is being released, Motor 2.1
will require 3.10<=PyMongo<4.
In ``setup.py`` there are tricks to conditionally import tests depending on
Python version. ``setup.py`` also avoids installing the ``frameworks/asyncio``
directory in a Python 2 environment.
Motor supports a range of Tornado versions, and all the Python versions those
Tornado versions work with. Motor still supports Tornado 3 because New Relic
has `only begun to support Tornado 4`_, and therefore Motor continues to
support Python 2.6.
Frameworks
----------
Motor abstracts the differences between Tornado and asyncio by wrapping each in a "framework" interface.
A Motor framework is a module implementing these properties and functions:
Motor abstracts the differences between Tornado and asyncio by wrapping each in a "framework" interface. A Motor framework
is a module implementing these properties and functions:
- ``CLASS_PREFIX``
- ``add_future``
- ``call_soon``
- ``chain_future``
- ``chain_return_value``
- ``check_event_loop``
- ``coroutine`` (**DEPRECATED**)
- ``coroutine``
- ``future_or_callback``
- ``get_event_loop``
- ``get_future``
- ``is_event_loop``
- ``is_future``
- ``platform_info``
- ``pymongo_class_wrapper``
- ``run_on_executor``
- ``yieldable`` (**DEPRECATED**)
- ``yieldable``
See the ``frameworks/tornado`` and ``frameworks/asyncio`` modules.
.. note:: Starting in Motor 2.2, the functions marked **DEPRECATED** in the
list above are not used internally in Motor. Instead of being removed
from the codebase, they have been left in a deprecated state to avoid
breaking any libraries built on top of Motor. These deprecated functions
may be removed in a future major release.
A framework-specific class, like ``MotorClient`` for Tornado or
``AsyncIOMotorClient`` for asyncio, is created by the
``create_class_with_framework`` function, which combines a framework with a
@ -95,8 +84,8 @@ framework to:
- create a ``Future`` that will be resolved by the event loop when the thread finishes
- returns the ``Future`` to the caller
This is what allows Tornado or asyncio awaitables to call Motor methods with
``await`` to await I/O without blocking the event loop.
This is what allows Tornado or asyncio coroutines to call Motor methods with
``yield``, ``yield from``, or ``await`` to await I/O without blocking the event loop.
Synchro
-------
@ -113,4 +102,6 @@ Synchro is a hacky little module that re-synchronizes all Motor methods using
the Tornado IOLoop's ``run_sync`` method. ``synchrotest.py`` overrides the Python
interpreter's import machinery to allow Synchro to masquerade as PyMongo, and
runs PyMongo's test suite against it. Use ``tox -e synchro`` to check out
PyMongo's test suite and run it with Synchro.
PyMongo test suite and run it with Synchro.
.. _only begun to support Tornado 4: https://docs.newrelic.com/docs/agents/python-agent/hosting-mechanisms/introductory-tornado-4-support

View File

@ -4,12 +4,6 @@
Differences between Motor and PyMongo
=====================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. important:: This page describes using Motor with Tornado. Beginning in
version 0.5 Motor can also integrate with asyncio instead of Tornado.
@ -24,11 +18,73 @@ Motor provides a single client class, :class:`MotorClient`. Unlike PyMongo's
not begin connecting in the background when it is instantiated. Instead it
connects on demand, when you first attempt an operation.
Coroutines
----------
Callbacks and Futures
---------------------
Motor supports nearly every method PyMongo does, but Motor methods that
do network I/O are *coroutines*. See :doc:`tutorial-tornado`.
do network I/O take an optional callback function. The callback must accept two
parameters:
.. code-block:: python
def callback(result, error):
pass
Motor's asynchronous methods return immediately, and execute the
callback, with either a result or an error, when the operation has completed.
For example, :meth:`~pymongo.collection.Collection.find_one` is used in PyMongo
like:
.. code-block:: python
db = MongoClient().test
user = db.users.find_one({'name': 'Jesse'})
print user
But Motor's :meth:`~MotorCollection.find_one` method is asynchronous:
.. code-block:: python
db = MotorClient().test
def got_user(user, error):
if error:
print 'error getting user!', error
else:
print user
db.users.find_one({'name': 'Jesse'}, callback=got_user)
The callback must be passed as a keyword argument, not a positional argument.
To find multiple documents, Motor provides :meth:`~MotorCursor.to_list`:
.. code-block:: python
def got_users(users, error):
if error:
print 'error getting users!', error
else:
for user in users:
print user
db.users.find().to_list(length=10, callback=got_users)
.. seealso:: :meth:`~MotorCursor.fetch_next`
If you pass no callback to an asynchronous method, it returns a Future for use
in a :func:`coroutine <tornado.gen.coroutine>`:
.. code-block:: python
from tornado import gen
@gen.coroutine
def f():
result = yield motor_db.collection.insert_one({'name': 'Randall'})
doc = yield motor_db.collection.find_one()
See :ref:`the coroutine example <coroutine-example>`.
Threading and forking
---------------------
@ -49,8 +105,8 @@ GridFS
- File-like
PyMongo's :class:`~pymongo.grid_file.GridIn` and
:class:`~pymongo.grid_file.GridOut` strive to act like Python's built-in
PyMongo's :class:`~gridfs.grid_file.GridIn` and
:class:`~gridfs.grid_file.GridOut` strive to act like Python's built-in
file objects, so they can be passed to many functions that expect files.
But the I/O methods of :class:`MotorGridIn` and
:class:`MotorGridOut` are asynchronous, so they cannot obey the
@ -59,24 +115,25 @@ GridFS
- Setting properties
In PyMongo, you can set arbitrary attributes on
a :class:`~pymongo.grid_file.GridIn` and they're stored as metadata on
a :class:`~gridfs.grid_file.GridIn` and they're stored as metadata on
the server, even after the ``GridIn`` is closed::
fs = gridfs.GridFSBucket(db)
grid_in = fs.open_upload_stream('test_file')
grid_in, file_id = fs.open_upload_stream('test_file')
grid_in.close()
grid_in.my_field = 'my_value' # Sends update to server.
Updating metadata on a :class:`MotorGridIn` is asynchronous, so
the API is different::
async def f():
@gen.coroutine
def f():
fs = motor.motor_tornado.MotorGridFSBucket(db)
grid_in = fs.open_upload_stream('test_file')
await grid_in.close()
grid_in, file_id = fs.open_upload_stream('test_file')
yield grid_in.close()
# Sends update to server.
await grid_in.set('my_field', 'my_value')
yield grid_in.set('my_field', 'my_value')
.. seealso:: :doc:`../api-tornado/gridfs`.
@ -88,7 +145,7 @@ In PyMongo ``is_locked`` is a property of
server has been fsyncLocked requires I/O, Motor has no such convenience method.
The equivalent in Motor is::
result = await client.admin.current_op()
result = yield client.admin.current_op()
locked = bool(result.get('fsyncLock', None))
system_js
@ -96,9 +153,9 @@ system_js
PyMongo supports Javascript procedures stored in MongoDB with syntax like:
.. code-block:: pycon
.. code-block:: python
>>> db.system_js.my_func = "function(x) { return x * x; }"
>>> db.system_js.my_func = 'function(x) { return x * x; }'
>>> db.system_js.my_func(2)
4.0
@ -107,7 +164,7 @@ Motor does not.
Cursor slicing
--------------
In PyMongo, the following raises an ``IndexError`` if the collection has fewer
In Pymongo, the following raises an ``IndexError`` if the collection has fewer
than 101 documents:
.. code-block:: python
@ -119,12 +176,13 @@ In Motor, however, no exception is raised. The query simply has no results:
.. code-block:: python
async def f():
@gen.coroutine
def f():
cursor = db.collection.find()[100]
# Iterates zero or one time.
async for doc in cursor:
print(doc)
# Iterates zero or one times.
while (yield cursor.fetch_next):
doc = cursor.next_object()
The difference arises because the PyMongo :class:`~pymongo.cursor.Cursor`'s
slicing operator blocks until it has queried the MongoDB server, and determines
@ -139,15 +197,26 @@ There are two ways to create a capped collection using PyMongo:
.. code-block:: python
# Typical:
db.create_collection("collection1", capped=True, size=1000)
db.create_collection(
'collection1',
capped=True,
size=1000)
# Unusual:
collection = Collection(db, "collection2", capped=True, size=1000)
collection = Collection(
db,
'collection2',
capped=True,
size=1000)
Motor can't do I/O in a constructor, so the unusual style is prohibited and
only the typical style is allowed:
.. code-block:: python
async def f():
await db.create_collection("collection1", capped=True, size=1000)
@gen.coroutine
def f():
yield db.create_collection(
'collection1',
capped=True,
size=1000)

View File

@ -1,59 +1,51 @@
# These comments let tutorial-asyncio.rst include this code in sections.
# -- setup-start --
from aiohttp import web
import asyncio
from aiohttp import web
from motor.motor_asyncio import AsyncIOMotorClient
async def setup_db():
@asyncio.coroutine
def setup_db():
db = AsyncIOMotorClient().test
await db.pages.drop()
html = "<html><body>{}</body></html>"
await db.pages.insert_one({"_id": "page-one", "body": html.format("Hello!")})
yield from db.pages.drop()
html = '<html><body>{}</body></html>'
yield from db.pages.insert_one({'_id': 'page-one',
'body': html.format('Hello!')})
await db.pages.insert_one({"_id": "page-two", "body": html.format("Goodbye.")})
yield from db.pages.insert_one({'_id': 'page-two',
'body': html.format('Goodbye.')})
return db
# -- setup-end --
# -- handler-start --
async def page_handler(request):
@asyncio.coroutine
def page_handler(request):
# If the visitor gets "/pages/page-one", then page_name is "page-one".
page_name = request.match_info.get("page_name")
page_name = request.match_info.get('page_name')
# Retrieve the long-lived database handle.
db = request.app["db"]
db = request.app['db']
# Find the page by its unique id.
document = await db.pages.find_one(page_name)
document = yield from db.pages.find_one(page_name)
if not document:
return web.HTTPNotFound(text=f"No page named {page_name!r}")
return web.Response(body=document["body"].encode(), content_type="text/html")
return web.HTTPNotFound(text='No page named {!r}'.format(page_name))
return web.Response(body=document['body'].encode(),
content_type='text/html')
# -- handler-end --
# -- main-start --
async def init_connection():
db = await setup_db()
app = web.Application()
app["db"] = db
# Route requests to the page_handler() coroutine.
app.router.add_get("/pages/{page_name}", page_handler)
return app
def main():
app = init_connection()
web.run_app(app)
if __name__ == "__main__":
main()
loop = asyncio.get_event_loop()
db = loop.run_until_complete(setup_db())
app = web.Application()
app['db'] = db
# Route requests to the page_handler() coroutine.
app.router.add_get('/pages/{page_name}', page_handler)
web.run_app(app)
# -- main-end --

View File

@ -1,7 +1,5 @@
"""Serve pre-compressed static content from GridFS with aiohttp.
Requires Python 3.5 or later and aiohttp 3.0 or later.
Start a MongoDB server on its default port, run this script, and visit:
http://localhost:8080/fs/my_file
@ -14,7 +12,7 @@ import tempfile
import aiohttp.web
from motor.aiohttp import AIOHTTPGridFS
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorGridFSBucket
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorGridFS
client = AsyncIOMotorClient()
@ -22,33 +20,33 @@ client = AsyncIOMotorClient()
# Use Motor to put compressed data in GridFS, with filename "my_file".
async def put_gridfile():
with tempfile.TemporaryFile() as tmp:
with gzip.GzipFile(mode="wb", fileobj=tmp) as gzfile:
with gzip.GzipFile(mode='wb', fileobj=tmp) as gzfile:
for _ in range(10):
gzfile.write(b"Nonesuch nonsense\n")
gzfile.write(b'Nonesuch nonsense\n')
gfs = AsyncIOMotorGridFSBucket(client.my_database)
gfs = AsyncIOMotorGridFS(client.my_database)
tmp.seek(0)
await gfs.upload_from_stream(
filename="my_file", source=tmp, metadata={"contentType": "text", "compressed": True}
)
await gfs.put(tmp,
filename='my_file',
content_type='text',
metadata={'compressed': True})
asyncio.run(put_gridfile())
asyncio.get_event_loop().run_until_complete(put_gridfile())
# Add "Content-Encoding: gzip" header for compressed data.
def gzip_header(response, gridout):
if gridout.metadata.get("compressed"):
response.headers["Content-Encoding"] = "gzip"
if gridout.metadata.get('compressed'):
response.headers['Content-Encoding'] = 'gzip'
gridfs_handler = AIOHTTPGridFS(client.my_database, set_extra_headers=gzip_header)
gridfs_handler = AIOHTTPGridFS(client.my_database,
set_extra_headers=gzip_header)
app = aiohttp.web.Application()
# The GridFS URL pattern must have a "{filename}" variable.
resource = app.router.add_resource("/fs/{filename}")
resource.add_route("GET", gridfs_handler)
resource.add_route("HEAD", gridfs_handler)
resource = app.router.add_resource('/fs/{filename}')
resource.add_route('GET', gridfs_handler)
resource.add_route('HEAD', gridfs_handler)
aiohttp.web.run_app(app)

View File

@ -1,13 +1,6 @@
AIOHTTPGridFS Example
=====================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
Serve pre-compressed static content from GridFS over HTTP. Uses the `aiohttp`_
web framework and :class:`~motor.aiohttp.AIOHTTPGridFS`.

View File

@ -3,13 +3,6 @@
Authentication With Motor
=========================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
This page describes using Motor with Tornado. Beginning in
version 0.5 Motor can also integrate with asyncio instead of Tornado.
@ -18,8 +11,10 @@ replica sets or sharded clusters, ``--keyFile``. Create an admin user and
optionally normal users or read-only users.
.. seealso:: `MongoDB Authentication
<https://mongodb.com/docs/manual/tutorial/control-access-to-mongodb-with-authentication/>`_
<http://docs.mongodb.org/manual/tutorial/control-access-to-mongodb-with-authentication/>`_
Authentication at Startup
-------------------------
To create an authenticated connection use a `MongoDB connection URI`_::
uri = "mongodb://user:pass@localhost:27017/database_name"
@ -27,4 +22,19 @@ To create an authenticated connection use a `MongoDB connection URI`_::
Motor logs in to the server on demand, when you first attempt an operation.
.. _MongoDB connection URI: https://mongodb.com/docs/manual/reference/connection-string/
Asynchronous Authentication
---------------------------
Use the non-blocking :meth:`~MotorDatabase.authenticate` method to log
in after starting the IOLoop::
client = motor.motor_tornado.MotorClient('localhost', 27017)
@gen.coroutine
def login(c):
yield c.my_database.authenticate("user", "pass")
After you've logged in to a database with a given :class:`MotorClient`, all further
operations on that database using that client will already be authenticated
until you call :meth:`~MotorDatabase.logout`.
.. _MongoDB connection URI: http://docs.mongodb.org/manual/reference/connection-string/

View File

@ -1,101 +0,0 @@
import asyncio
import os
from bson import json_util
from bson.codec_options import CodecOptions
from pymongo.encryption import Algorithm
from pymongo.encryption_options import AutoEncryptionOpts
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption
async def create_json_schema_file(kms_providers, key_vault_namespace, key_vault_client):
client_encryption = AsyncIOMotorClientEncryption(
kms_providers,
key_vault_namespace,
key_vault_client,
# The CodecOptions class used for encrypting and decrypting.
# This should be the same CodecOptions instance you have configured
# on MotorClient, Database, or Collection. We will not be calling
# encrypt() or decrypt() in this example so we can use any
# CodecOptions.
CodecOptions(),
)
# Create a new data key and json schema for the encryptedField.
# https://dochub.mongodb.org/core/client-side-field-level-encryption-automatic-encryption-rules
data_key_id = await client_encryption.create_data_key(
"local", key_alt_names=["pymongo_encryption_example_1"]
)
schema = {
"properties": {
"encryptedField": {
"encrypt": {
"keyId": [data_key_id],
"bsonType": "string",
"algorithm": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
}
}
},
"bsonType": "object",
}
# Use CANONICAL_JSON_OPTIONS so that other drivers and tools will be
# able to parse the MongoDB extended JSON file.
json_schema_string = json_util.dumps(schema, json_options=json_util.CANONICAL_JSON_OPTIONS)
with open("jsonSchema.json", "w") as file:
file.write(json_schema_string)
async def main():
# The MongoDB namespace (db.collection) used to store the
# encrypted documents in this example.
encrypted_namespace = "test.coll"
# This must be the same master key that was used to create
# the encryption key.
local_master_key = os.urandom(96)
kms_providers = {"local": {"key": local_master_key}}
# The MongoDB namespace (db.collection) used to store
# the encryption data keys.
key_vault_namespace = "encryption.__pymongoTestKeyVault"
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
# The MotorClient used to access the key vault (key_vault_namespace).
key_vault_client = AsyncIOMotorClient()
key_vault = key_vault_client[key_vault_db_name][key_vault_coll_name]
# Ensure that two data keys cannot share the same keyAltName.
await key_vault.drop()
await key_vault.create_index(
"keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}}
)
await create_json_schema_file(kms_providers, key_vault_namespace, key_vault_client)
# Load the JSON Schema and construct the local schema_map option.
with open("jsonSchema.json") as file:
json_schema_string = file.read()
json_schema = json_util.loads(json_schema_string)
schema_map = {encrypted_namespace: json_schema}
auto_encryption_opts = AutoEncryptionOpts(
kms_providers, key_vault_namespace, schema_map=schema_map
)
client = AsyncIOMotorClient(auto_encryption_opts=auto_encryption_opts)
db_name, coll_name = encrypted_namespace.split(".", 1)
coll = client[db_name][coll_name]
# Clear old data
await coll.drop()
await coll.insert_one({"encryptedField": "123456789"})
decrypted_doc = await coll.find_one()
print(f"Decrypted document: {decrypted_doc}")
unencrypted_coll = AsyncIOMotorClient()[db_name][coll_name]
encrypted_doc = await unencrypted_coll.find_one()
print(f"Encrypted document: {encrypted_doc}")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,17 +1,8 @@
.. currentmodule:: motor.motor_tornado
.. _bulk-write-tutorial:
Bulk Write Operations
=====================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. testsetup::
client = MotorClient()
@ -37,11 +28,11 @@ bulk insert operations.
.. doctest::
>>> async def f():
... await db.test.insert_many(({"i": i} for i in range(10000)))
... count = await db.test.count_documents({})
>>> @gen.coroutine
... def f():
... yield db.test.insert_many(({'i': i} for i in range(10000)))
... count = yield db.test.count()
... print("Final count: %d" % count)
...
>>>
>>> IOLoop.current().run_sync(f)
Final count: 10000
@ -49,9 +40,11 @@ bulk insert operations.
Mixed Bulk Write Operations
---------------------------
.. versionadded:: 0.2
Motor also supports executing mixed bulk write operations. A batch
of insert, update, and remove operations can be executed together using
the bulk write operations API.
of insert, update, and delete operations can be executed together using
the Bulk API.
.. _ordered_bulk:
@ -59,28 +52,26 @@ Ordered Bulk Write Operations
.............................
Ordered bulk write operations are batched and sent to the server in the
order provided for serial execution. The return value is an instance of
:class:`~pymongo.results.BulkWriteResult` describing the type and count
of operations performed.
order provided for serial execution. The return value is a document
describing the type and count of operations performed.
.. doctest::
:options: +NORMALIZE_WHITESPACE
>>> from pprint import pprint
>>> from pymongo import InsertOne, DeleteMany, ReplaceOne, UpdateOne
>>> async def f():
... result = await db.test.bulk_write(
... [
... DeleteMany({}), # Remove all documents from the previous example.
... InsertOne({"_id": 1}),
... InsertOne({"_id": 2}),
... InsertOne({"_id": 3}),
... UpdateOne({"_id": 1}, {"$set": {"foo": "bar"}}),
... UpdateOne({"_id": 4}, {"$inc": {"j": 1}}, upsert=True),
... ReplaceOne({"j": 1}, {"j": 2}),
... ]
... )
... pprint(result.bulk_api_result)
>>>
>>> @gen.coroutine
... def f():
... bulk = db.test.initialize_ordered_bulk_op()
... # Remove all documents from the previous example.
... bulk.find({}).remove()
... bulk.insert({'_id': 1})
... bulk.insert({'_id': 2})
... bulk.insert({'_id': 3})
... bulk.find({'_id': 1}).update({'$set': {'foo': 'bar'}})
... bulk.find({'_id': 4}).upsert().update({'$inc': {'j': 1}})
... bulk.find({'j': 1}).replace_one({'j': 2})
... result = yield bulk.execute()
... pprint(result)
...
>>> IOLoop.current().run_sync(f)
{'nInserted': 3,
@ -93,28 +84,28 @@ of operations performed.
'writeErrors': []}
The first write failure that occurs (e.g. duplicate key error) aborts the
remaining operations, and Motor raises
:class:`~pymongo.errors.BulkWriteError`. The :attr:`details` attribute of
the exception instance provides the execution results up until the failure
occurred and details about the failure - including the operation that caused
the failure.
remaining operations, and Motor raises :class:`~pymongo.errors.BulkWriteError`.
The :attr:`details` attibute of the exception instance provides the execution
results up until the failure occurred and details about the failure - including
the operation that caused the failure.
.. doctest::
:options: +NORMALIZE_WHITESPACE
>>> from pymongo import InsertOne, DeleteOne, ReplaceOne
>>> from pymongo.errors import BulkWriteError
>>> async def f():
... requests = [
... ReplaceOne({"j": 2}, {"i": 5}),
... InsertOne({"_id": 4}), # Violates the unique key constraint on _id.
... DeleteOne({"i": 5}),
... ]
... try:
... await db.test.bulk_write(requests)
... except BulkWriteError as bwe:
... pprint(bwe.details)
>>>
>>> @gen.coroutine
... def f():
... bulk = db.test.initialize_ordered_bulk_op()
... bulk.find({'j': 2}).replace_one({'i': 5})
... # Violates the unique key constraint on _id.
...
... bulk.insert({'_id': 4})
... bulk.find({'i': 5}).remove_one()
... try:
... yield bulk.execute()
... except BulkWriteError as err:
... pprint(err.details)
...
>>> IOLoop.current().run_sync(f)
{'nInserted': 0,
'nMatched': 1,
@ -126,8 +117,6 @@ the failure.
'writeErrors': [{'code': 11000,
'errmsg': '... duplicate key error ...',
'index': 1,
'keyPattern': {'_id': 1},
'keyValue': {'_id': 4},
'op': {'_id': 4}}]}
.. _unordered_bulk:
@ -144,20 +133,19 @@ constraint on _id. Since we are doing unordered execution the second
and fourth operations succeed.
.. doctest::
:options: +NORMALIZE_WHITESPACE
>>> async def f():
... requests = [
... InsertOne({"_id": 1}),
... DeleteOne({"_id": 2}),
... InsertOne({"_id": 3}),
... ReplaceOne({"_id": 4}, {"i": 1}),
... ]
>>> @gen.coroutine
... def f():
... bulk = db.test.initialize_unordered_bulk_op()
... bulk.insert({'_id': 1})
... bulk.find({'_id': 2}).remove_one()
... bulk.insert({'_id': 3})
... bulk.find({'_id': 4}).replace_one({'i': 1})
... try:
... await db.test.bulk_write(requests, ordered=False)
... except BulkWriteError as bwe:
... pprint(bwe.details)
...
... yield bulk.execute()
... except BulkWriteError as err:
... pprint(err.details)
...
>>> IOLoop.current().run_sync(f)
{'nInserted': 0,
'nMatched': 1,
@ -169,37 +157,41 @@ and fourth operations succeed.
'writeErrors': [{'code': 11000,
'errmsg': '... duplicate key error ...',
'index': 0,
'keyPattern': {'_id': 1},
'keyValue': {'_id': 1},
'op': {'_id': 1}},
{'code': 11000,
'errmsg': '... duplicate key error ...',
'index': 2,
'keyPattern': {'_id': 1},
'keyValue': {'_id': 3},
'op': {'_id': 3}}]}
Write Concern
.............
Bulk operations are executed with the
:attr:`~pymongo.collection.Collection.write_concern` of the collection they
are executed against. Write concern errors (e.g. wtimeout) will be reported
after all operations are attempted, regardless of execution order.
By default bulk operations are executed with the
:meth:`~MotorCollection.write_concern` of the collection they are
executed against, typically the default write concern ``{w: 1}``. A custom
write concern can be passed to the
:meth:`~MotorBulkOperationBuilder.execute` method. Write concern
errors (e.g. wtimeout) will be reported after all operations are attempted,
regardless of execution order.
.. doctest::
:options: +SKIP
.. Standalone MongoDB raises "can't use w>1" with this example, so skip it.
>>> from pymongo import WriteConcern
>>> async def f():
... coll = db.get_collection("test", write_concern=WriteConcern(w=4, wtimeout=1))
>>> @gen.coroutine
... def f():
... bulk = db.test.initialize_ordered_bulk_op()
... bulk.insert({'a': 0})
... bulk.insert({'a': 1})
... bulk.insert({'a': 2})
... bulk.insert({'a': 3})
... try:
... await coll.bulk_write([InsertOne({"a": i}) for i in range(4)])
... except BulkWriteError as bwe:
... pprint(bwe.details)
...
... # Times out if the replica set has fewer than four members.
... yield bulk.execute({'w': 4, 'wtimeout': 1})
... except BulkWriteError as err:
... pprint(err.details)
...
>>> IOLoop.current().run_sync(f)
{'nInserted': 4,
'nMatched': 0,

View File

@ -0,0 +1,201 @@
.. currentmodule:: motor.motor_tornado
Examples With Callbacks And Coroutines
======================================
Programming with Motor is far easier with Tornado coroutines than with
raw callbacks. Here's an example that shows the difference.
This page describes using Motor with Tornado. Beginning in
version 0.5 Motor can also integrate with asyncio instead of Tornado.
.. contents::
With callbacks
--------------
An application that can create and display short messages:
.. code-block:: python
import tornado.web, tornado.ioloop
import motor
class NewMessageHandler(tornado.web.RequestHandler):
def get(self):
"""Show a 'compose message' form."""
self.write('''
<form method="post">
<input type="text" name="msg">
<input type="submit">
</form>''')
# Method exits before the HTTP request completes, thus "asynchronous"
@tornado.web.asynchronous
def post(self):
"""Insert a message."""
msg = self.get_argument('msg')
# Async insert; callback is executed when insert completes
self.settings['db'].messages.insert_one(
{'msg': msg},
callback=self._on_response)
def _on_response(self, result, error):
if error:
raise tornado.web.HTTPError(500, error)
else:
self.redirect('/')
class MessagesHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
"""Display all messages."""
self.write('<a href="/compose">Compose a message</a><br>')
self.write('<ul>')
db = self.settings['db']
db.messages.find().sort([('_id', -1)]).each(self._got_message)
def _got_message(self, message, error):
if error:
raise tornado.web.HTTPError(500, error)
elif message:
self.write('<li>%s</li>' % message['msg'])
else:
# Iteration complete
self.write('</ul>')
self.finish()
db = motor.motor_tornado.MotorClient().test
application = tornado.web.Application(
[
(r'/compose', NewMessageHandler),
(r'/', MessagesHandler)
],
db=db
)
print('Listening on http://localhost:8888')
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
The call to :meth:`~MotorCursor.each` could be
replaced with :meth:`~MotorCursor.to_list`, which is easier to use
with templates because the callback receives the entire result at once:
.. code-block:: python
from tornado import template
messages_template = template.Template('''<ul>
{% for message in messages %}
<li>{{ message['msg'] }}</li>
{% end %}
</ul>''')
class MessagesHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
"""Display all messages
"""
self.write('<a href="/compose">Compose a message</a><br>')
self.write('<ul>')
db = self.settings['db']
(db.messages.find()
.sort([('_id', -1)])
.limit(10)
.to_list(length=10, callback=self._got_messages))
def _got_messages(self, messages, error):
if error:
raise tornado.web.HTTPError(500, error)
elif messages:
self.write(messages_template.generate(messages=messages))
self.finish()
To protect you from buffering huge numbers of documents in memory, ``to_list``
requires a maximum ``length`` argument.
.. _coroutine-example:
With coroutines
---------------
Motor's asynchronous methods return `Futures <tornado.concurrent.Future>`.
Yield a Future to resolve it into a result or an exception:
.. code-block:: python
from tornado import gen
class NewMessageHandler(tornado.web.RequestHandler):
@gen.coroutine
def post(self):
"""Insert a message."""
msg = self.get_argument('msg')
db = self.settings['db']
# insert_one() returns a Future. Yield the Future to get the result.
result = yield db.messages.insert_one({'msg': msg})
# Success
self.redirect('/')
class MessagesHandler(tornado.web.RequestHandler):
@gen.coroutine
def get(self):
"""Display all messages."""
self.write('<a href="/compose">Compose a message</a><br>')
self.write('<ul>')
db = self.settings['db']
cursor = db.messages.find().sort([('_id', -1)])
while (yield cursor.fetch_next):
message = cursor.next_object()
self.write('<li>%s</li>' % message['msg'])
# Iteration complete
self.write('</ul>')
self.finish()
One can parallelize operations and wait for all to complete. To query for
two messages at once and wait for both:
.. code-block:: python
msg = yield db.messages.find_one({'_id': msg_id})
# Start getting the previous. find_one returns a Future.
prev_future = db.messages.find_one({'_id': {'$lt': msg_id}})
# Start getting the next.
next_future = db.messages.find_one({'_id': {'$gt': msg_id}})
# Wait for both to complete by yielding the Futures.
previous_msg, next_msg = yield [prev_future, next_future]
`async` and `await`
-------------------
Python 3.5 introduces `native coroutines`_ that use the `async` and `await`
keywords. Starting in Python 3.5, you can use `async def` instead of the
`gen.coroutine` decorator, and replace the while-loop with `async for`:
.. code-block:: python
class MessagesHandler(tornado.web.RequestHandler):
async def get(self):
"""Display all messages."""
self.write('<a href="/compose">Compose a message</a><br>')
self.write('<ul>')
db = self.settings['db']
# New in Python 3.5:
async for message in db.messages.find().sort([('_id', -1)]):
self.write('<li>%s</li>' % message['msg'])
self.write('</ul>')
self.finish()
This version of the code is dramatically faster.
.. _native coroutines: https://www.python.org/dev/peps/pep-0492/

View File

@ -1,131 +0,0 @@
.. _Client-Side Field Level Encryption:
Client-Side Field Level Encryption
==================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
Starting in MongoDB 4.2, client-side field level encryption allows an application
to encrypt specific data fields in addition to pre-existing MongoDB
encryption features such as `Encryption at Rest
<https://dochub.mongodb.org/core/security-encryption-at-rest>`_ and
`TLS/SSL (Transport Encryption)
<https://dochub.mongodb.org/core/security-tls-transport-encryption>`_.
With field level encryption, applications can encrypt fields in documents
*prior* to transmitting data over the wire to the server. Client-side field
level encryption supports workloads where applications must guarantee that
unauthorized parties, including server administrators, cannot read the
encrypted data.
.. mongodoc:: client-side-field-level-encryption
Dependencies
------------
To get started using client-side field level encryption in your project,
you will need to install the
`pymongocrypt <https://pypi.org/project/pymongocrypt/>`_ library
as well as the driver itself. Install both the driver and a compatible
version of pymongocrypt like this::
$ python -m pip install 'motor[encryption]'
Note that installing on Linux requires pip 19 or later for manylinux2010 wheel
support. For more information about installing pymongocrypt see
`the installation instructions on the project's PyPI page
<https://pypi.org/project/pymongocrypt/>`_.
mongocryptd
-----------
The ``mongocryptd`` binary is required for automatic client-side encryption
and is included as a component in the `MongoDB Enterprise Server package
<https://dochub.mongodb.org/core/install-mongodb-enterprise>`_. For more
information on this binary, see the `PyMongo documentation on mongocryptd
<https://pymongo.readthedocs.io/en/stable/examples/encryption.html>`_.
Automatic Client-Side Field Level Encryption
--------------------------------------------
Automatic client-side field level encryption is enabled by creating a
:class:`~motor.motor_asyncio.AsyncIOMotorClient` with the ``auto_encryption_opts``
option set to an instance of
:class:`~pymongo.encryption_options.AutoEncryptionOpts`. The following
examples show how to setup automatic client-side field level encryption
using :class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption` to create a new
encryption data key.
.. note:: Automatic client-side field level encryption requires MongoDB 4.2+
enterprise or a MongoDB 4.2+ Atlas cluster. The community version of the
server supports automatic decryption as well as
:ref:`explicit-client-side-encryption`.
Providing Local Automatic Encryption Rules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following example shows how to specify automatic encryption rules via the
``schema_map`` option. The automatic encryption rules are expressed using a
`strict subset of the JSON Schema syntax
<https://dochub.mongodb.org/core/client-side-field-level-encryption-automatic-encryption-rules>`_.
Supplying a ``schema_map`` provides more security than relying on
JSON Schemas obtained from the server. It protects against a
malicious server advertising a false JSON Schema, which could trick
the client into sending unencrypted data that should be encrypted.
JSON Schemas supplied in the ``schema_map`` only apply to configuring
automatic client-side field level encryption. Other validation
rules in the JSON schema will not be enforced by the driver and
will result in an error.
.. literalinclude:: auto_csfle_example.py
:language: python3
Server-Side Field Level Encryption Enforcement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The MongoDB 4.2+ server supports using schema validation to enforce encryption
of specific fields in a collection. This schema validation will prevent an
application from inserting unencrypted values for any fields marked with the
``"encrypt"`` JSON schema keyword.
The following example shows how to setup automatic client-side field level
encryption using
:class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption` to create a new encryption
data key and create a collection with the
`Automatic Encryption JSON Schema Syntax
<https://dochub.mongodb.org/core/client-side-field-level-encryption-automatic-encryption-rules>`_.
.. literalinclude:: server_fle_enforcement_example.py
:language: python3
.. _explicit-client-side-encryption:
Explicit Encryption
~~~~~~~~~~~~~~~~~~~
Explicit encryption is a MongoDB community feature and does not use the
``mongocryptd`` process. Explicit encryption is provided by the
:class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption` class, for example:
.. literalinclude:: explicit_encryption_example.py
:language: python3
Explicit Encryption with Automatic Decryption
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Although automatic encryption requires MongoDB 4.2 enterprise or a
MongoDB 4.2 Atlas cluster, automatic *decryption* is supported for all users.
To configure automatic *decryption* without automatic *encryption* set
``bypass_auto_encryption=True`` in
:class:`~pymongo.encryption_options.AutoEncryptionOpts`:
.. literalinclude:: explicit_encryption_automatic_decryption_example.py
:language: python3

View File

@ -1,76 +0,0 @@
import asyncio
import os
from pymongo.encryption import Algorithm
from pymongo.encryption_options import AutoEncryptionOpts
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption
async def main():
# This must be the same master key that was used to create
# the encryption key.
local_master_key = os.urandom(96)
kms_providers = {"local": {"key": local_master_key}}
# The MongoDB namespace (db.collection) used to store
# the encryption data keys.
key_vault_namespace = "encryption.__pymongoTestKeyVault"
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
# bypass_auto_encryption=True disable automatic encryption but keeps
# the automatic _decryption_ behavior. bypass_auto_encryption will
# also disable spawning mongocryptd.
auto_encryption_opts = AutoEncryptionOpts(
kms_providers, key_vault_namespace, bypass_auto_encryption=True
)
client = AsyncIOMotorClient(auto_encryption_opts=auto_encryption_opts)
coll = client.test.coll
# Clear old data
await coll.drop()
# Set up the key vault (key_vault_namespace) for this example.
key_vault = client[key_vault_db_name][key_vault_coll_name]
# Ensure that two data keys cannot share the same keyAltName.
await key_vault.drop()
await key_vault.create_index(
"keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}}
)
client_encryption = AsyncIOMotorClientEncryption(
kms_providers,
key_vault_namespace,
# The MotorClient to use for reading/writing to the key vault.
# This can be the same MotorClient used by the main application.
client,
# The CodecOptions class used for encrypting and decrypting.
# This should be the same CodecOptions instance you have configured
# on MotorClient, Database, or Collection.
coll.codec_options,
)
# Create a new data key for the encryptedField.
_ = await client_encryption.create_data_key(
"local", key_alt_names=["pymongo_encryption_example_4"]
)
# Explicitly encrypt a field:
encrypted_field = await client_encryption.encrypt(
"123456789",
Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
key_alt_name="pymongo_encryption_example_4",
)
await coll.insert_one({"encryptedField": encrypted_field})
# Automatically decrypts any encrypted fields.
doc = await coll.find_one()
print(f"Decrypted document: {doc}")
unencrypted_coll = AsyncIOMotorClient().test.coll
print(f"Encrypted document: {await unencrypted_coll.find_one()}")
# Cleanup resources.
await client_encryption.close()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,68 +0,0 @@
import asyncio
import os
from pymongo.encryption import Algorithm
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption
async def main():
# This must be the same master key that was used to create
# the encryption key.
local_master_key = os.urandom(96)
kms_providers = {"local": {"key": local_master_key}}
# The MongoDB namespace (db.collection) used to store
# the encryption data keys.
key_vault_namespace = "encryption.__pymongoTestKeyVault"
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
# The MotorClient used to read/write application data.
client = AsyncIOMotorClient()
coll = client.test.coll
# Clear old data
await coll.drop()
# Set up the key vault (key_vault_namespace) for this example.
key_vault = client[key_vault_db_name][key_vault_coll_name]
# Ensure that two data keys cannot share the same keyAltName.
await key_vault.drop()
await key_vault.create_index(
"keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}}
)
client_encryption = AsyncIOMotorClientEncryption(
kms_providers,
key_vault_namespace,
# The Motorlient to use for reading/writing to the key vault.
# This can be the same MotorClient used by the main application.
client,
# The CodecOptions class used for encrypting and decrypting.
# This should be the same CodecOptions instance you have configured
# on MotorClient, Database, or Collection.
coll.codec_options,
)
# Create a new data key for the encryptedField.
data_key_id = await client_encryption.create_data_key(
"local", key_alt_names=["pymongo_encryption_example_3"]
)
# Explicitly encrypt a field:
encrypted_field = await client_encryption.encrypt(
"123456789", Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_id=data_key_id
)
await coll.insert_one({"encryptedField": encrypted_field})
doc = await coll.find_one()
print(f"Encrypted document: {doc}")
# Explicitly decrypt the field:
doc["encryptedField"] = await client_encryption.decrypt(doc["encryptedField"])
print(f"Decrypted document: {doc}")
# Cleanup resources.
await client_encryption.close()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,25 +1,15 @@
Motor Examples
==============
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. seealso:: :doc:`../tutorial-tornado`
.. toctree::
callbacks-and-coroutines
bulk
monitoring
tailable-cursors
tornado_change_stream_example
authentication
aiohttp_gridfs_example
encryption
timeouts
type_hints
See also :ref:`example-web-application-aiohttp`.

View File

@ -3,13 +3,6 @@
Application Performance Monitoring (APM)
========================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
Motor implements the same `Command Monitoring`_ and `Topology Monitoring`_ specifications as other MongoDB drivers.
Therefore, you can register callbacks to be notified of every MongoDB query or command your program sends, and the server's reply to each, as well as getting a notification whenever the driver checks a server's status or detects a change in your replica set.
@ -87,10 +80,12 @@ Further Information
See also:
* PyMongo's :mod:`~pymongo.monitoring` module
* The `Command Monitoring`_ Spec
* The `Topology Monitoring`_ Spec
* The `monitoring_example.py`_ example file in the Motor repository
* `The Command Monitoring Spec`_
* `The Topology Monitoring Spec`_
* The `monitoring.py`_ example file in the Motor repository
.. _Command Monitoring: https://github.com/mongodb/specifications/blob/master/source/command-logging-and-monitoring/command-logging-and-monitoring.rst
.. _Topology Monitoring: https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring-logging-and-monitoring.rst
.. _monitoring_example.py: https://github.com/mongodb/motor/blob/master/doc/examples/monitoring_example.py
.. _The Command Monitoring Spec:
.. _Command Monitoring: https://github.com/mongodb/specifications/blob/master/source/command-monitoring/command-monitoring.rst
.. _The Topology Monitoring Spec:
.. _Topology Monitoring: https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring-monitoring.rst
.. _monitoring.py: https://github.com/mongodb/motor/blob/master/doc/examples/monitoring.py

View File

@ -26,29 +26,21 @@ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
class CommandLogger(monitoring.CommandListener):
def started(self, event):
logging.info(
f"Command {event.command_name} with request id "
f"{event.request_id} started on server "
f"{event.connection_id}"
)
logging.info("Command {0.command_name} with request id "
"{0.request_id} started on server "
"{0.connection_id}".format(event))
def succeeded(self, event):
logging.info(
f"Command {event.command_name} with request id "
f"{event.request_id} on server {event.connection_id} "
f"succeeded in {event.duration_micros} "
"microseconds"
)
logging.info("Command {0.command_name} with request id "
"{0.request_id} on server {0.connection_id} "
"succeeded in {0.duration_micros} "
"microseconds".format(event))
def failed(self, event):
logging.info(
f"Command {event.command_name} with request id "
f"{event.request_id} on server {event.connection_id} "
f"failed in {event.duration_micros} "
"microseconds"
)
logging.info("Command {0.command_name} with request id "
"{0.request_id} on server {0.connection_id} "
"failed in {0.duration_micros} "
"microseconds".format(event))
# command logger end
# command logger register start
@ -57,14 +49,13 @@ monitoring.register(CommandLogger())
# motorclient start
from tornado import gen, ioloop
from motor import MotorClient
client = MotorClient()
async def do_insert():
await client.test.collection.insert_one({"message": "hi!"})
await client.test.collection.insert({'message': 'hi!'})
# For this example, wait 10 seconds for more monitoring events to fire.
await gen.sleep(10)
@ -73,70 +64,68 @@ async def do_insert():
ioloop.IOLoop.current().run_sync(do_insert)
# motorclient end
# server logger start
class ServerLogger(monitoring.ServerListener):
def opened(self, event):
logging.info(f"Server {event.server_address} added to topology {event.topology_id}")
logging.info("Server {0.server_address} added to topology "
"{0.topology_id}".format(event))
def description_changed(self, event):
previous_server_type = event.previous_description.server_type
new_server_type = event.new_description.server_type
if new_server_type != previous_server_type:
logging.info(
f"Server {event.server_address} changed type from "
f"{event.previous_description.server_type_name} to "
f"{event.new_description.server_type_name}"
)
"Server {0.server_address} changed type from "
"{0.previous_description.server_type_name} to "
"{0.new_description.server_type_name}".format(event))
def closed(self, event):
logging.warning(f"Server {event.server_address} removed from topology {event.topology_id}")
logging.warning("Server {0.server_address} removed from topology "
"{0.topology_id}".format(event))
monitoring.register(ServerLogger())
# server logger end
# topology logger start
class TopologyLogger(monitoring.TopologyListener):
def opened(self, event):
logging.info(f"Topology with id {event.topology_id} opened")
logging.info("Topology with id {0.topology_id} "
"opened".format(event))
def description_changed(self, event):
logging.info(f"Topology description updated for topology id {event.topology_id}")
logging.info("Topology description updated for "
"topology id {0.topology_id}".format(event))
previous_topology_type = event.previous_description.topology_type
new_topology_type = event.new_description.topology_type
if new_topology_type != previous_topology_type:
logging.info(
f"Topology {event.topology_id} changed type from "
f"{event.previous_description.topology_type_name} to "
f"{event.new_description.topology_type_name}"
)
"Topology {0.topology_id} changed type from "
"{0.previous_description.topology_type_name} to "
"{0.new_description.topology_type_name}".format(event))
def closed(self, event):
logging.info(f"Topology with id {event.topology_id} closed")
logging.info("Topology with id {0.topology_id} "
"closed".format(event))
monitoring.register(TopologyLogger())
# topology logger end
# heartbeat logger start
class HeartbeatLogger(monitoring.ServerHeartbeatListener):
def started(self, event):
logging.info(f"Heartbeat sent to server {event.connection_id}")
logging.info("Heartbeat sent to server "
"{0.connection_id}".format(event))
def succeeded(self, event):
logging.info(
f"Heartbeat to server {event.connection_id} "
"succeeded with reply "
f"{event.reply.document}"
)
logging.info("Heartbeat to server {0.connection_id} "
"succeeded with reply "
"{0.reply.document}".format(event))
def failed(self, event):
logging.warning(
f"Heartbeat to server {event.connection_id} failed with error {event.reply}"
)
logging.warning("Heartbeat to server {0.connection_id} "
"failed with error {0.reply}".format(event))
monitoring.register(HeartbeatLogger())

View File

@ -1,100 +0,0 @@
import asyncio
import os
from bson.binary import STANDARD
from bson.codec_options import CodecOptions
from pymongo.encryption import Algorithm
from pymongo.encryption_options import AutoEncryptionOpts
from pymongo.errors import OperationFailure
from pymongo.write_concern import WriteConcern
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption
async def main():
# The MongoDB namespace (db.collection) used to store the
# encrypted documents in this example.
encrypted_namespace = "test.coll"
# This must be the same master key that was used to create
# the encryption key.
local_master_key = os.urandom(96)
kms_providers = {"local": {"key": local_master_key}}
# The MongoDB namespace (db.collection) used to store
# the encryption data keys.
key_vault_namespace = "encryption.__pymongoTestKeyVault"
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
# The MotorClient used to access the key vault (key_vault_namespace).
key_vault_client = AsyncIOMotorClient()
key_vault = key_vault_client[key_vault_db_name][key_vault_coll_name]
# Ensure that two data keys cannot share the same keyAltName.
await key_vault.drop()
await key_vault.create_index(
"keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}}
)
client_encryption = AsyncIOMotorClientEncryption(
kms_providers,
key_vault_namespace,
key_vault_client,
# The CodecOptions class used for encrypting and decrypting.
# This should be the same CodecOptions instance you have configured
# on MotorClient, Database, or Collection. We will not be calling
# encrypt() or decrypt() in this example so we can use any
# CodecOptions.
CodecOptions(),
)
# Create a new data key and json schema for the encryptedField.
data_key_id = await client_encryption.create_data_key(
"local", key_alt_names=["pymongo_encryption_example_2"]
)
json_schema = {
"properties": {
"encryptedField": {
"encrypt": {
"keyId": [data_key_id],
"bsonType": "string",
"algorithm": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
}
}
},
"bsonType": "object",
}
auto_encryption_opts = AutoEncryptionOpts(kms_providers, key_vault_namespace)
client = AsyncIOMotorClient(auto_encryption_opts=auto_encryption_opts)
db_name, coll_name = encrypted_namespace.split(".", 1)
db = client[db_name]
# Clear old data
await db.drop_collection(coll_name)
# Create the collection with the encryption JSON Schema.
await db.create_collection(
coll_name,
# uuid_representation=STANDARD is required to ensure that any
# UUIDs in the $jsonSchema document are encoded to BSON Binary
# with the standard UUID subtype 4. This is only needed when
# running the "create" collection command with an encryption
# JSON Schema.
codec_options=CodecOptions(uuid_representation=STANDARD),
write_concern=WriteConcern(w="majority"),
validator={"$jsonSchema": json_schema},
)
coll = client[db_name][coll_name]
await coll.insert_one({"encryptedField": "123456789"})
decrypted_doc = await coll.find_one()
print(f"Decrypted document: {decrypted_doc}")
unencrypted_coll = AsyncIOMotorClient()[db_name][coll_name]
encrypted_doc = await unencrypted_coll.find_one()
print(f"Encrypted document: {encrypted_doc}")
try:
await unencrypted_coll.insert_one({"encryptedField": "123456789"})
except OperationFailure as exc:
print(f"Unencrypted insert failed: {exc.details}")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -3,53 +3,27 @@
Motor Tailable Cursor Example
=============================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
This example describes using Motor with Tornado. Beginning in
version 0.5 Motor can also integrate with asyncio instead of Tornado.
By default, MongoDB will automatically close a cursor when the client has
exhausted all results in the cursor. However, for capped collections you may
use a tailable cursor that remains open after the client exhausts the results
in the initial cursor.
The following is a basic example of using a tailable cursor to tail the oplog
of a replica set member:
A cursor on a capped collection can be tailed using :meth:`~MotorCursor.fetch_next`:
.. code-block:: python
from asyncio import sleep
from pymongo.cursor import CursorType
async def tail_oplog_example():
oplog = client.local.oplog.rs
first = await oplog.find().sort("$natural", pymongo.ASCENDING).limit(-1).next()
print(first)
ts = first["ts"]
@gen.coroutine
def tail_example():
results = []
collection = db.my_capped_collection
cursor = collection.find(cursor_type=CursorType.TAILABLE, await_data=True)
while True:
# For a regular capped collection CursorType.TAILABLE_AWAIT is the
# only option required to create a tailable cursor. When querying the
# oplog, the oplog_replay option enables an optimization to quickly
# find the 'ts' value we're looking for. The oplog_replay option
# can only be used when querying the oplog. Starting in MongoDB 4.4
# this option is ignored by the server as queries against the oplog
# are optimized automatically by the MongoDB query engine.
cursor = oplog.find(
{"ts": {"$gt": ts}},
cursor_type=CursorType.TAILABLE_AWAIT,
oplog_replay=True,
)
while cursor.alive:
async for doc in cursor:
ts = doc["ts"]
print(doc)
# We end up here if the find() returned no documents or if the
# tailable cursor timed out (no new documents were added to the
# collection for more than 1 second).
await sleep(1)
if not cursor.alive:
now = datetime.datetime.utcnow()
# While collection is empty, tailable cursor dies immediately
yield gen.Task(loop.add_timeout, datetime.timedelta(seconds=1))
cursor = collection.find(cursor_type=CursorType.TAILABLE, await_data=True)
if (yield cursor.fetch_next):
results.append(cursor.next_object())
print results
.. seealso:: `Tailable cursors <http://docs.mongodb.org/manual/tutorial/create-tailable-cursor/>`_

View File

@ -1,87 +0,0 @@
.. _timeout-example:
Client Side Operation Timeout
=============================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
PyMongo 4.2 introduced :meth:`~pymongo.timeout` and the ``timeoutMS``
URI and keyword argument to :class:`~pymongo.mongo_client.MongoClient`.
These features allow applications to more easily limit the amount of time that
one or more operations can execute before control is returned to the app. This
timeout applies to all of the work done to execute the operation, including
but not limited to server selection, connection checkout, serialization, and
server-side execution.
:meth:`~pymongo.timeout` is asyncio safe; the timeout only applies to current
Task and multiple Tasks can configure different timeouts concurrently.
:meth:`~pymongo.timeout` can be used identically in Motor 3.1+.
For more information and troubleshooting, see the PyMongo docs on
`Client Side Operation Timeout`_.
.. _Client Side Operation Timeout: https://pymongo.readthedocs.io/en/stable/examples/timeouts.html
Basic Usage
-----------
The following example uses :meth:`~pymongo.timeout` to configure a 10-second
timeout for an :meth:`~pymongo.collection.Collection.insert_one` operation::
import pymongo
import motor.motor_asyncio
client = motor.motor_asyncio.AsyncIOMotorClient()
coll = client.test.test
with pymongo.timeout(10):
await coll.insert_one({"name": "Nunu"})
The :meth:`~pymongo.timeout` applies to all pymongo operations within the block.
The following example ensures that both the ``insert`` and the ``find`` complete
within 10 seconds total, or raise a timeout error::
with pymongo.timeout(10):
await coll.insert_one({"name": "Nunu"})
await coll.find_one({"name": "Nunu"})
When nesting :func:`~pymongo.timeout`, the nested deadline is capped by the outer
deadline. The deadline can only be shortened, not extended.
When exiting the block, the previous deadline is restored::
with pymongo.timeout(5):
await coll.find_one() # Uses the 5 second deadline.
with pymongo.timeout(3):
await coll.find_one() # Uses the 3 second deadline.
await coll.find_one() # Uses the original 5 second deadline.
with pymongo.timeout(10):
await coll.find_one() # Still uses the original 5 second deadline.
await coll.find_one() # Uses the original 5 second deadline.
Timeout errors
--------------
When the :meth:`~pymongo.timeout` with-statement is entered, a deadline is set
for the entire block. When that deadline is exceeded, any blocking pymongo operation
will raise a timeout exception. For example::
try:
with pymongo.timeout(10):
await coll.insert_one({"name": "Nunu"})
await asyncio.sleep(10)
# The deadline has now expired, the next operation will raise
# a timeout exception.
await coll.find_one({"name": "Nunu"})
except PyMongoError as exc:
if exc.timeout:
print(f"block timed out: {exc!r}")
else:
print(f"failed with non-timeout error: {exc!r}")
The :attr:`pymongo.errors.PyMongoError.timeout` property (added in PyMongo 4.2)
will be ``True`` when the error was caused by a timeout and ``False`` otherwise.

View File

@ -1,117 +0,0 @@
import logging
import os
import sys
from base64 import urlsafe_b64encode
from pprint import pformat
import tornado.escape
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.websocket
from bson import json_util # Installed with PyMongo.
from tornado.options import define, options
from motor.motor_tornado import MotorClient
define("port", default=8888, help="run on the given port", type=int)
define("debug", default=False, help="reload on source changes")
define("mongo", default="mongodb://localhost", help="MongoDB URI")
define("ns", default="test.test", help="database and collection name")
class Application(tornado.web.Application):
def __init__(self):
handlers = [(r"/", MainHandler), (r"/socket", ChangesHandler)]
templates = os.path.join(os.path.dirname(__file__), "tornado_change_stream_templates")
super().__init__(
handlers, template_path=templates, template_whitespace="all", debug=options.debug
)
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html", changes=ChangesHandler.cache)
class ChangesHandler(tornado.websocket.WebSocketHandler):
waiters = set()
cache = []
cache_size = 5
def open(self):
ChangesHandler.waiters.add(self)
def on_close(self):
ChangesHandler.waiters.remove(self)
@classmethod
def update_cache(cls, change):
cls.cache.append(change)
if len(cls.cache) > cls.cache_size:
cls.cache = cls.cache[-cls.cache_size :]
@classmethod
def send_change(cls, change):
change_json = json_util.dumps(change)
for waiter in cls.waiters:
try:
waiter.write_message(change_json)
except Exception as exc:
logging.exception(exc)
@classmethod
def on_change(cls, change):
logging.info("got change of type '%s'", change.get("operationType"))
# Each change notification has a binary _id. Use it to make an HTML
# element id, then remove it.
data = change["_id"]["_data"]
if not isinstance(data, bytes):
data = data.encode("utf-8")
html_id = urlsafe_b64encode(data).decode().rstrip("=")
change.pop("_id")
change_pre = tornado.escape.xhtml_escape(pformat(change))
change["html"] = f'<div id="change-{html_id}"><pre>{change_pre}</pre></div>'
change["html_id"] = html_id
ChangesHandler.send_change(change)
ChangesHandler.update_cache(change)
change_stream = None
async def watch(collection):
global change_stream
async with collection.watch() as change_stream:
async for change in change_stream:
ChangesHandler.on_change(change)
def main():
tornado.options.parse_command_line()
if "." not in options.ns:
sys.stderr.write(f'Invalid ns "{options.ns}", must contain a "."')
sys.exit(1)
db_name, collection_name = options.ns.split(".", 1)
client = MotorClient(options.mongo)
collection = client[db_name][collection_name]
app = Application()
app.listen(options.port)
loop = tornado.ioloop.IOLoop.current()
# Start watching collection for changes.
try:
loop.run_sync(lambda: watch(collection))
except KeyboardInterrupt:
if change_stream:
loop.run_sync(change_stream.close)
if __name__ == "__main__":
main()

View File

@ -1,61 +0,0 @@
.. _tornado_change_stream_example:
Tornado Change Stream Example
=============================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
Watch a collection for changes with :meth:`MotorCollection.watch` and display
each change notification on a web page using web sockets.
Instructions
------------
Start a MongoDB server on its default port and run this script. Then visit:
http://localhost:8888
Open a ``mongo`` shell in the terminal and perform some operations on the
"test" collection in the "test" database:
.. code-block:: text
> use test
switched to db test
> db.test.insertOne({})
> db.test.updateOne({}, {$set: {x: 1}})
> db.test.deleteOne({})
The application receives each change notification and displays it as JSON on
the web page:
.. code-block:: text
Changes
{'documentKey': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')},
'fullDocument': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')},
'ns': {'coll': 'test', 'db': 'test'},
'operationType': 'insert'}
{'documentKey': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')},
'ns': {'coll': 'test', 'db': 'test'},
'operationType': 'update',
'updateDescription': {'removedFields': [], 'updatedFields': {'x': 1.0}}}
{'documentKey': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')},
'ns': {'coll': 'test', 'db': 'test'},
'operationType': 'delete'}
Display change notifications over a web socket
----------------------------------------------
.. literalinclude:: tornado_change_stream_example.py
:language: python3

View File

@ -1,51 +0,0 @@
<html>
<head>
<style type="text/css">
#changes div {
border-bottom: 1px solid gray;
margin-bottom: 1em;
}
</style>
</head>
<body>
<div id="body">
<div id="changes">
<h4>Changes</h4>
{% for change in changes %}
{% raw change['html'] %}
{% end %}
</div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
updater.start();
});
var updater = {
socket: null,
start: function() {
var url = "ws://" + location.host + "/socket";
updater.socket = new WebSocket(url);
updater.socket.onmessage = function(event) {
updater.showMessage(JSON.parse(event.data));
}
},
showMessage: function(change) {
var container = $("#changes");
var existing = $("#change-" + change.id);
if (existing.length > 0) return;
while (container.find('div').length >= 5) {
container.find('div:first').remove();
}
var node = $(change.html);
container.append(node);
}
};
</script>
</body>
</html>

View File

@ -1,393 +0,0 @@
.. _type_hints-example:
Type Hints
==========
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
As of version 3.3.0, Motor ships with `type hints`_. With type hints, Python
type checkers can easily find bugs before they reveal themselves in your code.
If your IDE is configured to use type hints,
it can suggest more appropriate completions and highlight errors in your code.
Some examples include `PyCharm`_, `Sublime Text`_, and `Visual Studio Code`_.
You can also use the `mypy`_ tool from your command line or in Continuous Integration tests.
All of the public APIs in Motor are fully type hinted, and
several of them support generic parameters for the
type of document object returned when decoding BSON documents.
Due to `limitations in mypy`_, the default
values for generic document types are not yet provided (they will eventually be ``Dict[str, any]``).
For a larger set of examples that use types, see the Motor `test_typing module`_.
If you would like to opt out of using the provided types, add the following to
your `mypy config`_: ::
[mypy-motor]
follow_imports = False
Basic Usage
-----------
Note that a type for :class:`~motor.motor_asyncio.AsyncIOMotorClient` must be specified. Here we use the
default, unspecified document type:
.. code-block:: python
from motor.motor_asyncio import AsyncIOMotorClient
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
collection = client.test.test
inserted = await collection.insert_one({"x": 1, "tags": ["dog", "cat"]})
retrieved = await collection.find_one({"x": 1})
assert isinstance(retrieved, dict)
For a more accurate typing for document type you can use:
.. code-block:: python
from typing import Any, Dict
from motor.motor_asyncio import AsyncIOMotorClient
async def main():
client: AsyncIOMotorClient[Dict[str, Any]] = AsyncIOMotorClient()
collection = client.test.test
inserted = await collection.insert_one({"x": 1, "tags": ["dog", "cat"]})
retrieved = await collection.find_one({"x": 1})
assert isinstance(retrieved, dict)
Typed Client
------------
:class:`~motor.motor_asyncio.AsyncIOMotorClient` is generic on the document type used to decode BSON documents.
You can specify a :class:`~pymongo.raw_bson.RawBSONDocument` document type:
.. code-block:: python
from motor.motor_asyncio import AsyncIOMotorClient
from bson.raw_bson import RawBSONDocument
async def main():
client = AsyncIOMotorClient(document_class=RawBSONDocument)
collection = client.test.test
inserted = await collection.insert_one({"x": 1, "tags": ["dog", "cat"]})
result = await collection.find_one({"x": 1})
assert isinstance(result, RawBSONDocument)
Subclasses of :py:class:`collections.abc.Mapping` can also be used, such as :class:`~pymongo.son.SON`:
.. code-block:: python
from bson import SON
from motor.motor_asyncio import AsyncIOMotorClient
async def main():
client = AsyncIOMotorClient(document_class=SON[str, int])
collection = client.test.test
inserted = await collection.insert_one({"x": 1, "y": 2})
result = await collection.find_one({"x": 1})
assert result is not None
assert result["x"] == 1
Note that when using :class:`~pymongo.son.SON`, the key and value types must be given, e.g. ``SON[str, Any]``.
Typed Collection
----------------
You can use :py:class:`~typing.TypedDict` when using a well-defined schema for the data in a
:class:`~motor.motor_asyncio.AsyncIOMotorClient`. Note that all `schema validation`_ for inserts and updates is done on the server.
These methods automatically add an "_id" field.
.. code-block:: python
from typing import TypedDict
from motor.motor_asyncio import AsyncIOMotorClient
from motor.motor_asyncio import AsyncIOMotorCollection
class Movie(TypedDict):
name: str
year: int
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
collection: AsyncIOMotorCollection[Movie] = client.test.test
inserted = await collection.insert_one(Movie(name="Jurassic Park", year=1993))
result = await collection.find_one({"name": "Jurassic Park"})
assert result is not None
assert result["year"] == 1993
# This will raise a type-checking error, despite being present, because it is added by Motor.
assert result["_id"] # type:ignore[typeddict-item]
This same typing scheme works for all of the insert methods (:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.insert_one`,
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.insert_many`, and :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.bulk_write`).
For ``bulk_write`` both :class:`~pymongo.operations.InsertOne` and :class:`~pymongo.operations.ReplaceOne` operators are generic.
.. code-block:: python
from typing import TypedDict
from motor.motor_asyncio import AsyncIOMotorClient
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo.operations import InsertOne
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
collection: AsyncIOMotorCollection[Movie] = client.test.test
inserted = await collection.bulk_write(
[InsertOne(Movie(name="Jurassic Park", year=1993))]
)
result = await collection.find_one({"name": "Jurassic Park"})
assert result is not None
assert result["year"] == 1993
# This will raise a type-checking error, despite being present, because it is added by Motor.
assert result["_id"] # type:ignore[typeddict-item]
Modeling Document Types with TypedDict
--------------------------------------
You can use :py:class:`~typing.TypedDict` to model structured data.
As noted above, Motor will automatically add an ``_id`` field if it is not present. This also applies to TypedDict.
There are three approaches to this:
1. Do not specify ``_id`` at all. It will be inserted automatically, and can be retrieved at run-time, but will yield a type-checking error unless explicitly ignored.
2. Specify ``_id`` explicitly. This will mean that every instance of your custom TypedDict class will have to pass a value for ``_id``.
3. Make use of :py:class:`~typing.NotRequired`. This has the flexibility of option 1, but with the ability to access the ``_id`` field without causing a type-checking error.
Note: to use :py:class:`~typing.NotRequired` in earlier versions of Python (<3.11), use the ``typing_extensions`` package.
.. code-block:: python
from typing import TypedDict, NotRequired
from motor.motor_asyncio import AsyncIOMotorClient
from motor.motor_asyncio import AsyncIOMotorCollection
from bson import ObjectId
class Movie(TypedDict):
name: str
year: int
class ExplicitMovie(TypedDict):
_id: ObjectId
name: str
year: int
class NotRequiredMovie(TypedDict):
_id: NotRequired[ObjectId]
name: str
year: int
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
collection: AsyncIOMotorCollection[Movie] = client.test.test
inserted = await collection.insert_one(Movie(name="Jurassic Park", year=1993))
result = await collection.find_one({"name": "Jurassic Park"})
assert result is not None
# This will yield a type-checking error, despite being present, because it is added by Motor.
assert result["_id"] # type:ignore[typeddict-item]
collection: AsyncIOMotorCollection[ExplicitMovie] = client.test.test
# Note that the _id keyword argument must be supplied
inserted = await collection.insert_one(
ExplicitMovie(_id=ObjectId(), name="Jurassic Park", year=1993)
)
result = await collection.find_one({"name": "Jurassic Park"})
assert result is not None
# This will not raise a type-checking error.
assert result["_id"]
collection: AsyncIOMotorCollection[NotRequiredMovie] = client.test.test
# Note the lack of _id, similar to the first example
inserted = await collection.insert_one(
NotRequiredMovie(name="Jurassic Park", year=1993)
)
result = await collection.find_one({"name": "Jurassic Park"})
assert result is not None
# This will not raise a type-checking error, despite not being provided explicitly.
assert result["_id"]
Typed Database
--------------
While less common, you could specify that the documents in an entire database
match a well-defined schema using :py:class:`~typing.TypedDict`.
.. code-block:: python
from typing import TypedDict
from motor.motor_asyncio import AsyncIOMotorClient
from motor.motor_asyncio import AsyncIOMotorDatabase
class Movie(TypedDict):
name: str
year: int
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
db: AsyncIOMotorDatabase[Movie] = client.test
collection = db.test
inserted = await collection.insert_one({"name": "Jurassic Park", "year": 1993})
result = await collection.find_one({"name": "Jurassic Park"})
assert result is not None
assert result["year"] == 1993
Typed Command
-------------
When using the :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command`, you can specify the document type by providing a custom :class:`~pymongo.codec_options.CodecOptions`:
.. code-block:: python
from motor.motor_asyncio import AsyncIOMotorClient
from bson.raw_bson import RawBSONDocument
from bson import CodecOptions
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
options = CodecOptions(RawBSONDocument)
result = await client.admin.command("ping", codec_options=options)
assert isinstance(result, RawBSONDocument)
Custom :py:class:`collections.abc.Mapping` subclasses and :py:class:`~typing.TypedDict` are also supported.
For :py:class:`~typing.TypedDict`, use the form: ``options: CodecOptions[MyTypedDict] = CodecOptions(...)``.
Typed BSON Decoding
-------------------
You can specify the document type returned by :mod:`bson` decoding functions by providing :class:`~pymongo.codec_options.CodecOptions`:
.. code-block:: python
from typing import Any, Dict
from bson import CodecOptions, encode, decode
class MyDict(Dict[str, Any]):
pass
def foo(self):
return "bar"
options = CodecOptions(document_class=MyDict)
doc = {"x": 1, "y": 2}
bsonbytes = encode(doc, codec_options=options)
rt_document = decode(bsonbytes, codec_options=options)
assert rt_document.foo() == "bar"
:class:`~pymongo.raw_bson.RawBSONDocument` and :py:class:`~typing.TypedDict` are also supported.
For :py:class:`~typing.TypedDict`, use the form: ``options: CodecOptions[MyTypedDict] = CodecOptions(...)``.
Troubleshooting
---------------
Client Type Annotation
~~~~~~~~~~~~~~~~~~~~~~
If you forget to add a type annotation for a :class:`~motor.motor_asyncio.AsyncIOMotorClient` object you may get the following ``mypy`` error:
.. code-block:: python
from motor.motor_asyncio import AsyncIOMotorClient
client = AsyncIOMotorClient() # error: Need type annotation for "client"
The solution is to annotate the type as ``client: AsyncIOMotorClient`` or ``client: AsyncIOMotorClient[Dict[str, Any]]``. See `Basic Usage`_.
Incompatible Types
~~~~~~~~~~~~~~~~~~
If you use the generic form of :class:`~motor.motor_asyncio.AsyncIOMotorClient` you
may encounter a ``mypy`` error like:
.. code-block:: python
from motor.motor_asyncio import AsyncIOMotorClient
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
await client.test.test.insert_many(
{"a": 1}
) # error: Dict entry 0 has incompatible type "str": "int";
# expected "Mapping[str, Any]": "int"
The solution is to use ``client: AsyncIOMotorClient[Dict[str, Any]]`` as used in
`Basic Usage`_ .
Actual Type Errors
~~~~~~~~~~~~~~~~~~
Other times ``mypy`` will catch an actual error, like the following code:
.. code-block:: python
from motor.motor_asyncio import AsyncIOMotorClient
from typing import Mapping
async def main():
client: AsyncIOMotorClient = AsyncIOMotorClient()
await client.test.test.insert_one(
[{}]
) # error: Argument 1 to "insert_one" of "Collection" has
# incompatible type "List[Dict[<nothing>, <nothing>]]";
# expected "Mapping[str, Any]"
In this case the solution is to use ``insert_one({})``, passing a document instead of a list.
Another example is trying to set a value on a :class:`~pymongo.raw_bson.RawBSONDocument`, which is read-only.:
.. code-block:: python
from bson.raw_bson import RawBSONDocument
from motor.motor_asyncio import AsyncIOMotorClient
async def main():
client = AsyncIOMotorClient(document_class=RawBSONDocument)
coll = client.test.test
doc = {"my": "doc"}
await coll.insert_one(doc)
retrieved = await coll.find_one({"_id": doc["_id"]})
assert retrieved is not None
assert len(retrieved.raw) > 0
retrieved["foo"] = "bar" # error: Unsupported target for indexed assignment
# ("RawBSONDocument") [index]
.. _PyCharm: https://www.jetbrains.com/help/pycharm/type-hinting-in-product.html
.. _Visual Studio Code: https://code.visualstudio.com/docs/languages/python
.. _Sublime Text: https://github.com/sublimelsp/LSP-pyright
.. _type hints: https://docs.python.org/3/library/typing.html
.. _mypy: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
.. _limitations in mypy: https://github.com/python/mypy/issues/3737
.. _mypy config: https://mypy.readthedocs.io/en/stable/config_file.html
.. _test_typing module: https://github.com/mongodb/motor/blob/master/test/test_typing.py
.. _schema validation: https://www.mongodb.com/docs/manual/core/schema-validation/#when-to-use-schema-validation

View File

@ -4,13 +4,6 @@ Motor Features
.. currentmodule:: motor.motor_tornado
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
Non-Blocking
============
Motor is an asynchronous driver for MongoDB. It can be used from Tornado_ or
@ -27,10 +20,11 @@ Featureful
Motor wraps almost all of PyMongo's API and makes it non-blocking. For the few
PyMongo features not implemented in Motor, see :doc:`differences`.
Convenient With ``tornado.gen``
===============================
Convenient With `tornado.gen`
=============================
The :mod:`tornado.gen` module lets you use coroutines to simplify asynchronous
code. Motor methods return Futures that are convenient to use with coroutines.
See :ref:`the coroutine example <coroutine-example>`.
Configurable IOLoops
====================

View File

@ -4,16 +4,10 @@ Motor: Asynchronous Python driver for MongoDB
.. image:: _static/motor.png
:align: center
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
About
-----
Motor presents a coroutine-based API for non-blocking access to
Motor presents a callback- or Future-based API for non-blocking access to
MongoDB from Tornado_ or asyncio_.
The `source is on GitHub <https://github.com/mongodb/motor>`_ and
@ -42,38 +36,16 @@ Install with::
.. _asyncio: https://docs.python.org/3/library/asyncio.html
Getting Help
------------
If you're having trouble or have questions about Motor, ask your question on
our `MongoDB Community Forum <https://developer.mongodb.com/community/forums/tags/c/drivers-odms-connectors/7/motor-driver>`_.
You may also want to consider a
`commercial support subscription <https://support.mongodb.com/welcome>`_.
Once you get an answer, it'd be great if you could work it back into this
documentation and contribute!
.. _PyMongo: http://pypi.python.org/pypi/pymongo/
Issues
------
All issues should be reported (and can be tracked / voted for /
commented on) at the main `MongoDB JIRA bug tracker
<http://jira.mongodb.org/browse/MOTOR>`_, in the "Motor"
project.
How To Ask For Help
-------------------
Feature Requests / Feedback
---------------------------
Use our `feedback engine <https://feedback.mongodb.com/?category=7548141816650747033>`_
to send us feature requests and general feedback about PyMongo.
Contributing
------------
**Motor** has a large :doc:`community <contributors>` and
contributions are always encouraged. Contributions can be as simple as
minor tweaks to this documentation. To contribute, fork the project on
`GitHub <http://github.com/mongodb/motor/>`_ and send a
pull request.
Changes
-------
See the :doc:`changelog` for a full list of changes to Motor.
Post questions about Motor to the
`mongodb-user list on Google Groups
<https://groups.google.com/forum/?fromgroups#!forum/mongodb-user>`_.
For confirmed issues or feature requests, open a case in
`Jira <http://jira.mongodb.org>`_ in the "MOTOR" project.
Contents
--------
@ -90,8 +62,7 @@ Contents
tutorial-asyncio
examples/index
changelog
migrate-to-motor-2
migrate-to-motor-3
migrate-to-motor-1
developer-guide
contributors

View File

@ -1,84 +1,13 @@
Installation
============
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
Install Motor from PyPI_ with pip_::
$ python3 -m pip install motor
$ python -m pip install motor
Pip automatically installs Motor's prerequisite packages.
See :doc:`requirements`.
To install Motor from sources, you can clone its git repository and do::
$ python3 -m pip install .
Dependencies
------------
Motor works in all the environments officially supported by Tornado or by
asyncio. It requires:
* Unix (including macOS) or Windows.
* PyMongo_ >=4.9,<5
* Python 3.10+
Optional dependencies:
Motor supports same optional dependencies as PyMongo. Required dependencies can be installed
along with Motor.
GSSAPI authentication requires ``gssapi`` extra dependency. The correct
dependency can be installed automatically along with Motor::
$ pip install "motor[gssapi]"
similarly,
`MONGODB-AWS <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/aws-iam/#std-label-pymongo-mongodb-aws>`_
authentication requires ``aws`` extra dependency::
$ pip install "motor[aws]"
Support for mongodb+srv:// URIs requires ``srv`` extra dependency::
$ pip install "motor[srv]"
`OCSP <https://pymongo.readthedocs.io/en/stable/examples/tls.html#ocsp>`_ requires ``ocsp`` extra dependency::
$ pip install "motor[ocsp]"
Wire protocol compression with snappy requires ``snappy`` extra dependency::
$ pip install "motor[snappy]"
Wire protocol compression with zstandard requires ``zstd`` extra dependency::
$ pip install "motor[zstd]"
`Client-Side Field Level Encryption
<https://pymongo.readthedocs.io/en/stable/examples/encryption.html#client-side-field-level-encryption>`_
requires ``encryption`` extra dependency::
$ pip install "motor[encryption]"
You can install all dependencies automatically with the following
command::
$ pip install "motor[gssapi,aws,ocsp,snappy,srv,zstd,encryption]"
See `requirements <https://motor.readthedocs.io/en/stable/requirements.html>`_
for details about compatibility.
.. _PyPI: http://pypi.python.org/pypi/motor
.. _pip: http://pip-installer.org
.. _PyMongo: http://pypi.python.org/pypi/pymongo/

400
doc/migrate-to-motor-1.rst Normal file
View File

@ -0,0 +1,400 @@
Motor 1.0 Migration Guide
=========================
.. currentmodule:: motor.motor_tornado
Motor 1.0 brings a number of backward breaking changes to the pre-1.0 API.
Follow this guide to migrate an existing application that had used an older
version of Motor.
Removed features with no migration path
---------------------------------------
:class:`MotorReplicaSetClient` is removed
..........................................
In Motor 1.0, :class:`MotorClient` is the only class. Connect to a replica set with
a "replicaSet" URI option or parameter::
MotorClient("mongodb://localhost/?replicaSet=my-rs")
MotorClient(host, port, replicaSet="my-rs")
The "compile_re" option is removed
..................................
In Motor 1.0 regular expressions are never compiled to Python `re.match`
objects.
Motor 0.7
---------
The first step in migrating to Motor 1.0 is to upgrade to at least Motor 0.7.
If your project has a
requirements.txt file, add the line::
motor >= 0.7, < 1.0
Most of the key new
methods and options from Motor 1.0 are backported in Motor 0.7 making
migration much easier.
Enable Deprecation Warnings
---------------------------
Starting with Motor 0.7, :exc:`DeprecationWarning` is raised by most methods
removed in Motor 1.0. Make sure you enable runtime warnings to see
where deprecated functions and methods are being used in your application::
python -Wd <your application>
Warnings can also be changed to errors::
python -Wd -Werror <your application>
Not all deprecated features raise `DeprecationWarning` when
used. For example, `~motor.motor_tornado.MotorReplicaSetClient` will be
removed in Motor 1.0 but it does not raise `DeprecationWarning`
in Motor 0.7. See also `Removed features with no migration path`_.
CRUD API
--------
Changes to find() and find_one()
................................
"spec" renamed "filter"
~~~~~~~~~~~~~~~~~~~~~~~
The ``spec`` option has been renamed to ``filter``. Code like this::
cursor = collection.find(spec={"a": 1})
can be changed to this with Motor 0.7 or later::
cursor = collection.find(filter={"a": 1})
or this with any version of Motor::
cursor = collection.find({"a": 1})
"fields" renamed "projection"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``fields`` option has been renamed to ``projection``. Code like this::
cursor = collection.find({"a": 1}, fields={"_id": False})
can be changed to this with Motor 0.7 or later::
cursor = collection.find({"a": 1}, projection={"_id": False})
or this with any version of Motor::
cursor = collection.find({"a": 1}, {"_id": False})
"partial" renamed "allow_partial_results"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``partial`` option has been renamed to ``allow_partial_results``. Code like
this::
cursor = collection.find({"a": 1}, partial=True)
can be changed to this with Motor 0.7 or later::
cursor = collection.find({"a": 1}, allow_partial_results=True)
"timeout" replaced by "no_cursor_timeout"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``timeout`` option has been replaced by ``no_cursor_timeout``. Code like this::
cursor = collection.find({"a": 1}, timeout=False)
can be changed to this with Motor 0.7 or later::
cursor = collection.find({"a": 1}, no_cursor_timeout=True)
"snapshot" and "max_scan" replaced by "modifiers"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``snapshot`` and ``max_scan`` options have been removed. They can now be set,
along with other $ query modifiers, through the ``modifiers`` option. Code like
this::
cursor = collection.find({"a": 1}, snapshot=True)
can be changed to this with Motor 0.7 or later::
cursor = collection.find({"a": 1}, modifiers={"$snapshot": True})
or with any version of Motor::
cursor = collection.find({"$query": {"a": 1}, "$snapshot": True})
"network_timeout" is removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``network_timeout`` option has been removed. This option was always the
wrong solution for timing out long running queries and should never be used
in production. Starting with **MongoDB 2.6** you can use the $maxTimeMS query
modifier. Code like this::
# Set a 5 second select() timeout.
cursor = collection.find({"a": 1}, network_timeout=5)
can be changed to this with Motor 0.7 or later::
# Set a 5 second (5000 millisecond) server side query timeout.
cursor = collection.find({"a": 1}, modifiers={"$maxTimeMS": 5000})
or with any version of Motor::
cursor = collection.find({"$query": {"a": 1}, "$maxTimeMS": 5000})
.. seealso:: `$maxTimeMS
<http://docs.mongodb.org/manual/reference/operator/meta/maxTimeMS/>`_
Tailable cursors
~~~~~~~~~~~~~~~~
The ``tailable`` and ``await_data`` options have been replaced by ``cursor_type``.
Code like this::
cursor = collection.find({"a": 1}, tailable=True)
cursor = collection.find({"a": 1}, tailable=True, await_data=True)
can be changed to this with Motor 0.7 or later::
from pymongo import CursorType
cursor = collection.find({"a": 1}, cursor_type=CursorType.TAILABLE)
cursor = collection.find({"a": 1}, cursor_type=CursorType.TAILABLE_AWAIT)
Other removed options
~~~~~~~~~~~~~~~~~~~~~
The ``read_preference``, ``tag_sets``,
and ``secondary_acceptable_latency_ms`` options have been removed. See the `Read
Preferences`_ section for solutions.
Read Preferences
----------------
The "read_preference" attribute is immutable
............................................
Code like this::
from pymongo import ReadPreference
db = client.my_database
db.read_preference = ReadPreference.SECONDARY
can be changed to this with Motor 0.7 or later::
db = client.get_database("my_database",
read_preference=ReadPreference.SECONDARY)
Code like this::
cursor = collection.find({"a": 1},
read_preference=ReadPreference.SECONDARY)
can be changed to this with Motor 0.7 or later::
coll2 = collection.with_options(read_preference=ReadPreference.SECONDARY)
cursor = coll2.find({"a": 1})
.. seealso:: :meth:`~MotorDatabase.get_collection`
The "tag_sets" option and attribute are removed
...............................................
The ``tag_sets`` MotorClient option is removed. The ``read_preference``
option can be used instead. Code like this::
client = MotorClient(
read_preference=ReadPreference.SECONDARY,
tag_sets=[{"dc": "ny"}, {"dc": "sf"}])
can be changed to this with Motor 0.7 or later::
from pymongo.read_preferences import Secondary
client = MotorClient(read_preference=Secondary([{"dc": "ny"}]))
To change the tags sets for a MotorDatabase or MotorCollection, code like this::
db = client.my_database
db.read_preference = ReadPreference.SECONDARY
db.tag_sets = [{"dc": "ny"}]
can be changed to this with Motor 0.7 or later::
db = client.get_database("my_database",
read_preference=Secondary([{"dc": "ny"}]))
Code like this::
cursor = collection.find(
{"a": 1},
read_preference=ReadPreference.SECONDARY,
tag_sets=[{"dc": "ny"}])
can be changed to this with Motor 0.7 or later::
from pymongo.read_preferences import Secondary
coll2 = collection.with_options(
read_preference=Secondary([{"dc": "ny"}]))
cursor = coll2.find({"a": 1})
.. seealso:: :meth:`~MotorDatabase.get_collection`
The "secondary_acceptable_latency_ms" option and attribute are removed
......................................................................
Motor 0.x supports ``secondary_acceptable_latency_ms`` as an option to methods
throughout the driver, but mongos only supports a global latency option.
Motor 1.0 has changed to match the behavior of mongos, allowing migration
from a single server, to a replica set, to a sharded cluster without a
surprising change in server selection behavior. A new option,
``localThresholdMS``, is available through MotorClient and should be used in
place of ``secondaryAcceptableLatencyMS``. Code like this::
client = MotorClient(readPreference="nearest",
secondaryAcceptableLatencyMS=100)
can be changed to this with Motor 0.7 or later::
client = MotorClient(readPreference="nearest",
localThresholdMS=100)
Write Concern
-------------
The "write_concern" attribute is immutable
..........................................
The ``write_concern`` attribute is immutable in Motor 1.0. Code like this::
client = MotorClient()
client.write_concern = {"w": "majority"}
can be changed to this with any version of Motor::
client = MotorClient(w="majority")
Code like this::
db = client.my_database
db.write_concern = {"w": "majority"}
can be changed to this with Motor 0.7 or later::
from pymongo import WriteConcern
db = client.get_database("my_database",
write_concern=WriteConcern(w="majority"))
The new CRUD API write methods do not accept write concern options. Code like
this::
oid = collection.insert({"a": 2}, w="majority")
can be changed to this with Motor 0.7 or later::
from pymongo import WriteConcern
coll2 = collection.with_options(
write_concern=WriteConcern(w="majority"))
oid = coll2.insert({"a": 2})
.. seealso:: :meth:`~MotorDatabase.get_collection`
Codec Options
-------------
The "document_class" attribute is removed
.........................................
Code like this::
from bson.son import SON
client = MotorClient()
client.document_class = SON
can be replaced by this in any version of Motor::
from bson.son import SON
client = MotorClient(document_class=SON)
or to change the ``document_class`` for a :class:`MotorDatabase`
with Motor 0.7 or later::
from bson.codec_options import CodecOptions
from bson.son import SON
db = client.get_database("my_database", CodecOptions(SON))
.. seealso:: :meth:`~MotorDatabase.get_collection` and
:meth:`~MotorCollection.with_options`
The "uuid_subtype" option and attribute are removed
...................................................
Code like this::
from bson.binary import JAVA_LEGACY
db = client.my_database
db.uuid_subtype = JAVA_LEGACY
can be replaced by this with Motor 0.7 or later::
from bson.binary import JAVA_LEGACY
from bson.codec_options import CodecOptions
db = client.get_database("my_database",
CodecOptions(uuid_representation=JAVA_LEGACY))
.. seealso:: :meth:`~MotorDatabase.get_collection` and
:meth:`~MotorCollection.with_options`
MotorClient
-----------
The ``open`` method
...................
The :meth:`~MotorClient.open` method is removed in Motor 1.0.
Motor clients have opened themselves on demand since Motor 0.2.
The max_pool_size parameter is removed
......................................
Motor 1.0 replaced the max_pool_size parameter with support for the MongoDB URI
``maxPoolSize`` option. Code like this::
client = MotorClient(max_pool_size=10)
can be replaced by this with Motor 0.7 or later::
client = MotorClient(maxPoolSize=10)
client = MotorClient("mongodb://localhost:27017/?maxPoolSize=10")
The "disconnect" method is removed
..................................
Code like this::
client.disconnect()
can be replaced by this::
client.close()
The host and port attributes are removed
........................................
Code like this::
host = client.host
port = client.port
can be replaced by this with Motor 0.7 or later::
address = client.address
host, port = address or (None, None)

View File

@ -1,215 +0,0 @@
Motor 2.0 Migration Guide
=========================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
Motor 2.0 brings a number of changes to Motor 1.0's API. The major version is
required in order to update the session API to support multi-document
transactions, introduced in MongoDB 4.0; this feature is so valuable that it
motivated me to make the breaking change and bump the version number to 2.0.
Since this is the first major version number in almost two years, it removes a
large number of APIs that have been deprecated in the time since Motor 1.0.
Follow this guide to migrate an existing application that had used Motor 1.x.
Check compatibility
-------------------
Read the :doc:`requirements` page and ensure your MongoDB server and Python
interpreter are compatible, and your Tornado version if you are using Tornado.
If you use aiohttp, upgrade to at least 3.0.
Upgrade to Motor 1.3
--------------------
The first step in migrating to Motor 2.0 is to upgrade to at least Motor 1.3.
If your project has a
requirements.txt file, add the line::
motor >= 1.3, < 2.0
Enable Deprecation Warnings
---------------------------
Starting with Motor 1.3, :exc:`DeprecationWarning` is raised by most methods
removed in Motor 2.0. Make sure you enable runtime warnings to see where
deprecated functions and methods are being used in your application::
python -Wd <your application>
Warnings can also be changed to errors::
python -Wd -Werror <your application>
Migrate from deprecated APIs
----------------------------
The following features are deprecated by PyMongo and scheduled for removal;
they are now deleted from Motor:
- ``MotorClient.kill_cursors`` and ``close_cursor``.
Allow :class:`MotorCursor` to handle its own cleanup.
- ``MotorClient.get_default_database``. Call :meth:`MotorClient.get_database`
with a database name of ``None`` for the same effect.
- ``MotorDatabase.add_son_manipulator``. Transform documents to and from
their MongoDB representations in your application code instead.
- The server command ``getLastError`` and related commands are deprecated,
their helper functions are deleted from Motor:
- ``MotorDatabase.last_status``
- ``MotorDatabase.error``
- ``MotorDatabase.previous_error``
- ``MotorDatabase.reset_error_history``
Use acknowledged writes and rely on Motor to raise exceptions.
- The server command ``parallelCollectionScan`` is deprecated and
``MotorCollection.parallel_scan`` is removed. Use a regular
:meth:`MotorCollection.find` cursor.
- ``MotorClient.database_names``. Use
:meth:`~MotorClient.list_database_names`.
- ``MotorDatabase.eval``. The server command is deprecated but
still available with ``MotorDatabase.command("eval", ...)``.
- ``MotorDatabase.group``. The server command is deprecated but
still available with ``MotorDatabase.command("group", ...)``.
- ``MotorDatabase.authenticate`` and ``MotorDatabase.logout``. Add credentials
to the URI or ``MotorClient`` options instead of calling ``authenticate``.
To authenticate as multiple users on the same database, instead of using
``authenticate`` and ``logout`` use a separate client for each user.
- ``MotorCollection.initialize_unordered_bulk_op``,
``initialize_unordered_bulk_op``, and ``MotorBulkOperationBuilder``. Use
:meth:`MotorCollection.bulk_write``, see :ref:`Bulk Writes Tutorial
<bulk-write-tutorial>`.
- ``MotorCollection.count``. Use :meth:`~MotorCollection.count_documents` or
:meth:`~MotorCollection.estimated_document_count`.
- ``MotorCollection.ensure_index``. Use
:meth:`MotorCollection.create_indexes`.
- Deprecated write methods have been deleted from :class:`MotorCollection`.
- ``save`` and ``insert``: Use :meth:`~MotorCollection.insert_one` or
:meth:`~MotorCollection.insert_many`.
- ``update``: Use :meth:`~MotorCollection.update_one`,
:meth:`~MotorCollection.update_many`, or
:meth:`~MotorCollection.replace_one`.
- ``remove``: Use :meth:`~MotorCollection.delete_one` or
:meth:`~MotorCollection.delete_many`.
- ``find_and_modify``: Use :meth:`~MotorCollection.find_one_and_update`,
:meth:`~MotorCollection.find_one_and_replace`, or
:meth:`~MotorCollection.find_one_and_delete`.
- ``MotorCursor.count`` and ``MotorGridOutCursor.count``. Use
:meth:`MotorCollection.count_documents` or
:meth:`MotorCollection.estimated_document_count`.
Migrate from the original callback API
--------------------------------------
Motor was first released before Tornado had introduced Futures, generator-based
coroutines, and the ``yield`` syntax, and long before the async features
developed during Python 3's career. Therefore Motor's original asynchronous API
used callbacks:
.. code-block:: python3
def callback(result, error):
if error:
print(error)
else:
print(result)
collection.find_one({}, callback=callback)
Callbacks have been largely superseded by a Futures API intended for use with
coroutines, see :doc:`tutorial-tornado`. You can still use callbacks with Motor when
appropriate but you must add the callback to a Future instead of passing it as
a parameter:
.. code-block:: python3
def callback(future):
try:
result = future.result()
print(result)
except Exception as exc:
print(exc)
future = collection.find_one({})
future.add_done_callback(callback)
The :meth:`~asyncio.Future.add_done_callback` call can be placed on the same
line:
.. code-block:: python3
collection.find_one({}).add_done_callback(callback)
In almost all cases the modern coroutine API is more readable and provides
better exception handling:
.. code-block:: python3
async def do_find():
try:
result = await collection.find_one({})
print(result)
except Exception as exc:
print(exc)
Upgrade to Motor 2.0
--------------------
Once your application runs without deprecation warnings with Motor 1.3, upgrade
to Motor 2.0. Update any calls in your code to
:meth:`MotorClient.start_session` or
:meth:`~pymongo.client_session.ClientSession.end_session` to handle the
following change.
:meth:`MotorClient.start_session` is a coroutine
------------------------------------------------
In the past, you could use a client session like:
.. code-block:: python3
session = client.start_session()
doc = await client.db.collection.find_one({}, session=session)
session.end_session()
Or:
.. code-block:: python3
with client.start_session() as session:
doc = client.db.collection.find_one({}, session=session)
To support multi-document transactions, in Motor 2.0
:meth:`MotorClient.start_session` is a coroutine, not a regular method. It must
be used like ``await client.start_session()`` or ``async with await
client.start_session()``. The coroutine now returns a new class
:class:`~motor.motor_tornado.MotorClientSession`, not PyMongo's
:class:`~pymongo.client_session.ClientSession`. The ``end_session`` method on
the returned :class:`~motor.motor_tornado.MotorClientSession` is also now a
coroutine instead of a regular method. Use it like:
.. code-block:: python3
session = await client.start_session()
doc = await client.db.collection.find_one({}, session=session)
await session.end_session()
Or:
.. code-block:: python3
async with client.start_session() as session:
doc = await client.db.collection.find_one({}, session=session)

View File

@ -1,469 +0,0 @@
Motor 3.0 Migration Guide
=========================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. currentmodule:: motor.motor_tornado
Motor 3.0 brings a number of changes to Motor 2.0's API. The major version is
required in order to bring support for PyMongo 4.0+.
To add compatibility with PyMongo 4, several methods were removed, as detailed below.
Some of the underlying behaviors and method arguments have changed in PyMongo
4.0 as well.
Follow this guide to migrate an existing application that had used Motor 2.x.
Check compatibility
-------------------
Read the :doc:`requirements` page and ensure your MongoDB server and Python
interpreter are compatible, and your Tornado version if you are using Tornado.
Upgrade to Motor 2.5
--------------------
The first step in migrating to Motor 3.0 is to upgrade to at least Motor 2.5.
If your project has a
``requirements.txt`` file, add the line::
motor >= 2.5, < 3.0
Python 3.7+
-----------
Motor 3.0 drops support for Python 3.5 and 3.6. Users who wish to upgrade to 3.x must first upgrade to Python 3.7+.
Enable Deprecation Warnings
---------------------------
A :exc:`DeprecationWarning` is raised by most changes made in PyMongo 4.0.
Make sure you enable runtime warnings to see where
deprecated functions and methods are being used in your application::
python -Wd <your application>
Warnings can also be changed to errors::
python -Wd -Werror <your application>
Note that there are some deprecation warnings raised by Motor itself for
APIs that are deprecated but not yet removed, like :meth:`~motor.motor_tornado.MotorCursor.fetch_next`.
MotorClient
-----------
``directConnection`` defaults to False
......................................
``directConnection`` URI option and keyword argument to :class:`~motor
.MotorClient` defaults to ``False`` instead of ``None``,
allowing for the automatic discovery of replica sets. This means that if you
want a direct connection to a single server you must pass
``directConnection=True`` as a URI option or keyword argument.
Renamed URI options
...................
Several deprecated URI options have been renamed to the standardized
option names defined in the
`URI options specification <https://github.com/mongodb/specifications/blob/master/source/uri-options/uri-options.rst>`_.
The old option names and their renamed equivalents are summarized in the table
below. Some renamed options have different semantics from the option being
replaced as noted in the 'Migration Notes' column.
+--------------------+-------------------------------+--------------------------------------------------------+
| Old URI Option | Renamed URI Option | Migration Notes |
+====================+===============================+========================================================+
| ssl_pem_passphrase | tlsCertificateKeyFilePassword | - |
+--------------------+-------------------------------+--------------------------------------------------------+
| ssl_ca_certs | tlsCAFile | - |
+--------------------+-------------------------------+--------------------------------------------------------+
| ssl_crlfile | tlsCRLFile | - |
+--------------------+-------------------------------+--------------------------------------------------------+
| ssl_match_hostname | tlsAllowInvalidHostnames | ``ssl_match_hostname=True`` is equivalent to |
| | | ``tlsAllowInvalidHostnames=False`` and vice-versa. |
+--------------------+-------------------------------+--------------------------------------------------------+
| ssl_cert_reqs | tlsAllowInvalidCertificates | Instead of ``ssl.CERT_NONE``, ``ssl.CERT_OPTIONAL`` |
| | | and ``ssl.CERT_REQUIRED``, the new option expects |
| | | a boolean value - ``True`` is equivalent to |
| | | ``ssl.CERT_NONE``, while ``False`` is equivalent to |
| | | ``ssl.CERT_REQUIRED``. |
+--------------------+-------------------------------+--------------------------------------------------------+
| ssl_certfile | tlsCertificateKeyFile | Instead of using ``ssl_certfile`` and ``ssl_keyfile`` |
| | | to specify the certificate and private key files |
+--------------------+ | respectively, use ``tlsCertificateKeyFile`` to pass |
| ssl_keyfile | | a single file containing both the client certificate |
| | | and the private key. |
+--------------------+-------------------------------+--------------------------------------------------------+
| j | journal | - |
+--------------------+-------------------------------+--------------------------------------------------------+
| wtimeout | wTimeoutMS | - |
+--------------------+-------------------------------+--------------------------------------------------------+
MotorClient.fsync is removed
............................
Removed :meth:`~motor.motor_tornado.MotorClient.fsync`. Run the
`fsync command`_ directly with :meth:`~motor.motor_tornado.MotorDatabase.command`
instead. For example::
await client.admin.command('fsync', lock=True)
.. _fsync command: https://mongodb.com/docs/manual/reference/command/fsync/
MotorClient.unlock is removed
.............................
Removed :meth:`~motor.motor_tornado.MotorClient.unlock`. Run the
`fsyncUnlock command`_ directly with
:meth:`~motor.motor_tornado.MotorDatabase.command` instead. For example::
await client.admin.command('fsyncUnlock')
.. _fsyncUnlock command: https://mongodb.com/docs/manual/reference/command/fsyncUnlock/
MotorClient.max_bson_size/max_message_size/max_write_batch_size are removed
...........................................................................
Removed :attr:`~motor.motor_tornado.MotorClient.max_bson_size`,
:attr:`~motor.motor_tornado.MotorClient.max_message_size`, and
:attr:`~motor.motor_tornado.MotorClient.max_write_batch_size`. These helpers
were incorrect when in ``loadBalanced=true mode`` and ambiguous in clusters
with mixed versions. Use the `hello command`_ to get the authoritative
value from the remote server instead. Code like this::
max_bson_size = client.max_bson_size
max_message_size = client.max_message_size
max_write_batch_size = client.max_write_batch_size
can be changed to this::
doc = await client.admin.command('hello')
max_bson_size = doc['maxBsonObjectSize']
max_message_size = doc['maxMessageSizeBytes']
max_write_batch_size = doc['maxWriteBatchSize']
.. _hello command: https://mongodb.com/docs/manual/reference/command/hello/
MotorClient.event_listeners and other configuration option helpers are removed
..............................................................................
The following client configuration option helpers are removed:
- :attr:`~motor.motor_tornado.MotorClient.event_listeners`.
- :attr:`~motor.motor_tornado.MotorClient.max_pool_size`.
- :attr:`~motor.motor_tornado.MotorClient.min_pool_size`.
- :attr:`~motor.motor_tornado.MotorClient.max_idle_time_ms`.
- :attr:`~motor.motor_tornado.MotorClient.local_threshold_ms`.
- :attr:`~motor.motor_tornado.MotorClient.server_selection_timeout`.
- :attr:`~motor.motor_tornado.MotorClient.retry_writes`.
- :attr:`~motor.motor_tornado.MotorClient.retry_reads`.
These helpers have been replaced by
:attr:`~motor.motor_tornado.MotorClient.options`. Code like this::
client.event_listeners
client.local_threshold_ms
client.server_selection_timeout
client.max_pool_size
client.min_pool_size
client.max_idle_time_ms
can be changed to this::
client.options.event_listeners
client.options.local_threshold_ms
client.options.server_selection_timeout
client.options.pool_options.max_pool_size
client.options.pool_options.min_pool_size
client.options.pool_options.max_idle_time_seconds
``tz_aware`` defaults to ``False``
..................................
``tz_aware``, an argument for :class:`~pymongo.json_util.JSONOptions`,
now defaults to ``False`` instead of ``True``. ``json_util.loads`` now
decodes datetime as naive by default.
MotorClient cannot execute operations after ``close()``
.......................................................
:class:`~motor.motor_tornado.MotorClient` cannot execute any operations
after being closed. The previous behavior would simply reconnect. However,
now you must create a new instance.
MotorClient raises exception when given more than one URI
.........................................................
:class:`~motor.motor_tornado.MotorClient` now raises a :exc:`~pymongo.errors.ConfigurationError`
when more than one URI is passed into the ``hosts`` argument.
MotorClient raises exception when given unescaped percent sign in login info
............................................................................
:class:`~motor.motor_tornado.MotorClient` now raises an
:exc:`~pymongo.errors.InvalidURI` exception
when it encounters unescaped percent signs in username and password.
Database
--------
MotorDatabase.current_op is removed
...................................
Removed :meth:`~motor.motor_tornado.MotorDatabase.current_op`. Use
:meth:`~motor.motor_tornado.MotorDatabase.aggregate` instead with the
`$currentOp aggregation pipeline stage`_. Code like
this::
ops = client.admin.current_op()['inprog']
can be changed to this::
ops = await client.admin.aggregate([{'$currentOp': {}}]).to_list()
.. _$currentOp aggregation pipeline stage: https://mongodb.com/docs/manual/reference/operator/aggregation/currentOp/
MotorDatabase.profiling_level is removed
........................................
Removed :meth:`~motor.motor_tornado.MotorDatabase.profiling_level` which was deprecated in
PyMongo 3.12. Use the `profile command`_ instead. Code like this::
level = db.profiling_level()
Can be changed to this::
profile = await db.command('profile', -1)
level = profile['was']
.. _profile command: https://mongodb.com/docs/manual/reference/command/profile/
MotorDatabase.set_profiling_level is removed
............................................
Removed :meth:`~motor.motor_tornado.MotorDatabase.set_profiling_level` which was deprecated in
PyMongo 3.12. Use the `profile command`_ instead. Code like this::
db.set_profiling_level(pymongo.ALL, filter={'op': 'query'})
Can be changed to this::
res = await db.command('profile', 2, filter={'op': 'query'})
MotorDatabase.profiling_info is removed
.......................................
Removed :meth:`~motor.motor_tornado.MotorDatabase.profiling_info` which was deprecated in
PyMongo 3.12. Query the `'system.profile' collection`_ instead. Code like this::
profiling_info = db.profiling_info()
Can be changed to this::
profiling_info = await db['system.profile'].find().to_list()
.. _'system.profile' collection: https://mongodb.com/docs/manual/reference/database-profiler/
MotorDatabase.__bool__ raises NotImplementedError
.................................................
:class:`~motor.motor_tornado.MotorDatabase` now raises an error upon evaluating as a
Boolean. Code like this::
if database:
Can be changed to this::
if database is not None:
You must now explicitly compare with None.
MotorCollection
---------------
MotorCollection.map_reduce and MotorCollection.inline_map_reduce are removed
............................................................................
Removed :meth:`~motor.motor_tornado.MotorCollection.map_reduce` and
:meth:`~motor.motor_tornado.MotorCollection.inline_map_reduce`.
Migrate to :meth:`~motor.motor_tornado.MotorCollection.aggregate` or run the
`mapReduce command`_ directly with :meth:`~motor.motor_tornado.MotorDatabase.command`
instead. For more guidance on this migration see:
- https://mongodb.com/docs/manual/reference/map-reduce-to-aggregation-pipeline/
- https://mongodb.com/docs/manual/reference/aggregation-commands-comparison/
.. _mapReduce command: https://mongodb.com/docs/manual/reference/command/mapReduce/
MotorCollection.reindex is removed
..................................
Removed :meth:`motor.motor_tornado.MotorCollection.reindex`. Run the
`reIndex command`_ directly instead. Code like this::
>>> result = await database.my_collection.reindex()
can be changed to this::
>>> result = await database.command('reIndex', 'my_collection')
.. _reIndex command: https://mongodb.com/docs/manual/reference/command/reIndex/
The modifiers parameter is removed
..................................
Removed the ``modifiers`` parameter from
:meth:`~motor.motor_tornado.MotorCollection.find`,
:meth:`~motor.motor_tornado.MotorCollection.find_one`,
:meth:`~motor.motor_tornado.MotorCollection.find_raw_batches`, and
:meth:`~motor.motor_tornado.MotorCursor`. Pass the options directly to the method
instead. Code like this::
cursor = await coll.find({}, modifiers={
"$comment": "comment",
"$hint": {"_id": 1},
"$min": {"_id": 0},
"$max": {"_id": 6},
"$maxTimeMS": 6000,
"$returnKey": False,
"$showDiskLoc": False,
})
can be changed to this::
cursor = await coll.find(
{},
comment="comment",
hint={"_id": 1},
min={"_id": 0},
max={"_id": 6},
max_time_ms=6000,
return_key=False,
show_record_id=False,
)
The hint parameter is required with min/max
...........................................
The ``hint`` option is now required when using ``min`` or ``max`` queries
with :meth:`~motor.motor_tornado.MotorCollection.find` to ensure the query utilizes
the correct index. For example, code like this::
cursor = await coll.find({}, min={'x', min_value})
can be changed to this::
cursor = await coll.find({}, min={'x', min_value}, hint=[('x', ASCENDING)])
MotorCollection.__bool__ raises NotImplementedError
...................................................
:class:`~motor.motor_tornado.MotorCollection` now raises an error upon evaluating
as a Boolean. Code like this::
if collection:
Can be changed to this::
if collection is not None:
You must now explicitly compare with None.
MotorCollection.find returns entire document with empty projection
..................................................................
Empty projections (eg {} or []) for
:meth:`~motor.motor_tornado.MotorCollection.find`, and
:meth:`~motor.motor_tornado.MotorCollection.find_one`
are passed to the server as-is rather than the previous behavior which
substituted in a projection of ``{"_id": 1}``. This means that an empty
projection will now return the entire document, not just the ``"_id"`` field.
To ensure that behavior remains consistent, code like this::
await coll.find({}, projection={})
Can be changed to this::
await coll.find({}, projection={"_id":1})
SONManipulator is removed
-------------------------
PyMongo 4.0 removed :mod:`pymongo.son_manipulator`.
Motor 3.0 removed :meth:`motor.MotorDatabase.add_son_manipulator`,
:attr:`motor.MotorDatabase.outgoing_copying_manipulators`,
:attr:`motor.MotorDatabase.outgoing_manipulators`,
:attr:`motor.MotorDatabase.incoming_copying_manipulators`, and
:attr:`motor.MotorDatabase.incoming_manipulators`.
Removed the ``manipulate`` parameter from
:meth:`~motor.motor_tornado.MotorCollection.find`,
:meth:`~motor.motor_tornado.MotorCollection.find_one`, and
:meth:`~motor.motor_tornado.MotorCursor`.
The :class:`pymongo.son_manipulator.SONManipulator` API has limitations as a
technique for transforming your data and was deprecated in PyMongo 3.0.
Instead, it is more flexible and straightforward to transform outgoing
documents in your own code before passing them to PyMongo, and transform
incoming documents after receiving them from PyMongo.
Alternatively, if your application uses the ``SONManipulator`` API to convert
custom types to BSON, the :class:`~pymongo.codec_options.TypeCodec` and
:class:`~pymongo.codec_options.TypeRegistry` APIs may be a suitable alternative.
For more information, see the `Custom Types documentation`_.
.. _Custom Types documentation: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/custom-types/type-codecs/
GridFS changes
--------------
.. _removed-gridfs-checksum:
disable_md5 parameter is removed
................................
Removed the ``disable_md5`` option for :class:`~motor.motor_tornado.gridfs.MotorGridFSBucket` and
:class:`~motor.motor_tornado.gridfs.MotorGridFS`. GridFS no longer generates checksums.
Applications that desire a file digest should implement it outside GridFS
and store it with other file metadata. For example::
import hashlib
my_db = MotorClient().test
fs = GridFSBucket(my_db)
grid_in = fs.open_upload_stream("test_file")
file_data = b'...'
sha356 = hashlib.sha256(file_data).hexdigest()
await grid_in.write(file_data)
grid_in.sha356 = sha356 # Set the custom 'sha356' field
await grid_in.close()
Note that for large files, the checksum may need to be computed in chunks
to avoid the excessive memory needed to load the entire file at once.
Removed features with no migration path
---------------------------------------
Encoding a UUID raises an error by default
..........................................
The default uuid_representation for :class:`~pymongo.codec_options.CodecOptions`,
:class:`~pymongo.json_util.JSONOptions`, and
:class:`~motor.motor_tornado.MotorClient` has been changed from
:data:`bson.binary.UuidRepresentation.PYTHON_LEGACY` to
:data:`bson.binary.UuidRepresentation.UNSPECIFIED`. Attempting to encode a
:class:`uuid.UUID` instance to BSON or JSON now produces an error by default.
Upgrade to Motor 3.0
--------------------
Once your application runs without deprecation warnings with Motor 2.5, upgrade
to Motor 3.0.

View File

@ -1,4 +1,4 @@
# Copyright 2009-present MongoDB, Inc.
# Copyright 2009-2015 MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,8 +15,9 @@
"""MongoDB specific extensions to Sphinx."""
from docutils import nodes
from docutils.parsers import rst
from sphinx import addnodes
from sphinx.util.compat import (Directive,
make_admonition)
class mongodoc(nodes.Admonition, nodes.Element):
@ -28,7 +29,7 @@ class mongoref(nodes.reference):
def visit_mongodoc_node(self, node):
self.visit_admonition(node, "seealso")
self.visit_admonition(node)
def depart_mongodoc_node(self, node):
@ -36,17 +37,20 @@ def depart_mongodoc_node(self, node):
def visit_mongoref_node(self, node):
atts = {"class": "reference external", "href": node["refuri"], "name": node["name"]}
self.body.append(self.starttag(node, "a", "", **atts))
atts = {"class": "reference external",
"href": node["refuri"],
"name": node["name"]}
self.body.append(self.starttag(node, 'a', '', **atts))
def depart_mongoref_node(self, node):
self.body.append("</a>")
self.body.append('</a>')
if not isinstance(node.parent, nodes.TextElement):
self.body.append("\n")
self.body.append('\n')
class MongodocDirective(rst.Directive):
class MongodocDirective(Directive):
has_content = True
required_arguments = 0
optional_arguments = 0
@ -54,11 +58,11 @@ class MongodocDirective(rst.Directive):
option_spec = {}
def run(self):
node = mongodoc()
title = "The MongoDB documentation on"
node += nodes.title(title, title)
self.state.nested_parse(self.content, self.content_offset, node)
return [node]
return make_admonition(mongodoc, self.name,
['See general MongoDB documentation'],
self.options, self.content, self.lineno,
self.content_offset, self.block_text,
self.state, self.state_machine)
def process_mongodoc_nodes(app, doctree, fromdocname):
@ -72,7 +76,7 @@ def process_mongodoc_nodes(app, doctree, fromdocname):
anchor = name["ids"][0]
break
for para in node.traverse(nodes.paragraph):
tag = str(list(para.traverse())[1])
tag = str(para.traverse()[1])
link = mongoref("", "")
link["refuri"] = "http://dochub.mongodb.org/core/%s" % tag
link["name"] = anchor
@ -83,13 +87,13 @@ def process_mongodoc_nodes(app, doctree, fromdocname):
def setup(app):
app.add_node(
mongodoc,
html=(visit_mongodoc_node, depart_mongodoc_node),
latex=(visit_mongodoc_node, depart_mongodoc_node),
text=(visit_mongodoc_node, depart_mongodoc_node),
)
app.add_node(mongoref, html=(visit_mongoref_node, depart_mongoref_node))
app.add_node(mongodoc,
html=(visit_mongodoc_node, depart_mongodoc_node),
latex=(visit_mongodoc_node, depart_mongodoc_node),
text=(visit_mongodoc_node, depart_mongodoc_node))
app.add_node(mongoref,
html=(visit_mongoref_node, depart_mongoref_node))
app.add_directive("mongodoc", MongodocDirective)
app.connect("doctree-resolved", process_mongodoc_nodes)

View File

@ -1,4 +1,4 @@
# Copyright 2012-present MongoDB, Inc.
# Copyright 2012-2014 MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -14,25 +14,35 @@
"""Motor specific extensions to Sphinx."""
import inspect
import re
from itertools import chain
from docutils.nodes import doctest_block, literal_block
from docutils.nodes import field, list_item, paragraph, title_reference, literal
from docutils.nodes import field_list, field_body, bullet_list, Text, field_name
from docutils.nodes import literal_block, doctest_block
from sphinx import addnodes
from sphinx.addnodes import desc, desc_content, desc_signature, seealso, versionmodified
from sphinx.addnodes import (desc, desc_content, versionmodified,
desc_signature, seealso, pending_xref)
from sphinx.util.inspect import safe_getattr
import motor
import motor.core
# This is a place to store info while parsing, to be used before generating.
motor_info = {}
def is_asyncio_api(name):
return 'motor_asyncio.' in name
def has_node_of_type(root, klass):
if isinstance(root, klass):
return True
for child in root.children: # noqa: SIM110
for child in root.children:
if has_node_of_type(child, klass):
return True
@ -52,6 +62,57 @@ def find_by_path(root, classes):
return rv
def get_parameter_names(parameters_node):
parameter_names = []
# Most PyMongo methods have bullet lists.
for list_item_node in find_by_path(parameters_node, [list_item]):
title_ref_nodes = find_by_path(
list_item_node, [paragraph, (title_reference, pending_xref)])
parameter_names.append(title_ref_nodes[0].astext())
# Some are just paragraphs.
for title_ref_node in find_by_path(parameters_node, [title_reference]):
parameter_names.append(title_ref_node.astext())
return parameter_names
def insert_callback(parameters_node):
# We need to know what params are here already
parameter_names = get_parameter_names(parameters_node)
if 'callback' not in parameter_names:
if '*args' in parameter_names:
args_pos = parameter_names.index('*args')
else:
args_pos = len(parameter_names)
if '**kwargs' in parameter_names:
kwargs_pos = parameter_names.index('**kwargs')
else:
kwargs_pos = len(parameter_names)
doc = (
" (optional): function taking (result, error), executed when"
" operation completes")
new_item = paragraph(
'', '',
literal('', 'callback'),
Text(doc))
if parameters_node.children and isinstance(parameters_node.children[0], list_item):
# Insert "callback" before *args and **kwargs
parameters_node.insert(min(args_pos, kwargs_pos), list_item('', new_item))
else:
parameters_node.append(new_item)
# Insert "callback" before *args and **kwargs
parameters_node.insert(min(args_pos, kwargs_pos), new_item)
docstring_warnings = []
@ -62,7 +123,7 @@ def maybe_warn_about_code_block(name, content_node):
def has_coro_annotation(signature_node):
try:
return "coroutine" in signature_node[0][0]
return 'coroutine' in signature_node[0][0]
except IndexError:
return False
@ -70,7 +131,10 @@ def has_coro_annotation(signature_node):
def process_motor_nodes(app, doctree):
# Search doctree for Motor's methods and attributes whose docstrings were
# copied from PyMongo, and fix them up for Motor:
# 1. Add a 'coroutine' annotation to the beginning of the declaration.
# 1. Add a 'callback' param (sometimes optional, sometimes required) to
# all Motor Tornado methods. If the PyMongo method took no params, we
# create a parameter-list from scratch, otherwise we edit PyMongo's
# list.
# 2. Remove all version annotations like "New in version 2.0" since
# PyMongo's version numbers are meaningless in Motor's docs.
# 3. Remove "seealso" directives that reference PyMongo's docs.
@ -79,31 +143,66 @@ def process_motor_nodes(app, doctree):
# 'autodoc-process-signature' event, because it's way easier to handle the
# parsed doctree before it's turned into HTML than it is to update the RST.
for objnode in doctree.traverse(desc):
if objnode["objtype"] in ("method", "attribute"):
if objnode['objtype'] in ('method', 'attribute'):
signature_node = find_by_path(objnode, [desc_signature])[0]
name = ".".join([signature_node["module"], signature_node["fullname"]])
name = '.'.join([
signature_node['module'], signature_node['fullname']])
assert name.startswith("motor.")
assert name.startswith('motor.')
obj_motor_info = motor_info.get(name)
if obj_motor_info:
desc_content_node = find_by_path(objnode, [desc_content])[0]
if desc_content_node.line is None and obj_motor_info["is_pymongo_docstring"]:
if (desc_content_node.line is None and
obj_motor_info['is_pymongo_docstring']):
maybe_warn_about_code_block(name, desc_content_node)
if obj_motor_info["is_async_method"]: # noqa: SIM102
if obj_motor_info['is_async_method']:
# Might be a handwritten RST with "coroutine" already.
if not has_coro_annotation(signature_node):
coro_annotation = addnodes.desc_annotation(
"coroutine ", "coroutine ", classes=["coro-annotation"]
)
'coroutine ', 'coroutine ',
classes=['coro-annotation'])
signature_node.insert(0, coro_annotation)
if obj_motor_info["is_pymongo_docstring"]:
if not is_asyncio_api(name):
retval = ("If a callback is passed, returns None, else"
" returns a Future.")
callback_p = paragraph('', Text(retval))
# Find the parameter list.
parameters_nodes = find_by_path(
desc_content_node, [
field_list,
field,
field_body,
(bullet_list, paragraph)])
if parameters_nodes:
parameters_node = parameters_nodes[0]
else:
# PyMongo method has no parameters, create an empty
# params list
parameters_node = bullet_list()
parameters_field_list_node = field_list(
'',
field(
'',
field_name('', 'Parameters '),
field_body('', parameters_node)))
desc_content_node.append(parameters_field_list_node)
insert_callback(parameters_node)
if retval not in str(desc_content_node):
desc_content_node.append(callback_p)
if obj_motor_info['is_pymongo_docstring']:
# Remove all "versionadded", "versionchanged" and
# "deprecated" directives from the docs we imported from
# PyMongo
version_nodes = find_by_path(desc_content_node, [versionmodified])
version_nodes = find_by_path(
desc_content_node, [versionmodified])
for version_node in version_nodes:
version_node.parent.remove(version_node)
@ -122,19 +221,22 @@ def get_motor_attr(motor_class, name, *defargs):
attribute. While we're at it, store some info about each attribute
in the global motor_info dict.
"""
attr = safe_getattr(motor_class, name, *defargs)
attr = safe_getattr(motor_class, name)
# Store some info for process_motor_nodes()
full_name = f"{motor_class.__module__}.{motor_class.__name__}.{name}"
full_name = '%s.%s.%s' % (
motor_class.__module__, motor_class.__name__, name)
full_name_legacy = f"motor.{motor_class.__module__}.{motor_class.__name__}.{name}"
full_name_legacy = 'motor.%s.%s.%s' % (
motor_class.__module__, motor_class.__name__, name)
# These sub-attributes are set in motor.asynchronize()
has_coroutine_annotation = getattr(attr, "coroutine_annotation", False)
is_async_method = getattr(attr, "is_async_method", False)
is_cursor_method = getattr(attr, "is_motorcursor_chaining_method", False)
has_coroutine_annotation = getattr(attr, 'coroutine_annotation', False)
is_async_method = getattr(attr, 'is_async_method', False)
is_cursor_method = getattr(attr, 'is_motorcursor_chaining_method', False)
if is_async_method or is_cursor_method:
pymongo_method = getattr(motor_class.__delegate_class__, attr.pymongo_method_name)
pymongo_method = getattr(
motor_class.__delegate_class__, attr.pymongo_method_name)
else:
pymongo_method = None
@ -142,27 +244,68 @@ def get_motor_attr(motor_class, name, *defargs):
is_pymongo_doc = pymongo_method and attr.__doc__ == pymongo_method.__doc__
motor_info[full_name] = motor_info[full_name_legacy] = {
"is_async_method": is_async_method or has_coroutine_annotation,
"is_pymongo_docstring": is_pymongo_doc,
"pymongo_method": pymongo_method,
'is_async_method': is_async_method or has_coroutine_annotation,
'is_pymongo_docstring': is_pymongo_doc,
'pymongo_method': pymongo_method,
}
return attr
pymongo_ref_pat = re.compile(r":doc:`(.*?)`", re.MULTILINE)
def get_motor_argspec(name, method):
args, varargs, kwargs, defaults = inspect.getargspec(method)
# This part is copied from Sphinx's autodoc.py
if args and args[0] in ('cls', 'self'):
del args[0]
defaults = list(defaults) if defaults else []
if ('callback' not in chain(args or [], kwargs or []) and
not is_asyncio_api(name)):
# Add 'callback=None' argument
args.append('callback')
defaults.append(None)
return args, varargs, kwargs, defaults
# Adapted from MethodDocumenter.format_args
def format_motor_args(name, motor_method, pymongo_method):
if pymongo_method:
argspec = get_motor_argspec(name, pymongo_method)
else:
argspec = get_motor_argspec(name, motor_method)
formatted_argspec = inspect.formatargspec(*argspec)
# escape backslashes for reST
return formatted_argspec.replace('\\', '\\\\')
pymongo_ref_pat = re.compile(r':doc:`(.*?)`', re.MULTILINE)
def _sub_pymongo_ref(match):
ref = match.group(1)
return ":doc:`%s`" % ref.lstrip("/")
return ':doc:`%s`' % ref.lstrip('/')
def process_motor_docstring(app, what, name, obj, options, lines):
if name in motor_info and motor_info[name].get("is_pymongo_docstring"):
joined = "\n".join(lines)
subbed = pymongo_ref_pat.sub(_sub_pymongo_ref, joined)
lines[:] = subbed.split("\n")
if name in motor_info and motor_info[name].get('is_pymongo_docstring'):
joined = '\n'.join(lines)
subbed = pymongo_ref_pat.sub(_sub_pymongo_ref, joined, re.MULTILINE)
lines[:] = subbed.split('\n')
def process_motor_signature(
app, what, name, obj, options, signature, return_annotation):
info = motor_info.get(name)
if info:
# Real sig obscured by decorator, reconstruct it
pymongo_method = info['pymongo_method']
if info['is_async_method']:
args = format_motor_args(name, obj, pymongo_method)
return args, return_annotation
def build_finished(app, exception):
@ -174,7 +317,7 @@ def build_finished(app, exception):
def setup(app):
app.add_autodoc_attrgetter(type(motor.core.AgnosticBase), get_motor_attr)
app.connect("autodoc-process-docstring", process_motor_docstring)
app.connect("doctree-read", process_motor_nodes)
app.connect("build-finished", build_finished)
return {"parallel_write_safe": True, "parallel_read_safe": False}
app.connect('autodoc-process-docstring', process_motor_docstring)
app.connect('autodoc-process-signature', process_motor_signature)
app.connect('doctree-read', process_motor_nodes)
app.connect('build-finished', build_finished)

View File

@ -1,29 +1,37 @@
Requirements
============
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
The current version of Motor requires:
* CPython 3.10 and later.
* PyMongo_ 4.9 and later.
* CPython 2.6, 2.7, or 3.3 and later.
* PyMongo_ 3.4 and later.
Motor can integrate with either Tornado or asyncio.
Beginning with version 0.5, Motor can integrate with either Tornado or asyncio.
The default authentication mechanism for MongoDB is SCRAM-SHA-1.
Requires the `futures`_ package from PyPI on Python 2.
The default authentication mechanism for MongoDB 3.0+ is SCRAM-SHA-1.
Install `backports.pbkdf2`_ for faster authentication with MongoDB 3.0+,
especially on Python older than 2.7.8, or on Python 3.3.
(Python 2.7.9 and later, or Python 3.4 and later, have builtin hash functions
nearly as fast as backports.pbkdf2.)
Building the docs requires `sphinx`_.
In Python 2.6, unittest2_ is automatically installed by
``python setup.py test``.
.. _PyMongo: https://pypi.python.org/pypi/pymongo/
.. _sphinx: https://www.sphinx-doc.org/
.. _futures: https://pypi.python.org/pypi/futures
.. _backports.pbkdf2: https://pypi.python.org/pypi/backports.pbkdf2/
.. _sphinx: http://sphinx.pocoo.org/
.. _unittest2: https://pypi.python.org/pypi/unittest2
.. _compatibility-matrix:
Compatibility Matrix
--------------------
@ -31,54 +39,63 @@ Compatibility Matrix
Motor and PyMongo
`````````````````
Older versions of Motor depended on exact PyMongo versions. Version 0.7 requires
the latest PyMongo 2.9.x release beginning with 2.9.4, Version 1.0 works
with any PyMongo version beginning with 3.3.0, and Version 1.1 works with any
PyMongo version beginning with 3.4.0.
+-------------------+-----------------+
| Motor Version | PyMongo Version |
+===================+=================+
| 2.5 | 3.12+ |
| 0.1 | 2.5.0 |
+-------------------+-----------------+
| 3.0 | 4.1+ |
| 0.2 | 2.7.0 |
+-------------------+-----------------+
| 3.1 | 4.2+ |
| 0.3 | 2.7.1 |
+-------------------+-----------------+
| 3.2 | 4.4+ |
| 0.4 | 2.8.0 |
+-------------------+-----------------+
| 3.3 | 4.5+ |
| 0.5 | 2.8.0 |
+-------------------+-----------------+
| 3.4 | 4.5+ |
| 0.6 | 2.8.0 |
+-------------------+-----------------+
| 3.5 | 4.5+ |
| 0.7 | 2.9.4+ |
+-------------------+-----------------+
| 3.6 | 4.9 |
| 1.0 | 3.3+ |
+-------------------+-----------------+
| 3.7 | 4.9+ |
| 1.1 | 3.4+ |
+-------------------+-----------------+
Motor and MongoDB
`````````````````
+---------------------------------------------------------------------+
| MongoDB Version |
+=====================+=====+=====+=====+=====+=====+=====+=====+=====+
| | 3.6 | 4.0 | 4.2 | 4.4 | 5.0 | 6.0 | 7.0 | 8.0 |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Motor Version | 2.5 | Y | Y | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.0 | Y | Y | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.1 | Y | Y | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.2 | Y | Y | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.3 | Y | Y | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.4 | Y | Y | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.5 | Y | Y | Y | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.6 | Y | Y | Y | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.7 | N | Y | Y | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
All Motor versions are usable with all MongoDB versions as old as 2.2.
Where "N" appears there are some incompatibilities and
unsupported server features.
+---------------------------------------------------------+
| MongoDB Version |
+=====================+=====+=====+=====+=====+=====+=====+
| | 2.2 | 2.4 | 2.6 | 3.0 | 3.2 | 3.4 |
+---------------+-----+-----+-----+-----+-----+-----+-----+
| Motor Version | 0.1 | Y | Y |**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 0.2 | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 0.3 | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 0.4 | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 0.5 | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 0.6 | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 0.7 | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 1.0 | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+
| | 1.1 | Y | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+-----+-----+
There is no relationship between PyMongo and MongoDB version numbers, although
the numbers happen to be close or equal in recent releases of PyMongo and MongoDB.
@ -86,7 +103,7 @@ Use `the PyMongo compatibility matrix`_ to determine what MongoDB version is
supported by PyMongo. Use the compatibility matrix above to determine what
MongoDB version Motor supports.
.. _the PyMongo compatibility matrix: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/compatibility/
.. _the PyMongo compatibility matrix: https://docs.mongodb.org/ecosystem/drivers/python/#mongodb-compatibility
Motor and Tornado
`````````````````
@ -94,84 +111,76 @@ Motor and Tornado
Where "N" appears in this matrix, the versions of Motor and Tornado are
known to be incompatible, or have not been tested together.
+---------------------------------------------+
| Tornado Version |
+=====================+=====+=====+=====+=====+
| Motor Version | 2.5 |**N**|**N**| Y | Y |
+---------------+-----+-----+-----+-----+-----+
| | 3.0 |**N**|**N**|**N**| Y |
+---------------+-----+-----+-----+-----+-----+
| | 3.1 |**N**|**N**|**N**| Y |
+---------------+-----+-----+-----+-----+-----+
| | 3.2 |**N**|**N**|**N**| Y |
+---------------+-----+-----+-----+-----+-----+
| | 3.3 |**N**|**N**|**N**| Y |
+---------------+-----+-----+-----+-----+-----+
| | 3.4 |**N**|**N**|**N**| Y |
+---------------+-----+-----+-----+-----+-----+
+---------------------------------+
| Tornado Version |
+=====================+=====+=====+
| | 3.x | 4.x |
+---------------+-----+-----+-----+
| Motor Version | 0.1 | Y |**N**|
+---------------+-----+-----+-----+
| | 0.2 | Y | Y |
+---------------+-----+-----+-----+
| | 0.3 | Y | Y |
+---------------+-----+-----+-----+
| | 0.4 | Y | Y |
+---------------+-----+-----+-----+
| | 0.5 | Y | Y |
+---------------+-----+-----+-----+
| | 0.6 | Y | Y |
+---------------+-----+-----+-----+
| | 0.7 | Y | Y |
+---------------+-----+-----+-----+
| | 1.0 | Y | Y |
+---------------+-----+-----+-----+
| | 1.1 | Y | Y |
+---------------+-----+-----+-----+
Motor and Python
````````````````
Motor 2.5 deprecated support for Python 3.5.
Until version 0.5, Motor required Tornado, and it supported the same version of
Python as its supported Tornado versions did.
Motor 3.0 dropped support for Pythons older than 3.7.
Beginning in version 0.5, Motor integrates with asyncio or Tornado.
For asyncio support specifically, Motor requires Python 3.4+, or Python 3.3
with the `asyncio package from PyPI`_.
Motor 3.1.1 added support for Python 3.11.
+----------------------------------------------------------------+
| Python Version |
+=====================+=====+=====+=====+======+=====+=====+=====+
| | 2.5 | 2.6 | 2.7 | 3.3 | 3.4 | 3.5 | 3.6 |
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| Motor Version | 0.1 | Y | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 0.2 |**N**| Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 0.3 |**N**| Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 0.4 |**N**| Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 0.5 |**N**| Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 0.6 |**N**| Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 0.7 |**N**| Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 1.0 |**N**| Y | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
| | 1.1 |**N**| Y | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+------+-----+-----+-----+
Motor 3.3 added support for Python 3.12.
Motor 3.5 dropped support for Python 3.7 and added support for Python 3.13.
Motor 3.7 dropped support for Python 3.8.
Motor 3.8 dropped support for Python 3.9 and added support for Python 3.14.
+---------------------------------------------------------------------------+
| Python Version |
+=====================+=====+=====+=====+=====+=====+=====+=====+=====+=====+
| | 3.6 | 3.7 | 3.8 | 3.9 | 3.10| 3.11| 3.12| 3.13| 3.14|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Motor Version | 1.0 | Y |**N**|**N**|**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 1.1 | Y |**N**|**N**|**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 1.2 | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 1.3 | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.0 | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.1 | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.2 | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.3 | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.4 | Y | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.5 | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.0 |**N**| Y | Y | Y | Y |**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.1 |**N**| Y | Y | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.2 |**N**| Y | Y | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.3 |**N**| Y | Y | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.4 |**N**| Y | Y | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.5 |**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.6 |**N**|**N**| Y | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.7 |**N**|**N**|**N**| Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 3.8 |**N**|**N**|**N**|**N**| Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
.. _asyncio package from PyPI: https://pypi.python.org/pypi/asyncio
Not Supported
-------------
Motor does not support Jython or IronPython.
Motor does not support Windows:
* The author does not test Motor on Windows to ensure it is correct or fast.
* Tornado `is not officially supported on Windows
<http://www.tornadoweb.org/en/stable/index.html#installation>`_,
so Motor's Tornado integration on Windows is doubly-unsupported.
* Since asyncio *does* officially support Windows, Motor's asyncio integration
is more likely to work there, but it is untested.
Motor also does not support Jython.

View File

@ -3,13 +3,6 @@
Tutorial: Using Motor With :mod:`asyncio`
=========================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. These setups are redundant because I can't figure out how to make doctest
run a common setup *before* the setup for the two groups. A "testsetup:: *"
is the obvious answer, but it's run *after* group-specific setup.
@ -19,26 +12,22 @@ Tutorial: Using Motor With :mod:`asyncio`
import pymongo
import motor.motor_asyncio
import asyncio
client = motor.motor_asyncio.AsyncIOMotorClient()
db = client.test_database
from asyncio import coroutine
db = motor.motor_asyncio.AsyncIOMotorClient().test_database
.. testsetup:: after-inserting-2000-docs
import pymongo
import motor.motor_asyncio
import asyncio
client = motor.motor_asyncio.AsyncIOMotorClient()
db = client.test_database
from asyncio import coroutine
db = motor.motor_asyncio.AsyncIOMotorClient().test_database
pymongo.MongoClient().test_database.test_collection.insert_many(
[{"i": i} for i in range(2000)]
)
[{'i': i} for i in range(2000)])
.. testcleanup:: *
import pymongo
pymongo.MongoClient().test_database.test_collection.delete_many({})
A guide to using MongoDB and asyncio with Motor.
@ -49,13 +38,13 @@ Tutorial Prerequisites
----------------------
You can learn about MongoDB with the `MongoDB Tutorial`_ before you learn Motor.
Using Python 3.5 or later, do::
Using Python 3.4 or later, do::
$ python3 -m pip install motor
This tutorial assumes that a MongoDB instance is running on the
default host and port. Assuming you have `downloaded and installed
<https://mongodb.com/docs/manual/installation/>`_ MongoDB, you
<http://docs.mongodb.org/manual/installation/>`_ MongoDB, you
can start it like so:
.. code-block:: bash
@ -64,7 +53,7 @@ can start it like so:
.. _pip: http://www.pip-installer.org/en/latest/installing.html
.. _MongoDB Tutorial: https://mongodb.com/docs/manual/tutorial/getting-started/
.. _MongoDB Tutorial: http://docs.mongodb.org/manual/tutorial/getting-started/
Object Hierarchy
----------------
@ -85,9 +74,7 @@ Motor, like PyMongo, represents data with a 4-level object hierarchy:
Creating a Client
-----------------
Creating a client is what establishes a connection to MongoDB and tells your
app what deployment (i.e. cluster) to connect to. You typically create a single
instance of :class:`~motor.motor_asyncio.AsyncIOMotorClient` at the time your
You typically create a single instance of :class:`~motor.motor_asyncio.AsyncIOMotorClient` at the time your
application starts up.
.. doctest:: before-inserting-2000-docs
@ -100,38 +87,38 @@ specify the host and port like:
.. doctest:: before-inserting-2000-docs
>>> client = motor.motor_asyncio.AsyncIOMotorClient("localhost", 27017)
>>> client = motor.motor_asyncio.AsyncIOMotorClient('localhost', 27017)
Motor also supports `connection URIs`_:
.. doctest:: before-inserting-2000-docs
>>> client = motor.motor_asyncio.AsyncIOMotorClient("mongodb://localhost:27017")
>>> client = motor.motor_asyncio.AsyncIOMotorClient('mongodb://localhost:27017')
Connect to a replica set like:
>>> client = motor.motor_asyncio.AsyncIOMotorClient('mongodb://host1,host2/?replicaSet=my-replicaset-name')
.. _connection URIs: https://mongodb.com/docs/manual/reference/connection-string/
.. _connection URIs: http://docs.mongodb.org/manual/reference/connection-string/
Getting a Database
------------------
A single instance of MongoDB can support multiple independent
`databases <https://www.mongodb.com/docs/manual/core/databases-and-collections/>`_.
`databases <http://docs.mongodb.org/manual/reference/glossary/#term-database>`_.
From an open client, you can get a reference to a particular database with
dot-notation or bracket-notation:
.. doctest:: before-inserting-2000-docs
>>> db = client.test_database
>>> db = client["test_database"]
>>> db = client['test_database']
Creating a reference to a database does no I/O and does not require an
``await`` expression.
Getting a Collection
--------------------
A `collection <https://www.mongodb.com/docs/manual/core/databases-and-collections/>`_
A `collection <http://docs.mongodb.org/manual/reference/glossary/#term-collection>`_
is a group of documents stored in MongoDB, and can be thought of as roughly
the equivalent of a table in a relational database. Getting a
collection in Motor works the same as getting a database:
@ -139,7 +126,7 @@ collection in Motor works the same as getting a database:
.. doctest:: before-inserting-2000-docs
>>> collection = db.test_collection
>>> collection = db["test_collection"]
>>> collection = db['test_collection']
Just like getting a reference to a database, getting a reference to a
collection does no I/O and doesn't require an ``await`` expression.
@ -153,13 +140,12 @@ store a document in MongoDB, call :meth:`~AsyncIOMotorCollection.insert_one` in
.. doctest:: before-inserting-2000-docs
>>> async def do_insert():
... document = {"key": "value"}
... document = {'key': 'value'}
... result = await db.test_collection.insert_one(document)
... print("result %s" % repr(result.inserted_id))
... print('result %s' % repr(result.inserted_id))
...
>>>
>>> import asyncio
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_insert())
result ObjectId('...')
@ -170,22 +156,31 @@ store a document in MongoDB, call :meth:`~AsyncIOMotorCollection.insert_one` in
>>> # Clean up from previous insert
>>> pymongo.MongoClient().test_database.test_collection.delete_many({})
DeleteResult({'n': 1, 'ok': 1.0}, acknowledged=True)
<pymongo.results.DeleteResult ...>
Insert documents in large batches with :meth:`~AsyncIOMotorCollection.insert_many`:
Using native coroutines
-----------------------
Starting in Python 3.5, you can define a `native coroutine`_ with `async def`
instead of the ``coroutine`` decorator. Within a native coroutine, wait
for an async operation with `await` instead of `yield`:
.. doctest:: before-inserting-2000-docs
>>> async def do_insert():
... result = await db.test_collection.insert_many([{"i": i} for i in range(2000)])
... print("inserted %d docs" % (len(result.inserted_ids),))
... for i in range(2000):
... result = await db.test_collection.insert_one({'i': i})
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_insert())
inserted 2000 docs
Getting a Single Document With ``find_one``
-------------------------------------------
Within a native coroutine, the syntax to use Motor with Tornado or asyncio
is often identical.
.. _native coroutine: https://www.python.org/dev/peps/pep-0492/
Getting a Single Document With `find_one`
-----------------------------------------
Use :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_one` to get the first document that
matches a query. For example, to get a document where the value for key "i" is
@ -194,10 +189,10 @@ less than 1:
.. doctest:: after-inserting-2000-docs
>>> async def do_find_one():
... document = await db.test_collection.find_one({"i": {"$lt": 1}})
... document = await db.test_collection.find_one({'i': {'$lt': 1}})
... pprint.pprint(document)
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_find_one())
{'_id': ObjectId('...'), 'i': 0}
@ -221,11 +216,11 @@ To find all documents with "i" less than 5:
.. doctest:: after-inserting-2000-docs
>>> async def do_find():
... cursor = db.test_collection.find({"i": {"$lt": 5}}).sort("i")
... cursor = db.test_collection.find({'i': {'$lt': 5}}).sort('i')
... for document in await cursor.to_list(length=100):
... pprint.pprint(document)
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_find())
{'_id': ObjectId('...'), 'i': 0}
{'_id': ObjectId('...'), 'i': 1}
@ -245,10 +240,10 @@ You can handle one document at a time in an ``async for`` loop:
>>> async def do_find():
... c = db.test_collection
... async for document in c.find({"i": {"$lt": 2}}):
... async for document in c.find({'i': {'$lt': 2}}):
... pprint.pprint(document)
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_find())
{'_id': ObjectId('...'), 'i': 0}
{'_id': ObjectId('...'), 'i': 1}
@ -258,13 +253,13 @@ You can apply a sort, limit, or skip to a query before you begin iterating:
.. doctest:: after-inserting-2000-docs
>>> async def do_find():
... cursor = db.test_collection.find({"i": {"$lt": 4}})
... cursor = db.test_collection.find({'i': {'$lt': 5}})
... # Modify the query before iterating
... cursor.sort("i", -1).skip(1).limit(2)
... cursor.sort('i', -1).limit(2).skip(2)
... async for document in cursor:
... pprint.pprint(document)
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_find())
{'_id': ObjectId('...'), 'i': 2}
{'_id': ObjectId('...'), 'i': 1}
@ -272,27 +267,55 @@ You can apply a sort, limit, or skip to a query before you begin iterating:
The cursor does not actually retrieve each document from the server
individually; it gets documents efficiently in `large batches`_.
.. _`large batches`: https://www.mongodb.com/docs/manual/core/cursors/#cursor-batches
.. _`large batches`: https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches
Iteration in Python 3.3 and 3.4
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In Python versions without ``async for``, handle one document at a time with
:attr:`~motor.motor_asyncio.AsyncIOMotorCursor.fetch_next`
and :meth:`~motor.motor_asyncio.AsyncIOMotorCursor.next_object`:
.. doctest:: after-inserting-2000-docs
>>> @coroutine
... def do_find():
... cursor = db.test_collection.find({'i': {'$lt': 5}})
... while (yield from cursor.fetch_next):
... document = cursor.next_object()
... pprint.pprint(document)
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_find())
{'_id': ObjectId('...'), 'i': 0}
{'_id': ObjectId('...'), 'i': 1}
{'_id': ObjectId('...'), 'i': 2}
{'_id': ObjectId('...'), 'i': 3}
{'_id': ObjectId('...'), 'i': 4}
Counting Documents
------------------
Use :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.count_documents` to
determine the number of documents in a collection, or the number of documents
that match a query:
Use :meth:`~motor.motor_asyncio.AsyncIOMotorCursor.count` to determine the number of documents in
a collection, or the number of documents that match a query:
.. doctest:: after-inserting-2000-docs
>>> async def do_count():
... n = await db.test_collection.count_documents({})
... print("%s documents in collection" % n)
... n = await db.test_collection.count_documents({"i": {"$gt": 1000}})
... print("%s documents where i > 1000" % n)
... n = await db.test_collection.find().count()
... print('%s documents in collection' % n)
... n = await db.test_collection.find({'i': {'$gt': 1000}}).count()
... print('%s documents where i > 1000' % n)
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_count())
2000 documents in collection
999 documents where i > 1000
:meth:`~motor.motor_asyncio.AsyncIOMotorCursor.count` uses the *count command* internally; we'll
cover commands_ below.
.. seealso:: `Count command <http://docs.mongodb.org/manual/reference/command/count/>`_
Updating Documents
------------------
@ -305,15 +328,15 @@ replacement document. The query follows the same syntax as for :meth:`find` or
>>> async def do_replace():
... coll = db.test_collection
... old_document = await coll.find_one({"i": 50})
... print("found document: %s" % pprint.pformat(old_document))
... _id = old_document["_id"]
... result = await coll.replace_one({"_id": _id}, {"key": "value"})
... print("replaced %s document" % result.modified_count)
... new_document = await coll.find_one({"_id": _id})
... print("document is now %s" % pprint.pformat(new_document))
... old_document = await coll.find_one({'i': 50})
... print('found document: %s' % pprint.pformat(old_document))
... _id = old_document['_id']
... result = await coll.replace_one({'_id': _id}, {'key': 'value'})
... print('replaced %s document' % result.modified_count)
... new_document = await coll.find_one({'_id': _id})
... print('document is now %s' % pprint.pformat(new_document))
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_replace())
found document: {'_id': ObjectId('...'), 'i': 50}
replaced 1 document
@ -331,12 +354,12 @@ operator to set "key" to "value":
>>> async def do_update():
... coll = db.test_collection
... result = await coll.update_one({"i": 51}, {"$set": {"key": "value"}})
... print("updated %s document" % result.modified_count)
... new_document = await coll.find_one({"i": 51})
... print("document is now %s" % pprint.pformat(new_document))
... result = await coll.update_one({'i': 51}, {'$set': {'key': 'value'}})
... print('updated %s document' % result.modified_count)
... new_document = await coll.find_one({'i': 51})
... print('document is now %s' % pprint.pformat(new_document))
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_update())
updated 1 document
document is now {'_id': ObjectId('...'), 'i': 51, 'key': 'value'}
@ -354,24 +377,6 @@ update all of them with :meth:`update_many`::
Deleting Documents
------------------
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.delete_one` takes a query with the same syntax as
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find`.
:meth:`delete_one` immediately removes the first returned matching document.
.. doctest:: after-inserting-2000-docs
>>> async def do_delete_one():
... coll = db.test_collection
... n = await coll.count_documents({})
... print("%s documents before calling delete_one()" % n)
... result = await db.test_collection.delete_one({"i": {"$gte": 1000}})
... print("%s documents after" % (await coll.count_documents({})))
...
>>> loop = client.get_io_loop()
>>> loop.run_until_complete(do_delete_one())
2000 documents before calling delete_one()
1999 documents after
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.delete_many` takes a query with the same syntax as
:meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find`.
:meth:`delete_many` immediately removes all matching documents.
@ -380,36 +385,40 @@ Deleting Documents
>>> async def do_delete_many():
... coll = db.test_collection
... n = await coll.count_documents({})
... print("%s documents before calling delete_many()" % n)
... result = await db.test_collection.delete_many({"i": {"$gte": 1000}})
... print("%s documents after" % (await coll.count_documents({})))
... n = await coll.count()
... print('%s documents before calling delete_many()' % n)
... result = await db.test_collection.delete_many({'i': {'$gte': 1000}})
... print('%s documents after' % (await coll.count()))
...
>>> loop = client.get_io_loop()
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(do_delete_many())
1999 documents before calling delete_many()
2000 documents before calling delete_many()
1000 documents after
.. mongodoc:: remove
Commands
--------
All operations on MongoDB are implemented internally as commands. Run them using
the :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command` method on
:class:`~motor.motor_asyncio.AsyncIOMotorDatabase`::
Besides the "CRUD" operations--insert, update, delete, and find--all other
operations on MongoDB are commands. Run them using
the :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command` method on :class:`~motor.motor_asyncio.AsyncIOMotorDatabase`:
.. doctest:: after-inserting-2000-docs
>>> from bson import SON
>>> async def use_distinct_command():
... response = await db.command(SON([("distinct", "test_collection"), ("key", "i")]))
>>> async def use_count_command():
... response = await db.command(SON([("count", "test_collection")]))
... print('response: %s' % pprint.pformat(response))
...
>>> loop = client.get_io_loop()
>>> loop.run_until_complete(use_distinct_command())
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(use_count_command())
response: {'n': 1000, 'ok': 1.0...}
Since the order of command parameters matters, don't use a Python dict to pass
the command's parameters. Instead, make a habit of using :class:`bson.SON`,
from the ``bson`` module included with PyMongo.
from the ``bson`` module included with PyMongo::
await db.command(SON([("distinct", "test_collection"), ("key", "my_key"]))
Many commands have special helper methods, such as
:meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.create_collection` or
@ -461,9 +470,11 @@ to an :class:`aiohttp.web.Application`:
:start-after: main-start
:end-before: main-end
.. warning:: It is a common mistake to create a new client object for every request; this comes at a dire performance cost.
Create the client when your application starts and reuse that one client for the lifetime of the process.
You can maintain the client by storing a database handle from the client on your application object, as shown in this example.
Note that it is a common mistake to create a new client object for every
request; this comes at a dire performance cost. Create the client
when your application starts and reuse that one client for the lifetime
of the process. You can maintain the client by storing a database handle
from the client on your application object, as shown in this example.
Visit ``localhost:8080/pages/page-one`` and the server responds "Hello!".
At ``localhost:8080/pages/page-two`` it responds "Goodbye." At other URLs it
@ -485,4 +496,5 @@ reference to Motor's complete feature set.
Learning to use the MongoDB driver is just the beginning, of course. For
in-depth instruction in MongoDB itself, see `The MongoDB Manual`_.
.. _The MongoDB Manual: https://mongodb.com/docs/manual/
.. _The MongoDB Manual: http://docs.mongodb.org/manual/

View File

@ -3,13 +3,6 @@
Tutorial: Using Motor With Tornado
==================================
.. warning:: As of May 14th, 2025, Motor is deprecated in favor of the GA release of the PyMongo Async API.
No new features will be added to Motor, and only bug fixes will be provided until it reaches end of life on May 14th, 2026.
After that, only critical bug fixes will be made until final support ends on May 14th, 2027.
We strongly recommend migrating to the PyMongo Async API while Motor is still supported.
For help transitioning, see the `Migrate to PyMongo Async guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_.
.. These setups are redundant because I can't figure out how to make doctest
run a common setup *before* the setup for the two groups. A "testsetup:: *"
is the obvious answer, but it's run *after* group-specific setup.
@ -21,7 +14,6 @@ Tutorial: Using Motor With Tornado
import tornado.web
from tornado.ioloop import IOLoop
from tornado import gen
db = motor.motor_tornado.MotorClient().test_database
.. testsetup:: after-inserting-2000-docs
@ -31,16 +23,15 @@ Tutorial: Using Motor With Tornado
import tornado.web
from tornado.ioloop import IOLoop
from tornado import gen
db = motor.motor_tornado.MotorClient().test_database
sync_db = pymongo.MongoClient().test_database
sync_db.test_collection.drop()
sync_db.test_collection.insert_many([{"i": i} for i in range(2000)])
sync_db.test_collection.insert_many(
[{'i': i} for i in range(2000)])
.. testcleanup:: *
import pymongo
pymongo.MongoClient().test_database.test_collection.delete_many({})
A guide to using MongoDB and Tornado with Motor.
@ -64,7 +55,7 @@ exception:
This tutorial also assumes that a MongoDB instance is running on the
default host and port. Assuming you have `downloaded and installed
<https://mongodb.com/docs/manual/installation/>`_ MongoDB, you
<http://docs.mongodb.org/manual/installation/>`_ MongoDB, you
can start it like so:
.. code-block:: bash
@ -73,7 +64,7 @@ can start it like so:
.. _pip: http://www.pip-installer.org/en/latest/installing.html
.. _MongoDB Tutorial: https://mongodb.com/docs/manual/tutorial/getting-started/
.. _MongoDB Tutorial: http://docs.mongodb.org/manual/tutorial/getting-started/
Object Hierarchy
----------------
@ -93,9 +84,8 @@ Motor, like PyMongo, represents data with a 4-level object hierarchy:
Creating a Client
-----------------
Creating a client is what establishes a connection to MongoDB and tells your
app what deployment (i.e. cluster) to connect to. You typically create a single
instance of :class:`MotorClient` at the time your application starts up.
You typically create a single instance of :class:`MotorClient` at the time your
application starts up.
.. doctest:: before-inserting-2000-docs
@ -106,34 +96,34 @@ specify the host and port like:
.. doctest:: before-inserting-2000-docs
>>> client = motor.motor_tornado.MotorClient("localhost", 27017)
>>> client = motor.motor_tornado.MotorClient('localhost', 27017)
Motor also supports `connection URIs`_:
.. doctest:: before-inserting-2000-docs
>>> client = motor.motor_tornado.MotorClient("mongodb://localhost:27017")
>>> client = motor.motor_tornado.MotorClient('mongodb://localhost:27017')
Connect to a replica set like:
>>> client = motor.motor_tornado.MotorClient('mongodb://host1,host2/?replicaSet=my-replicaset-name')
.. _connection URIs: https://mongodb.com/docs/manual/reference/connection-string/
.. _connection URIs: http://docs.mongodb.org/manual/reference/connection-string/
Getting a Database
------------------
A single instance of MongoDB can support multiple independent
`databases <https://www.mongodb.com/docs/manual/core/databases-and-collections/>`_.
`databases <http://docs.mongodb.org/manual/reference/glossary/#term-database>`_.
From an open client, you can get a reference to a particular database with
dot-notation or bracket-notation:
.. doctest:: before-inserting-2000-docs
>>> db = client.test_database
>>> db = client["test_database"]
>>> db = client['test_database']
Creating a reference to a database does no I/O and does not require an
``await`` expression.
Creating a reference to a database does no I/O and does not accept a callback
or return a Future.
Tornado Application Startup Sequence
------------------------------------
@ -159,37 +149,14 @@ makes it available to request handlers::
def get(self):
db = self.settings['db']
.. warning:: It is a common mistake to create a new client object for every
request; **this comes at a dire performance cost**. Create the client
when your application starts and reuse that one client for the lifetime
of the process, as shown in these examples.
The Tornado :class:`~tornado.httpserver.HTTPServer` class's :meth:`start`
method is a simple way to fork multiple web servers and use all of your
machine's CPUs. However, you must create your ``MotorClient`` after forking::
# Create the application before creating a MotorClient.
application = tornado.web.Application([
(r'/', MainHandler)
])
server = tornado.httpserver.HTTPServer(application)
server.bind(8888)
# Forks one process per CPU.
server.start(0)
# Now, in each child process, create a MotorClient.
application.settings['db'] = MotorClient().test_database
IOLoop.current().start()
For production-ready, multiple-CPU deployments of Tornado there are better
methods than ``HTTPServer.start()``. See Tornado's guide to
:doc:`tornado:guide/running`.
.. warning:: It is a common mistake to create a new client object for every
request; this comes at a dire performance cost. Create the client
when your application starts and reuse that one client for the lifetime
of the process, as shown in these examples.
Getting a Collection
--------------------
A `collection <https://www.mongodb.com/docs/manual/core/databases-and-collections/>`_
A `collection <http://docs.mongodb.org/manual/reference/glossary/#term-collection>`_
is a group of documents stored in MongoDB, and can be thought of as roughly
the equivalent of a table in a relational database. Getting a
collection in Motor works the same as getting a database:
@ -197,36 +164,73 @@ collection in Motor works the same as getting a database:
.. doctest:: before-inserting-2000-docs
>>> collection = db.test_collection
>>> collection = db["test_collection"]
>>> collection = db['test_collection']
Just like getting a reference to a database, getting a reference to a
collection does no I/O and doesn't require an ``await`` expression.
collection does no I/O and doesn't accept a callback or return a Future.
Inserting a Document
--------------------
As in PyMongo, Motor represents MongoDB documents with Python dictionaries. To
store a document in MongoDB, call :meth:`~MotorCollection.insert_one` in an
``await`` expression:
store a document in MongoDB, call :meth:`~MotorCollection.insert_one` with a
document and a callback:
.. doctest:: before-inserting-2000-docs
>>> async def do_insert():
... document = {"key": "value"}
... result = await db.test_collection.insert_one(document)
... print("result %s" % repr(result.inserted_id))
>>> from tornado.ioloop import IOLoop
>>> def my_callback(result, error):
... print('result %s' % repr(result.inserted_id))
... IOLoop.current().stop()
...
>>>
>>> IOLoop.current().run_sync(do_insert)
>>> document = {'key': 'value'}
>>> db.test_collection.insert_one(document, callback=my_callback)
>>> IOLoop.current().start()
result ObjectId('...')
.. mongodoc:: insert
There are several differences to note between Motor and PyMongo. One is that,
unlike PyMongo's :meth:`~pymongo.collection.Collection.insert_one`, Motor's has no
return value. Another is that ``insert_one`` accepts an optional callback function.
The function must take two arguments and it must be passed to ``insert_one`` as a
keyword argument, like::
db.test_collection.insert_one(document, callback=some_function)
.. warning:: Passing the callback function using the ``callback=`` syntax is
required. (This requirement is a side-effect of the technique Motor uses to
wrap PyMongo.) If you pass the callback as a positional argument instead,
you may see an exception like ``TypeError: method takes exactly 1 argument (2
given)``, or ``TypeError: callable is required``, or some silent misbehavior.
:meth:`insert_one` is *asynchronous*. This means it returns immediately, and
the actual work of inserting the document into the collection is performed in
the background. When it completes, the callback is executed. If the insert
succeeded, the ``result`` parameter is a
:class:`~pymongo.results.InsertOneResult` with the new document's unique id and
the ``error`` parameter is ``None``. If there was an error, ``result`` is
``None`` and ``error`` is an ``Exception`` object. For example, we can trigger
a duplicate-key error by trying to insert two documents with the same unique
id:
.. doctest:: before-inserting-2000-docs
:hide:
>>> # Clean up from previous insert
>>> pymongo.MongoClient().test_database.test_collection.delete_many({})
DeleteResult({'n': 1, 'ok': 1.0}, acknowledged=True)
>>> loop = IOLoop.current()
>>> def my_callback(result, error):
... print('result %s error %s' % (repr(result), repr(error)))
... IOLoop.current().stop()
...
>>> def insert_two_documents():
... db.test_collection.insert_one({'_id': 1}, callback=my_callback)
...
>>> IOLoop.current().add_callback(insert_two_documents)
>>> IOLoop.current().start()
result <pymongo.results.InsertOneResult ...> error None
>>> IOLoop.current().add_callback(insert_two_documents)
>>> IOLoop.current().start()
result None error DuplicateKeyError(...)
The first insert results in ``my_callback`` being called with result 1 and
error ``None``. The second insert triggers ``my_callback`` with result None and
a :class:`~pymongo.errors.DuplicateKeyError`.
A typical beginner's mistake with Motor is to insert documents in a loop,
not waiting for each insert to complete before beginning the next::
@ -239,19 +243,62 @@ not waiting for each insert to complete before beginning the next::
In PyMongo this would insert each document in turn using a single socket, but
Motor attempts to run all the :meth:`insert_one` operations at once. This requires
up to ``max_pool_size`` open sockets connected to MongoDB,
which taxes the client and server. To ensure instead that all inserts run in
sequence, use ``await``:
which taxes the client and server. To ensure instead that all inserts use a
single connection, wait for acknowledgment of each. This is a bit complex using
callbacks:
.. doctest:: before-inserting-2000-docs
>>> async def do_insert():
>>> i = 0
>>> def do_insert(result, error):
... global i
... if error:
... raise error
... i += 1
... if i < 2000:
... db.test_collection.insert_one({'i': i}, callback=do_insert)
... else:
... IOLoop.current().stop()
...
>>> # Start
>>> db.test_collection.insert_one({'i': i}, callback=do_insert)
>>> IOLoop.current().start()
You can simplify this code with ``gen.coroutine``.
Using Motor with `gen.coroutine`
--------------------------------
The :mod:`tornado.gen` module lets you use generators to simplify asynchronous
code. There are two parts to coding with generators:
:func:`coroutine <tornado.gen.coroutine>` and
:class:`~tornado.concurrent.Future`.
First, decorate your generator function with ``@gen.coroutine``:
>>> @gen.coroutine
... def do_insert():
... pass
If you pass no callback to one of Motor's asynchronous methods, it returns a
``Future``. Yield the ``Future`` instance to wait for an operation to complete
and obtain its result:
.. doctest:: before-inserting-2000-docs
>>> @gen.coroutine
... def do_insert():
... for i in range(2000):
... await db.test_collection.insert_one({"i": i})
... future = db.test_collection.insert_one({'i': i})
... result = yield future
...
>>> IOLoop.current().run_sync(do_insert)
In the code above, ``result`` is the ``_id`` of each inserted document.
.. seealso:: :doc:`examples/bulk`.
.. seealso:: :ref:`Detailed example of Motor and gen.coroutine <coroutine-example>`
.. mongodoc:: insert
.. doctest:: before-inserting-2000-docs
@ -259,19 +306,27 @@ sequence, use ``await``:
>>> # Clean up from previous insert
>>> pymongo.MongoClient().test_database.test_collection.delete_many({})
DeleteResult({'n': 2000, 'ok': 1.0}, acknowledged=True)
<pymongo.results.DeleteResult ...>
For better performance, insert documents in large batches with
:meth:`~MotorCollection.insert_many`:
Using native coroutines
-----------------------
Starting in Python 3.5, you can define a `native coroutine`_ with `async def`
instead of the `gen.coroutine` decorator. Within a native coroutine, wait
for an async operation with `await` instead of `yield`:
.. doctest:: before-inserting-2000-docs
>>> async def do_insert():
... result = await db.test_collection.insert_many([{"i": i} for i in range(2000)])
... print("inserted %d docs" % (len(result.inserted_ids),))
... for i in range(2000):
... result = await db.test_collection.insert_one({'i': i})
...
>>> IOLoop.current().run_sync(do_insert)
inserted 2000 docs
Within a native coroutine, the syntax to use Motor with Tornado or asyncio
is often identical.
.. _native coroutine: https://www.python.org/dev/peps/pep-0492/
Getting a Single Document With :meth:`~MotorCollection.find_one`
----------------------------------------------------------------
@ -281,8 +336,9 @@ less than 1:
.. doctest:: after-inserting-2000-docs
>>> async def do_find_one():
... document = await db.test_collection.find_one({"i": {"$lt": 1}})
>>> @gen.coroutine
... def do_find_one():
... document = yield db.test_collection.find_one({'i': {'$lt': 1}})
... pprint.pprint(document)
...
>>> IOLoop.current().run_sync(do_find_one)
@ -301,18 +357,19 @@ are sorted the same in your output as ours.)
Querying for More Than One Document
-----------------------------------
Use :meth:`~MotorCollection.find` to query for a set of documents.
:meth:`~MotorCollection.find` does no I/O and does not require an ``await``
expression. It merely creates an :class:`~MotorCursor` instance. The query is
actually executed on the server when you call :meth:`~MotorCursor.to_list`
or execute an ``async for`` loop.
:meth:`~MotorCollection.find` does no I/O and does not take a callback,
it merely creates a :class:`MotorCursor` instance. The query is actually
executed on the server when you call :meth:`~MotorCursor.to_list` or
:meth:`~MotorCursor.each`, or yield :attr:`~motor.motor_tornado.MotorCursor.fetch_next`.
To find all documents with "i" less than 5:
.. doctest:: after-inserting-2000-docs
>>> async def do_find():
... cursor = db.test_collection.find({"i": {"$lt": 5}}).sort("i")
... for document in await cursor.to_list(length=100):
>>> @gen.coroutine
... def do_find():
... cursor = db.test_collection.find({'i': {'$lt': 5}}).sort('i')
... for document in (yield cursor.to_list(length=100)):
... pprint.pprint(document)
...
>>> IOLoop.current().run_sync(do_find)
@ -322,62 +379,93 @@ To find all documents with "i" less than 5:
{'_id': ObjectId('...'), 'i': 3}
{'_id': ObjectId('...'), 'i': 4}
A ``length`` argument is required when you call ``to_list`` to prevent Motor
from buffering an unlimited number of documents.
A ``length`` argument is required when you call to_list to prevent Motor from
buffering an unlimited number of documents.
``async for``
~~~~~~~~~~~~~
You can handle one document at a time in an ``async for`` loop:
To get one document at a time with :attr:`~motor.motor_tornado.MotorCursor.fetch_next`
and :meth:`~MotorCursor.next_object`:
.. doctest:: after-inserting-2000-docs
>>> async def do_find():
... c = db.test_collection
... async for document in c.find({"i": {"$lt": 2}}):
>>> @gen.coroutine
... def do_find():
... cursor = db.test_collection.find({'i': {'$lt': 5}})
... while (yield cursor.fetch_next):
... document = cursor.next_object()
... pprint.pprint(document)
...
>>> IOLoop.current().run_sync(do_find)
{'_id': ObjectId('...'), 'i': 0}
{'_id': ObjectId('...'), 'i': 1}
{'_id': ObjectId('...'), 'i': 2}
{'_id': ObjectId('...'), 'i': 3}
{'_id': ObjectId('...'), 'i': 4}
You can apply a sort, limit, or skip to a query before you begin iterating:
.. doctest:: after-inserting-2000-docs
>>> async def do_find():
... cursor = db.test_collection.find({"i": {"$lt": 4}})
>>> @gen.coroutine
... def do_find():
... c = db.test_collection
... cursor = c.find({'i': {'$lt': 5}})
... # Modify the query before iterating
... cursor.sort("i", -1).skip(1).limit(2)
... async for document in cursor:
... cursor.sort('i', -1).limit(2).skip(2)
... while (yield cursor.fetch_next):
... document = cursor.next_object()
... pprint.pprint(document)
...
>>> IOLoop.current().run_sync(do_find)
{'_id': ObjectId('...'), 'i': 2}
{'_id': ObjectId('...'), 'i': 1}
The cursor does not actually retrieve each document from the server
``fetch_next`` does not actually retrieve each document from the server
individually; it gets documents efficiently in `large batches`_.
.. _`large batches`: https://www.mongodb.com/docs/manual/core/cursors/#cursor-batches
.. _`large batches`: https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches
Counting Documents
------------------
Use :meth:`~MotorCollection.count_documents` to determine the number of
documents in a collection, or the number of documents that match a query:
`async for`
-----------
In a native coroutine defined with `async def`, replace the while-loop with
`async for`:
.. doctest:: after-inserting-2000-docs
>>> async def do_count():
... n = await db.test_collection.count_documents({})
... print("%s documents in collection" % n)
... n = await db.test_collection.count_documents({"i": {"$gt": 1000}})
... print("%s documents where i > 1000" % n)
>>> async def do_find():
... c = db.test_collection
... async for document in c.find({'i': {'$lt': 2}}):
... pprint.pprint(document)
...
>>> IOLoop.current().run_sync(do_find)
{'_id': ObjectId('...'), 'i': 0}
{'_id': ObjectId('...'), 'i': 1}
This version of the code is dramatically faster.
Counting Documents
------------------
Use :meth:`~MotorCursor.count` to determine the number of documents in
a collection, or the number of documents that match a query:
.. doctest:: after-inserting-2000-docs
>>> @gen.coroutine
... def do_count():
... n = yield db.test_collection.find().count()
... print('%s documents in collection' % n)
... n = yield db.test_collection.find({'i': {'$gt': 1000}}).count()
... print('%s documents where i > 1000' % n)
...
>>> IOLoop.current().run_sync(do_count)
2000 documents in collection
999 documents where i > 1000
:meth:`~MotorCursor.count` uses the *count command* internally; we'll
cover commands_ below.
.. seealso:: `Count command <http://docs.mongodb.org/manual/reference/command/count/>`_
Updating Documents
------------------
@ -388,15 +476,16 @@ replacement document. The query follows the same syntax as for :meth:`find` or
.. doctest:: after-inserting-2000-docs
>>> async def do_replace():
>>> @gen.coroutine
... def do_replace():
... coll = db.test_collection
... old_document = await coll.find_one({"i": 50})
... print("found document: %s" % pprint.pformat(old_document))
... _id = old_document["_id"]
... result = await coll.replace_one({"_id": _id}, {"key": "value"})
... print("replaced %s document" % result.modified_count)
... new_document = await coll.find_one({"_id": _id})
... print("document is now %s" % pprint.pformat(new_document))
... old_document = yield coll.find_one({'i': 50})
... print('found document: %s' % pprint.pformat(old_document))
... _id = old_document['_id']
... result = yield coll.replace_one({'_id': _id}, {'key': 'value'})
... print('replaced %s document' % result.modified_count)
... new_document = yield coll.find_one({'_id': _id})
... print('document is now %s' % pprint.pformat(new_document))
...
>>> IOLoop.current().run_sync(do_replace)
found document: {'_id': ObjectId('...'), 'i': 50}
@ -413,12 +502,13 @@ operator to set "key" to "value":
.. doctest:: after-inserting-2000-docs
>>> async def do_update():
>>> @gen.coroutine
... def do_update():
... coll = db.test_collection
... result = await coll.update_one({"i": 51}, {"$set": {"key": "value"}})
... print("updated %s document" % result.modified_count)
... new_document = await coll.find_one({"i": 51})
... print("document is now %s" % pprint.pformat(new_document))
... result = yield coll.update_one({'i': 51}, {'$set': {'key': 'value'}})
... print('updated %s document' % result.modified_count)
... new_document = yield coll.find_one({'i': 51})
... print('document is now %s' % pprint.pformat(new_document))
...
>>> IOLoop.current().run_sync(do_update)
updated 1 document
@ -429,7 +519,7 @@ operator to set "key" to "value":
:meth:`update_one` only affects the first document it finds, you can
update all of them with :meth:`update_many`::
await coll.update_many({'i': {'$gt': 100}},
yield coll.update_many({'i': {'$gt': 100}},
{'$set': {'key': 'value'}})
.. mongodoc:: update
@ -437,59 +527,48 @@ update all of them with :meth:`update_many`::
Removing Documents
------------------
:meth:`~MotorCollection.delete_one` takes a query with the same syntax as
:meth:`~MotorCollection.find`.
:meth:`delete_one` immediately removes the first returned matching document.
.. doctest:: after-inserting-2000-docs
>>> async def do_delete_one():
... coll = db.test_collection
... n = await coll.count_documents({})
... print("%s documents before calling delete_one()" % n)
... result = await db.test_collection.delete_one({"i": {"$gte": 1000}})
... print("%s documents after" % (await coll.count_documents({})))
...
>>> IOLoop.current().run_sync(do_delete_one)
2000 documents before calling delete_one()
1999 documents after
:meth:`~MotorCollection.delete_many` takes a query with the same syntax as
:meth:`~MotorCollection.find`.
:meth:`delete_many` immediately removes all matching documents.
.. doctest:: after-inserting-2000-docs
>>> async def do_delete_many():
>>> @gen.coroutine
... def do_delete_many():
... coll = db.test_collection
... n = await coll.count_documents({})
... print("%s documents before calling delete_many()" % n)
... result = await db.test_collection.delete_many({"i": {"$gte": 1000}})
... print("%s documents after" % (await coll.count_documents({})))
... n = yield coll.count()
... print('%s documents before calling delete_many()' % n)
... result = yield db.test_collection.delete_many({'i': {'$gte': 1000}})
... print('%s documents after' % (yield coll.count()))
...
>>> IOLoop.current().run_sync(do_delete_many)
1999 documents before calling delete_many()
2000 documents before calling delete_many()
1000 documents after
.. mongodoc:: remove
Commands
--------
All operations on MongoDB are implemented internally as commands. Run them using
the :meth:`~motor.motor_tornado.MotorDatabase.command` method on
:class:`~motor.motor_tornado.MotorDatabase`::
Besides the "CRUD" operations--insert, update, delete, and find--all other
operations on MongoDB are commands. Run them using
the :meth:`~MotorDatabase.command` method on :class:`MotorDatabase`:
.. doctest:: after-inserting-2000-docs
>>> from bson import SON
>>> async def use_distinct_command():
... response = await db.command(SON([("distinct", "test_collection"), ("key", "i")]))
>>> @gen.coroutine
... def use_count_command():
... response = yield db.command(SON([("count", "test_collection")]))
... print('response: %s' % pprint.pformat(response))
...
>>> IOLoop.current().run_sync(use_distinct_command)
>>> IOLoop.current().run_sync(use_count_command)
response: {'n': 1000, 'ok': 1.0...}
Since the order of command parameters matters, don't use a Python dict to pass
the command's parameters. Instead, make a habit of using :class:`bson.SON`,
from the ``bson`` module included with PyMongo.
from the ``bson`` module included with PyMongo::
yield db.command(SON([("distinct", "test_collection"), ("key", "my_key")]))
Many commands have special helper methods, such as
:meth:`~MotorDatabase.create_collection` or
@ -508,4 +587,4 @@ reference to Motor's complete feature set.
Learning to use the MongoDB driver is just the beginning, of course. For
in-depth instruction in MongoDB itself, see `The MongoDB Manual`_.
.. _The MongoDB Manual: https://mongodb.com/docs/manual/
.. _The MongoDB Manual: http://docs.mongodb.org/manual/

426
ez_setup.py Normal file
View File

@ -0,0 +1,426 @@
#!/usr/bin/env python
"""
Setuptools bootstrapping installer.
Maintained at https://github.com/pypa/setuptools/tree/bootstrap.
Run this script to install or upgrade setuptools.
"""
import os
import shutil
import sys
import tempfile
import zipfile
import optparse
import subprocess
import platform
import textwrap
import contextlib
import json
import codecs
from distutils import log
try:
from urllib.request import urlopen
from urllib.parse import urljoin
except ImportError:
from urllib2 import urlopen
from urlparse import urljoin
try:
from site import USER_SITE
except ImportError:
USER_SITE = None
LATEST = object()
DEFAULT_VERSION = LATEST
DEFAULT_URL = "https://pypi.io/packages/source/s/setuptools/"
DEFAULT_SAVE_DIR = os.curdir
def _python_cmd(*args):
"""
Execute a command.
Return True if the command succeeded.
"""
args = (sys.executable,) + args
return subprocess.call(args) == 0
def _install(archive_filename, install_args=()):
"""Install Setuptools."""
with archive_context(archive_filename):
# installing
log.warn('Installing Setuptools')
if not _python_cmd('setup.py', 'install', *install_args):
log.warn('Something went wrong during the installation.')
log.warn('See the error message above.')
# exitcode will be 2
return 2
def _build_egg(egg, archive_filename, to_dir):
"""Build Setuptools egg."""
with archive_context(archive_filename):
# building an egg
log.warn('Building a Setuptools egg in %s', to_dir)
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
# returning the result
log.warn(egg)
if not os.path.exists(egg):
raise IOError('Could not build the egg.')
class ContextualZipFile(zipfile.ZipFile):
"""Supplement ZipFile class to support context manager for Python 2.6."""
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
def __new__(cls, *args, **kwargs):
"""Construct a ZipFile or ContextualZipFile as appropriate."""
if hasattr(zipfile.ZipFile, '__exit__'):
return zipfile.ZipFile(*args, **kwargs)
return super(ContextualZipFile, cls).__new__(cls)
@contextlib.contextmanager
def archive_context(filename):
"""
Unzip filename to a temporary directory, set to the cwd.
The unzipped target is cleaned up after.
"""
tmpdir = tempfile.mkdtemp()
log.warn('Extracting in %s', tmpdir)
old_wd = os.getcwd()
try:
os.chdir(tmpdir)
with ContextualZipFile(filename) as archive:
archive.extractall()
# going in the directory
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
os.chdir(subdir)
log.warn('Now working in %s', subdir)
yield
finally:
os.chdir(old_wd)
shutil.rmtree(tmpdir)
def _do_download(version, download_base, to_dir, download_delay):
"""Download Setuptools."""
py_desig = 'py{sys.version_info[0]}.{sys.version_info[1]}'.format(sys=sys)
tp = 'setuptools-{version}-{py_desig}.egg'
egg = os.path.join(to_dir, tp.format(**locals()))
if not os.path.exists(egg):
archive = download_setuptools(version, download_base,
to_dir, download_delay)
_build_egg(egg, archive, to_dir)
sys.path.insert(0, egg)
# Remove previously-imported pkg_resources if present (see
# https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
if 'pkg_resources' in sys.modules:
_unload_pkg_resources()
import setuptools
setuptools.bootstrap_install_from = egg
def use_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL,
to_dir=DEFAULT_SAVE_DIR, download_delay=15):
"""
Ensure that a setuptools version is installed.
Return None. Raise SystemExit if the requested version
or later cannot be installed.
"""
version = _resolve_version(version)
to_dir = os.path.abspath(to_dir)
# prior to importing, capture the module state for
# representative modules.
rep_modules = 'pkg_resources', 'setuptools'
imported = set(sys.modules).intersection(rep_modules)
try:
import pkg_resources
pkg_resources.require("setuptools>=" + version)
# a suitable version is already installed
return
except ImportError:
# pkg_resources not available; setuptools is not installed; download
pass
except pkg_resources.DistributionNotFound:
# no version of setuptools was found; allow download
pass
except pkg_resources.VersionConflict as VC_err:
if imported:
_conflict_bail(VC_err, version)
# otherwise, unload pkg_resources to allow the downloaded version to
# take precedence.
del pkg_resources
_unload_pkg_resources()
return _do_download(version, download_base, to_dir, download_delay)
def _conflict_bail(VC_err, version):
"""
Setuptools was imported prior to invocation, so it is
unsafe to unload it. Bail out.
"""
conflict_tmpl = textwrap.dedent("""
The required version of setuptools (>={version}) is not available,
and can't be installed while this script is running. Please
install a more recent version first, using
'easy_install -U setuptools'.
(Currently using {VC_err.args[0]!r})
""")
msg = conflict_tmpl.format(**locals())
sys.stderr.write(msg)
sys.exit(2)
def _unload_pkg_resources():
sys.meta_path = [
importer
for importer in sys.meta_path
if importer.__class__.__module__ != 'pkg_resources.extern'
]
del_modules = [
name for name in sys.modules
if name.startswith('pkg_resources')
]
for mod_name in del_modules:
del sys.modules[mod_name]
def _clean_check(cmd, target):
"""
Run the command to download target.
If the command fails, clean up before re-raising the error.
"""
try:
subprocess.check_call(cmd)
except subprocess.CalledProcessError:
if os.access(target, os.F_OK):
os.unlink(target)
raise
def download_file_powershell(url, target):
"""
Download the file at url to target using Powershell.
Powershell will validate trust.
Raise an exception if the command cannot complete.
"""
target = os.path.abspath(target)
ps_cmd = (
"[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
"[System.Net.CredentialCache]::DefaultCredentials; "
'(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")'
% locals()
)
cmd = [
'powershell',
'-Command',
ps_cmd,
]
_clean_check(cmd, target)
def has_powershell():
"""Determine if Powershell is available."""
if platform.system() != 'Windows':
return False
cmd = ['powershell', '-Command', 'echo test']
with open(os.path.devnull, 'wb') as devnull:
try:
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
except Exception:
return False
return True
download_file_powershell.viable = has_powershell
def download_file_curl(url, target):
cmd = ['curl', url, '--location', '--silent', '--output', target]
_clean_check(cmd, target)
def has_curl():
cmd = ['curl', '--version']
with open(os.path.devnull, 'wb') as devnull:
try:
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
except Exception:
return False
return True
download_file_curl.viable = has_curl
def download_file_wget(url, target):
cmd = ['wget', url, '--quiet', '--output-document', target]
_clean_check(cmd, target)
def has_wget():
cmd = ['wget', '--version']
with open(os.path.devnull, 'wb') as devnull:
try:
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
except Exception:
return False
return True
download_file_wget.viable = has_wget
def download_file_insecure(url, target):
"""Use Python to download the file, without connection authentication."""
src = urlopen(url)
try:
# Read all the data in one block.
data = src.read()
finally:
src.close()
# Write all the data in one block to avoid creating a partial file.
with open(target, "wb") as dst:
dst.write(data)
download_file_insecure.viable = lambda: True
def get_best_downloader():
downloaders = (
download_file_powershell,
download_file_curl,
download_file_wget,
download_file_insecure,
)
viable_downloaders = (dl for dl in downloaders if dl.viable())
return next(viable_downloaders, None)
def download_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL,
to_dir=DEFAULT_SAVE_DIR, delay=15,
downloader_factory=get_best_downloader):
"""
Download setuptools from a specified location and return its filename.
`version` should be a valid setuptools version number that is available
as an sdist for download under the `download_base` URL (which should end
with a '/'). `to_dir` is the directory where the egg will be downloaded.
`delay` is the number of seconds to pause before an actual download
attempt.
``downloader_factory`` should be a function taking no arguments and
returning a function for downloading a URL to a target.
"""
version = _resolve_version(version)
# making sure we use the absolute path
to_dir = os.path.abspath(to_dir)
zip_name = "setuptools-%s.zip" % version
url = download_base + zip_name
saveto = os.path.join(to_dir, zip_name)
if not os.path.exists(saveto): # Avoid repeated downloads
log.warn("Downloading %s", url)
downloader = downloader_factory()
downloader(url, saveto)
return os.path.realpath(saveto)
def _resolve_version(version):
"""
Resolve LATEST version
"""
if version is not LATEST:
return version
meta_url = urljoin(DEFAULT_URL, '/pypi/setuptools/json')
resp = urlopen(meta_url)
with contextlib.closing(resp):
try:
charset = resp.info().get_content_charset()
except Exception:
# Python 2 compat; assume UTF-8
charset = 'UTF-8'
reader = codecs.getreader(charset)
doc = json.load(reader(resp))
return str(doc['info']['version'])
def _build_install_args(options):
"""
Build the arguments to 'python setup.py install' on the setuptools package.
Returns list of command line arguments.
"""
return ['--user'] if options.user_install else []
def _parse_args():
"""Parse the command line for options."""
parser = optparse.OptionParser()
parser.add_option(
'--user', dest='user_install', action='store_true', default=False,
help='install in user site package')
parser.add_option(
'--download-base', dest='download_base', metavar="URL",
default=DEFAULT_URL,
help='alternative URL from where to download the setuptools package')
parser.add_option(
'--insecure', dest='downloader_factory', action='store_const',
const=lambda: download_file_insecure, default=get_best_downloader,
help='Use internal, non-validating downloader'
)
parser.add_option(
'--version', help="Specify which version to download",
default=DEFAULT_VERSION,
)
parser.add_option(
'--to-dir',
help="Directory to save (and re-use) package",
default=DEFAULT_SAVE_DIR,
)
options, args = parser.parse_args()
# positional arguments are ignored
return options
def _download_args(options):
"""Return args for download_setuptools function from cmdline args."""
return dict(
version=options.version,
download_base=options.download_base,
downloader_factory=options.downloader_factory,
to_dir=options.to_dir,
)
def main():
"""Install or upgrade setuptools and EasyInstall."""
options = _parse_args()
archive = download_setuptools(**_download_args(options))
return _install(archive, _build_install_args(options))
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,4 +1,4 @@
# Copyright 2011-present MongoDB, Inc.
# Copyright 2011-2015 MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,17 +13,66 @@
# limitations under the License.
"""Motor, an asynchronous driver for MongoDB."""
from ._version import get_version_string, version, version_tuple # noqa: F401
from __future__ import unicode_literals, absolute_import
import pymongo
from motor.motor_py3_compat import text_type
version_tuple = (1, 2, 'dev0')
def get_version_string():
if isinstance(version_tuple[-1], text_type):
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1]
return '.'.join(map(str, version_tuple))
version = get_version_string()
"""Current version of Motor."""
pymongo_required = 3, 4
if pymongo.version_tuple[:2] < pymongo_required:
major, minor = pymongo_required
msg = (
"Motor %s requires PyMongo %s.%s or later. "
"You have PyMongo %s. "
"Do python -m pip install \"pymongo>=%s.%s,<4\""
) % (version,
major, minor,
pymongo.version,
major, minor)
raise ImportError(msg)
# MotorClient runs getaddrinfo on a thread. If the hostname is unicode,
# getaddrinfo attempts to import encodings.idna. If this is done at
# module-import time, perhaps with "loop.run_sync(MotorClient.open)" at
# module scope, the import lock is already held by the main thread, leading to
# AutoReconnect. Avoid it by caching the idna encoder on the main thread now.
#
# Only required in Python 2 and Tornado 3.
#
# See also https://github.com/tornadoweb/tornado/pull/964
u'foo'.encode('idna')
try:
import tornado
except ImportError:
tornado = None # type:ignore[assignment]
tornado = None
else:
# For backwards compatibility with Motor 0.4, export Motor's Tornado classes
# at module root. This may change in the future.
from .motor_tornado import * # noqa: F403
from .motor_tornado import __all__ # noqa: F401
# at module root. This may change in Motor 1.0. First get __all__.
from .motor_tornado import *
# Now some classes that aren't in __all__ but might be expected.
from .motor_tornado import (MotorCollection,
MotorDatabase,
MotorGridFS,
MotorGridFSBucket,
MotorGridIn,
MotorGridOut,
MotorBulkOperationBuilder)
# Make "from motor import *" the same as "from motor.motor_tornado import *"
from .motor_tornado import __all__

View File

@ -1,41 +0,0 @@
# Copyright 2022-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.
"""Version-related data for motor."""
import re
from typing import Union
__version__ = "3.7.2.dev0"
def get_version_tuple(version: str) -> tuple[Union[int, str], ...]:
pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
match = re.match(pattern, version)
if match:
parts: list[Union[int, str]] = [int(match[part]) for part in ["major", "minor", "patch"]]
if match["rest"]:
parts.append(match["rest"])
elif re.match(r"\d+.\d+", version):
parts = [int(part) for part in version.split(".")]
else:
raise ValueError("Could not parse version")
return tuple(parts)
version_tuple = get_version_tuple(__version__)
version = __version__
def get_version_string() -> str:
return __version__

View File

@ -14,21 +14,18 @@
"""Serve GridFS files with Motor and aiohttp.
Requires Python 3.5 or later and aiohttp 3.0 or later.
See the :doc:`/examples/aiohttp_gridfs_example`.
"""
import asyncio
import datetime
import mimetypes
import aiohttp.web
from aiohttp.web_reqrep import StreamResponse
import gridfs
from motor.motor_asyncio import AsyncIOMotorDatabase, AsyncIOMotorGridFSBucket
from motor.motor_gridfs import _hash_gridout
# mypy: disable-error-code="no-untyped-def,no-untyped-call"
from motor.motor_asyncio import (AsyncIOMotorDatabase,
AsyncIOMotorGridFSBucket)
def get_gridfs_file(bucket, filename, request):
@ -108,15 +105,17 @@ def set_extra_headers(response, gridout):
- `gridout`: The :class:`~motor.motor_asyncio.AsyncIOMotorGridOut` we
will serve to the client
"""
pass
def _config_error(request):
try:
formatter = request.match_info.route.resource.get_info()["formatter"]
msg = 'Bad AIOHTTPGridFS route "%s", requires a {filename} variable' % formatter
formatter = request.match_info.route.resource.get_info()['formatter']
msg = ('Bad AIOHTTPGridFS route "%s", requires a {filename} variable' %
formatter)
except (KeyError, AttributeError):
# aiohttp API changed? Fall back to simpler error message.
msg = "Bad AIOHTTPGridFS route for request: %s" % request
msg = ('Bad AIOHTTPGridFS route for request: %s' % request)
raise aiohttp.web.HTTPInternalServerError(text=msg) from None
@ -135,9 +134,9 @@ class AIOHTTPGridFS:
app = aiohttp.web.Application()
# The GridFS URL pattern must have a "{filename}" variable.
resource = app.router.add_resource("/fs/{filename}")
resource.add_route("GET", gridfs_handler)
resource.add_route("HEAD", gridfs_handler)
resource = app.router.add_resource('/fs/{filename}')
resource.add_route('GET', gridfs_handler)
resource.add_route('HEAD', gridfs_handler)
app_handler = app.make_handler()
server = loop.create_server(app_handler, port=80)
@ -153,21 +152,18 @@ class AIOHTTPGridFS:
- `get_cache_time`: Optional override for :func:`get_cache_time`
- `set_extra_headers`: Optional override for :func:`set_extra_headers`
.. _GridFS: https://www.mongodb.com/docs/manual/core/gridfs/
.. _GridFS: https://docs.mongodb.com/manual/core/gridfs/
"""
def __init__(
self,
database,
root_collection="fs",
get_gridfs_file=get_gridfs_file,
get_cache_time=get_cache_time,
set_extra_headers=set_extra_headers,
):
def __init__(self,
database,
root_collection='fs',
get_gridfs_file=get_gridfs_file,
get_cache_time=get_cache_time,
set_extra_headers=set_extra_headers):
if not isinstance(database, AsyncIOMotorDatabase):
raise TypeError(
"First argument to AIOHTTPGridFS must be AsyncIOMotorDatabase, not %r" % database
)
raise TypeError("First argument to AIOHTTPGridFS must be "
"AsyncIOMotorDatabase, not %r" % database)
self._database = database
self._bucket = AsyncIOMotorGridFSBucket(self._database, root_collection)
@ -175,29 +171,27 @@ class AIOHTTPGridFS:
self._get_cache_time = get_cache_time
self._set_extra_headers = set_extra_headers
async def __call__(self, request):
@asyncio.coroutine
def __call__(self, request):
"""Send filepath to client using request."""
try:
filename = request.match_info["filename"]
filename = request.match_info['filename']
except KeyError:
_config_error(request)
if request.method not in ("GET", "HEAD"):
if request.method not in ('GET', 'HEAD'):
raise aiohttp.web.HTTPMethodNotAllowed(
method=request.method, allowed_methods={"GET", "HEAD"}
)
method=request.method, allowed_methods={'GET', 'HEAD'})
try:
gridout = await self._get_gridfs_file(self._bucket, filename, request)
except gridfs.NoFile as e:
raise aiohttp.web.HTTPNotFound(text=request.path) from e
gridout = yield from self._get_gridfs_file(self._bucket,
filename,
request)
except gridfs.NoFile:
raise aiohttp.web.HTTPNotFound(text=request.path)
resp = aiohttp.web.StreamResponse()
# Get the hash for the GridFS file.
checksum = _hash_gridout(gridout)
self._set_standard_headers(request.path, resp, gridout, checksum)
resp = StreamResponse()
self._set_standard_headers(request.path, resp, gridout)
# Overridable method set_extra_headers.
self._set_extra_headers(resp, gridout)
@ -216,23 +210,29 @@ class AIOHTTPGridFS:
# Same for Etag
etag = request.headers.get("If-None-Match")
if etag is not None and etag.strip('"') == checksum:
if etag is not None and etag.strip('"') == gridout.md5:
resp.set_status(304)
return resp
resp.content_length = gridout.length
await resp.prepare(request)
yield from resp.prepare(request)
if request.method == 'GET':
resp.set_tcp_cork(True)
try:
written = 0
while written < gridout.length:
# Reading chunk_size at a time minimizes buffering.
chunk = yield from gridout.read(gridout.chunk_size)
resp.write(chunk)
yield from resp.drain()
written += len(chunk)
finally:
resp.set_tcp_nodelay(True)
if request.method == "GET":
written = 0
while written < gridout.length:
# Reading chunk_size at a time minimizes buffering.
chunk = await gridout.read(gridout.chunk_size)
await resp.write(chunk)
written += len(chunk)
return resp
def _set_standard_headers(self, path, resp, gridout, checksum):
def _set_standard_headers(self, path, resp, gridout):
resp.last_modified = gridout.upload_date
content_type = gridout.content_type
if content_type is None:
@ -241,15 +241,18 @@ class AIOHTTPGridFS:
if content_type:
resp.content_type = content_type
resp.headers["Etag"] = '"%s"' % checksum
# MD5 is calculated on the MongoDB server when GridFS file is created.
resp.headers["Etag"] = '"%s"' % gridout.md5
# Overridable method get_cache_time.
cache_time = self._get_cache_time(path, gridout.upload_date, gridout.content_type)
cache_time = self._get_cache_time(path,
gridout.upload_date,
gridout.content_type)
if cache_time > 0:
resp.headers["Expires"] = (
datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
+ datetime.timedelta(seconds=cache_time)
datetime.datetime.utcnow() +
datetime.timedelta(seconds=cache_time)
).strftime("%a, %d %b %Y %H:%M:%S GMT")
resp.headers["Cache-Control"] = "max-age=" + str(cache_time)

File diff suppressed because it is too large Load Diff

View File

@ -1,889 +0,0 @@
# Copyright 2023-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.
from asyncio import Future
from collections.abc import Callable, Coroutine, Iterable, Mapping, MutableMapping, Sequence
from typing import (
Any,
Generic,
NoReturn,
Optional,
TypeVar,
Union,
overload,
)
import pymongo.common
import pymongo.database
import pymongo.errors
import pymongo.mongo_client
import typing_extensions
from bson import Binary, Code, CodecOptions, DBRef, Timestamp
from bson.codec_options import TypeRegistry
from bson.raw_bson import RawBSONDocument
from pymongo import IndexModel, ReadPreference, WriteConcern
from pymongo.change_stream import ChangeStream
from pymongo.client_options import ClientOptions
from pymongo.client_session import ClientSession, SessionOptions, TransactionOptions
from pymongo.collection import Collection, ReturnDocument # noqa: F401
from pymongo.command_cursor import CommandCursor, RawBatchCommandCursor
from pymongo.cursor import Cursor, RawBatchCursor
from pymongo.cursor_shared import _Hint, _Sort
from pymongo.database import Database
from pymongo.encryption import ClientEncryption, RewrapManyDataKeyResult
from pymongo.encryption_options import RangeOpts
from pymongo.operations import _IndexKeyHint, _IndexList
from pymongo.read_concern import ReadConcern
from pymongo.read_preferences import _ServerMode
from pymongo.results import (
BulkWriteResult,
ClientBulkWriteResult,
DeleteResult,
InsertManyResult,
InsertOneResult,
UpdateResult,
)
from pymongo.synchronous.client_session import _T
from pymongo.synchronous.collection import _WriteOp
from pymongo.topology_description import TopologyDescription
from pymongo.typings import (
_Address,
_CollationIn,
_DocumentType,
_DocumentTypeArg,
_Pipeline,
)
try:
from pymongo.operations import SearchIndexModel
except ImportError:
SearchIndexModel: typing_extensions.TypeAlias = Any # type:ignore[no-redef]
_WITH_TRANSACTION_RETRY_TIME_LIMIT: int
_CodecDocumentType = TypeVar("_CodecDocumentType", bound=Mapping[str, Any])
def _within_time_limit(start_time: float) -> bool: ...
def _max_time_expired_error(exc: Exception) -> bool: ...
class AgnosticBase:
delegate: Any
def __eq__(self, other: object) -> bool: ...
def __init__(self, delegate: Any) -> None: ...
class AgnosticBaseProperties(AgnosticBase, Generic[_DocumentType]):
codec_options: CodecOptions[_DocumentType]
read_preference: _ServerMode
read_concern: ReadConcern
write_concern: WriteConcern
class AgnosticClient(AgnosticBaseProperties[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[pymongo.MongoClient[_DocumentType]]
def address(self) -> Optional[tuple[str, int]]: ...
def arbiters(self) -> set[tuple[str, int]]: ...
def close(self) -> None: ...
def __hash__(self) -> int: ...
async def drop_database(
self,
name_or_database: Union[str, AgnosticDatabase[_DocumentTypeArg]],
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
) -> None: ...
def options(self) -> ClientOptions: ...
def get_database(
self,
name: Optional[str] = None,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AgnosticDatabase[_DocumentType]: ...
def get_default_database(
self,
default: Optional[str] = None,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AgnosticDatabase[_DocumentType]: ...
async def bulk_write(
self,
models: Sequence[_WriteOp[_DocumentType]],
session: Optional[ClientSession] = None,
ordered: bool = True,
verbose_results: bool = False,
bypass_document_validation: Optional[bool] = None,
comment: Optional[Any] = None,
let: Optional[Mapping] = None,
write_concern: Optional[WriteConcern] = None,
) -> ClientBulkWriteResult: ...
HOST: str
def is_mongos(self) -> bool: ...
def is_primary(self) -> bool: ...
async def list_databases(
self,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> AgnosticCommandCursor[dict[str, Any]]: ...
async def list_database_names(
self,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
) -> list[str]: ...
def nodes(self) -> frozenset[_Address]: ...
PORT: int
def primary(self) -> Optional[tuple[str, int]]: ...
read_concern: ReadConcern
def secondaries(self) -> set[tuple[str, int]]: ...
async def server_info(
self, session: Optional[AgnosticClientSession] = None
) -> dict[str, Any]: ...
def topology_description(self) -> TopologyDescription: ...
async def start_session(
self,
causal_consistency: Optional[bool] = None,
default_transaction_options: Optional[TransactionOptions] = None,
snapshot: Optional[bool] = False,
) -> AgnosticClientSession: ...
_io_loop: Optional[Any]
_framework: Any
def __init__(
self,
host: Optional[Union[str, Sequence[str]]] = None,
port: Optional[int] = None,
document_class: Optional[type[_DocumentType]] = None,
tz_aware: Optional[bool] = None,
connect: Optional[bool] = None,
type_registry: Optional[TypeRegistry] = None,
**kwargs: Any,
) -> None: ...
@property
def io_loop(self) -> Any: ...
def get_io_loop(self) -> Any: ...
def watch(
self,
pipeline: Optional[_Pipeline] = None,
full_document: Optional[str] = None,
resume_after: Optional[Mapping[str, Any]] = None,
max_await_time_ms: Optional[int] = None,
batch_size: Optional[int] = None,
collation: Optional[_CollationIn] = None,
start_at_operation_time: Optional[Timestamp] = None,
session: Optional[AgnosticClientSession] = None,
start_after: Optional[Mapping[str, Any]] = None,
comment: Optional[str] = None,
full_document_before_change: Optional[str] = None,
show_expanded_events: Optional[bool] = None,
) -> AgnosticChangeStream[_DocumentType]: ...
def __getattr__(self, name: str) -> AgnosticDatabase[_DocumentType]: ...
def __getitem__(self, name: str) -> AgnosticDatabase[_DocumentType]: ...
def wrap(self, obj: Any) -> Any: ...
class _MotorTransactionContext:
_session: AgnosticClientSession
def __init__(self, session: AgnosticClientSession): ...
async def __aenter__(self) -> _MotorTransactionContext: ...
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
class AgnosticClientSession(AgnosticBase):
__motor_class_name__: str
__delegate_class__: type[ClientSession]
async def commit_transaction(self) -> None: ...
async def abort_transaction(self) -> None: ...
async def end_session(self) -> None: ...
def cluster_time(self) -> Optional[Mapping[str, Any]]: ...
def has_ended(self) -> bool: ...
def in_transaction(self) -> bool: ...
def options(self) -> SessionOptions: ...
def operation_time(self) -> Optional[Timestamp]: ...
def session_id(self) -> Mapping[str, Any]: ...
def advance_cluster_time(self, cluster_time: Mapping[str, Any]) -> None: ...
def advance_operation_time(self, operation_time: Timestamp) -> None: ...
def __init__(self, delegate: ClientSession, motor_client: AgnosticClient): ...
def get_io_loop(self) -> Any: ...
async def with_transaction(
self,
coro: Callable[..., Coroutine[Any, Any, Any]],
read_concern: Optional[ReadConcern] = None,
write_concern: Optional[WriteConcern] = None,
read_preference: Optional[_ServerMode] = None,
max_commit_time_ms: Optional[int] = None,
) -> _T: ...
def start_transaction(
self,
read_concern: Optional[ReadConcern] = None,
write_concern: Optional[WriteConcern] = None,
read_preference: Optional[_ServerMode] = None,
max_commit_time_ms: Optional[int] = None,
) -> _MotorTransactionContext: ...
@property
def client(self) -> AgnosticClient: ...
async def __aenter__(self) -> AgnosticClientSession: ...
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
def __enter__(self) -> None: ...
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
class AgnosticDatabase(AgnosticBaseProperties[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[Database[_DocumentType]]
def __hash__(self) -> int: ...
def __bool__(self) -> int: ...
async def cursor_command(
self,
command: Union[str, MutableMapping[str, Any]],
value: Any = 1,
read_preference: Optional[_ServerMode] = None,
codec_options: Optional[CodecOptions[_CodecDocumentType]] = None,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
max_await_time_ms: Optional[int] = None,
**kwargs: Any,
) -> AgnosticCommandCursor[_DocumentType]: ...
@overload
async def command(
self,
command: Union[str, MutableMapping[str, Any]],
value: Any = ...,
check: bool = ...,
allowable_errors: Optional[Sequence[Union[str, int]]] = ...,
read_preference: Optional[_ServerMode] = ...,
codec_options: None = ...,
session: Optional[AgnosticClientSession] = ...,
comment: Optional[Any] = ...,
**kwargs: Any,
) -> dict[str, Any]: ...
@overload
async def command(
self,
command: Union[str, MutableMapping[str, Any]],
value: Any = 1,
check: bool = True,
allowable_errors: Optional[Sequence[Union[str, int]]] = None,
read_preference: Optional[_ServerMode] = None,
codec_options: CodecOptions[_CodecDocumentType] = ...,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _CodecDocumentType: ...
@overload
async def command(
self,
command: Union[str, MutableMapping[str, Any]],
value: Any = 1,
check: bool = True,
allowable_errors: Optional[Sequence[Union[str, int]]] = None,
read_preference: Optional[_ServerMode] = None,
codec_options: Optional[CodecOptions[_CodecDocumentType]] = None,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> Union[dict[str, Any], _CodecDocumentType]: ...
async def create_collection(
self,
name: str,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
session: Optional[AgnosticClientSession] = None,
check_exists: Optional[bool] = True,
**kwargs: Any,
) -> AgnosticCollection[_DocumentType]: ...
async def dereference(
self,
dbref: DBRef,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> Optional[_DocumentType]: ...
async def drop_collection(
self,
name_or_collection: Union[str, AgnosticCollection[_DocumentTypeArg]],
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
encrypted_fields: Optional[Mapping[str, Any]] = None,
) -> dict[str, Any]: ...
def get_collection(
self,
name: str,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AgnosticCollection[_DocumentType]: ...
async def list_collection_names(
self,
session: Optional[AgnosticClientSession] = None,
filter: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> list[str]: ...
async def list_collections(
self,
session: Optional[AgnosticClientSession] = None,
filter: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> AgnosticCommandCursor[MutableMapping[str, Any]]: ...
@property
def name(self) -> str: ...
async def validate_collection(
self,
name_or_collection: Union[str, AgnosticCollection[_DocumentTypeArg]],
scandata: bool = False,
full: bool = False,
session: Optional[AgnosticClientSession] = None,
background: Optional[bool] = None,
comment: Optional[Any] = None,
) -> dict[str, Any]: ...
def with_options(
self,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AgnosticDatabase[_DocumentType]: ...
async def _async_aggregate(
self, pipeline: _Pipeline, session: Optional[AgnosticClientSession] = None, **kwargs: Any
) -> AgnosticCommandCursor[_DocumentType]: ...
def __init__(self, client: AgnosticClient[_DocumentType], name: str, **kwargs: Any) -> None: ...
def aggregate(
self, pipeline: _Pipeline, *args: Any, **kwargs: Any
) -> AgnosticLatentCommandCursor[_DocumentType]: ...
def watch(
self,
pipeline: Optional[_Pipeline] = None,
full_document: Optional[str] = None,
resume_after: Optional[Mapping[str, Any]] = None,
max_await_time_ms: Optional[int] = None,
batch_size: Optional[int] = None,
collation: Optional[_CollationIn] = None,
start_at_operation_time: Optional[Timestamp] = None,
session: Optional[AgnosticClientSession] = None,
start_after: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
full_document_before_change: Optional[str] = None,
show_expanded_events: Optional[bool] = None,
) -> AgnosticChangeStream[_DocumentType]: ...
@property
def client(self) -> AgnosticClient[_DocumentType]: ...
def __getattr__(self, name: str) -> AgnosticCollection[_DocumentType]: ...
def __getitem__(self, name: str) -> AgnosticCollection[_DocumentType]: ...
def __call__(self, *args: Any, **kwargs: Any) -> None: ...
def wrap(self, obj: Any) -> Any: ...
def get_io_loop(self) -> Any: ...
class AgnosticCollection(AgnosticBaseProperties[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[Collection[_DocumentType]]
def __hash__(self) -> int: ...
def __bool__(self) -> bool: ...
async def bulk_write(
self,
requests: Sequence[_WriteOp[_DocumentType]],
ordered: bool = True,
bypass_document_validation: bool = False,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
let: Optional[Mapping] = None,
) -> BulkWriteResult: ...
async def count_documents(
self,
filter: Mapping[str, Any],
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> int: ...
async def create_index(
self,
keys: _IndexKeyHint,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> str: ...
async def create_indexes(
self,
indexes: Sequence[IndexModel],
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> list[str]: ...
async def delete_many(
self,
filter: Mapping[str, Any],
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
) -> DeleteResult: ...
async def delete_one(
self,
filter: Mapping[str, Any],
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
) -> DeleteResult: ...
async def distinct(
self,
key: str,
filter: Optional[Mapping[str, Any]] = None,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> list[Any]: ...
async def drop(
self,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
encrypted_fields: Optional[Mapping[str, Any]] = None,
) -> None: ...
async def drop_index(
self,
index_or_name: _IndexKeyHint,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> None: ...
async def drop_indexes(
self,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> None: ...
async def estimated_document_count(
self, comment: Optional[Any] = None, **kwargs: Any
) -> int: ...
async def find_one(
self, filter: Optional[Any] = None, *args: Any, **kwargs: Any
) -> Optional[_DocumentType]: ...
async def find_one_and_delete(
self,
filter: Mapping[str, Any],
projection: Optional[Union[Mapping[str, Any], Iterable[str]]] = None,
sort: Optional[_IndexList] = None,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType: ...
async def find_one_and_replace(
self,
filter: Mapping[str, Any],
replacement: Mapping[str, Any],
projection: Optional[Union[Mapping[str, Any], Iterable[str]]] = None,
sort: Optional[_IndexList] = None,
upsert: bool = False,
return_document: bool = ...,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType: ...
async def find_one_and_update(
self,
filter: Mapping[str, Any],
update: Union[Mapping[str, Any], _Pipeline],
projection: Optional[Union[Mapping[str, Any], Iterable[str]]] = None,
sort: Optional[_IndexList] = None,
upsert: bool = False,
return_document: bool = ...,
array_filters: Optional[Sequence[Mapping[str, Any]]] = None,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType: ...
def full_name(self) -> str: ...
async def index_information(
self, session: Optional[AgnosticClientSession] = None, comment: Optional[Any] = None
) -> MutableMapping[str, Any]: ...
async def insert_many(
self,
documents: Iterable[Union[_DocumentType, RawBSONDocument]],
ordered: bool = True,
bypass_document_validation: bool = False,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
) -> InsertManyResult: ...
async def insert_one(
self,
document: Union[_DocumentType, RawBSONDocument],
bypass_document_validation: bool = False,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
) -> InsertOneResult: ...
@property
def name(self) -> str: ...
async def options(
self, session: Optional[AgnosticClientSession] = None, comment: Optional[Any] = None
) -> MutableMapping[str, Any]: ...
async def rename(
self,
new_name: str,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> MutableMapping[str, Any]: ...
async def replace_one(
self,
filter: Mapping[str, Any],
replacement: Mapping[str, Any],
upsert: bool = False,
bypass_document_validation: bool = False,
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
) -> UpdateResult: ...
async def update_many(
self,
filter: Mapping[str, Any],
update: Union[Mapping[str, Any], _Pipeline],
upsert: bool = False,
array_filters: Optional[Sequence[Mapping[str, Any]]] = None,
bypass_document_validation: Optional[bool] = None,
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
) -> UpdateResult: ...
async def update_one(
self,
filter: Mapping[str, Any],
update: Union[Mapping[str, Any], _Pipeline],
upsert: bool = False,
bypass_document_validation: bool = False,
collation: Optional[_CollationIn] = None,
array_filters: Optional[Sequence[Mapping[str, Any]]] = None,
hint: Optional[_IndexKeyHint] = None,
session: Optional[AgnosticClientSession] = None,
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
) -> UpdateResult: ...
def with_options(
self,
codec_options: Optional[CodecOptions] = None,
read_preference: Optional[ReadPreference] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AgnosticCollection[_DocumentType]: ...
def list_search_indexes(
self,
name: Optional[str] = None,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> AgnosticLatentCommandCursor[Mapping[str, Any]]: ...
async def create_search_index(
self,
model: Union[Mapping[str, SearchIndexModel], Any],
session: Optional[AgnosticClientSession] = None,
comment: Any = None,
**kwargs: Any,
) -> str: ...
async def create_search_indexes(
self,
models: list[SearchIndexModel],
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> list[str]: ...
async def drop_search_index(
self,
name: str,
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> None: ...
async def update_search_index(
self,
name: str,
definition: Mapping[str, Any],
session: Optional[AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> None: ...
def __init__(
self,
database: Database[_DocumentType],
name: str,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
_delegate: Any = None,
**kwargs: Any,
) -> None: ...
def __getattr__(self, name: str) -> AgnosticCollection[_DocumentType]: ...
def __getitem__(self, name: str) -> AgnosticCollection[_DocumentType]: ...
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
def find(self, *args: Any, **kwargs: Any) -> AgnosticCursor[_DocumentType]: ...
def find_raw_batches(
self, *args: Any, **kwargs: Any
) -> AgnosticRawBatchCursor[_DocumentType]: ...
def aggregate(
self, pipeline: _Pipeline, *args: Any, **kwargs: Any
) -> AgnosticCommandCursor[_DocumentType]: ...
def aggregate_raw_batches(
self, pipeline: _Pipeline, **kwargs: Any
) -> AgnosticRawBatchCursor[_DocumentType]: ...
def watch(
self,
pipeline: Optional[_Pipeline] = None,
full_document: Optional[str] = None,
resume_after: Optional[Mapping[str, Any]] = None,
max_await_time_ms: Optional[int] = None,
batch_size: Optional[int] = None,
collation: Optional[_CollationIn] = None,
start_at_operation_time: Optional[Timestamp] = None,
session: Optional[AgnosticClientSession] = None,
start_after: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
full_document_before_change: Optional[str] = None,
show_expanded_events: Optional[bool] = None,
) -> Any: ...
def list_indexes(
self, session: Optional[AgnosticClientSession] = None, **kwargs: Any
) -> AgnosticLatentCommandCursor[MutableMapping[str, Any]]: ...
def wrap(self, obj: Any) -> Any: ...
def get_io_loop(self) -> Any: ...
class AgnosticBaseCursor(AgnosticBase, Generic[_DocumentType]):
def __init__(
self,
cursor: Union[
Cursor[_DocumentType], CommandCursor[_DocumentType], _LatentCursor[_DocumentType]
],
collection: AgnosticCollection[_DocumentType],
) -> None: ...
def address(self) -> Optional[_Address]: ...
def cursor_id(self) -> Optional[int]: ...
def alive(self) -> bool: ...
def session(self) -> Optional[AgnosticClientSession]: ...
async def _async_close(self) -> None: ...
async def _refresh(self) -> int: ...
def __aiter__(self) -> Any: ...
async def next(self) -> _DocumentType: ...
__anext__ = next
async def __aenter__(self) -> Any: ...
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> Any: ...
def _get_more(self) -> int: ...
@property
def fetch_next(self) -> Future[Any]: ...
def next_object(self) -> Any: ...
def each(self, callback: Callable) -> None: ...
def _each_got_more(self, callback: Callable, future: Any) -> None: ...
def to_list(self, length: Optional[int] = ...) -> Future[list[_DocumentType]]: ...
def _to_list(
self, length: Union[int, None], the_list: list, future: Any, get_more_result: Any
) -> None: ...
def get_io_loop(self) -> Any: ...
def batch_size(self, batch_size: int) -> AgnosticBaseCursor[_DocumentType]: ...
def _buffer_size(self) -> int: ...
def _query_flags(self) -> Optional[int]: ...
def _data(self) -> None: ...
def _killed(self) -> None: ...
async def close(self) -> None: ...
class AgnosticCursor(AgnosticBaseCursor[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[Cursor]
def collation(self, collation: Optional[_CollationIn]) -> AgnosticCursor[_DocumentType]: ...
async def distinct(self, key: str) -> list: ...
async def explain(self) -> _DocumentType: ...
def add_option(self, mask: int) -> AgnosticCursor[_DocumentType]: ...
def remove_option(self, mask: int) -> AgnosticCursor[_DocumentType]: ...
def limit(self, limit: int) -> AgnosticCursor[_DocumentType]: ...
def skip(self, skip: int) -> AgnosticCursor[_DocumentType]: ...
def max_scan(self, max_scan: Optional[int]) -> AgnosticCursor[_DocumentType]: ...
def sort(
self, key_or_list: _Hint, direction: Optional[Union[int, str]] = None
) -> AgnosticCursor[_DocumentType]: ...
def hint(self, index: Optional[_Hint]) -> AgnosticCursor[_DocumentType]: ...
def where(self, code: Union[str, Code]) -> AgnosticCursor[_DocumentType]: ...
def max_await_time_ms(
self, max_await_time_ms: Optional[int]
) -> AgnosticCursor[_DocumentType]: ...
def max_time_ms(self, max_time_ms: Optional[int]) -> AgnosticCursor[_DocumentType]: ...
def min(self, spec: _Sort) -> AgnosticCursor[_DocumentType]: ...
def max(self, spec: _Sort) -> AgnosticCursor[_DocumentType]: ...
def comment(self, comment: Any) -> AgnosticCursor[_DocumentType]: ...
def allow_disk_use(self, allow_disk_use: bool) -> AgnosticCursor[_DocumentType]: ...
def rewind(self) -> AgnosticCursor[_DocumentType]: ...
def clone(self) -> AgnosticCursor[_DocumentType]: ...
def __copy__(self) -> AgnosticCursor[_DocumentType]: ...
def __deepcopy__(self, memo: Any) -> AgnosticCursor[_DocumentType]: ...
def _query_flags(self) -> int: ...
def _data(self) -> Any: ...
def _killed(self) -> Any: ...
class AgnosticRawBatchCursor(AgnosticCursor[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[RawBatchCursor]
class AgnosticCommandCursor(AgnosticBaseCursor[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[CommandCursor]
def _query_flags(self) -> int: ...
def _data(self) -> Any: ...
def _killed(self) -> Any: ...
class AgnosticRawBatchCommandCursor(AgnosticCommandCursor[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[RawBatchCommandCursor]
class _LatentCursor(Generic[_DocumentType]):
def __init__(self, collection: AgnosticCollection[_DocumentType]): ...
def _end_session(self, *args: Any, **kwargs: Any) -> None: ...
def clone(self) -> _LatentCursor[_DocumentType]: ...
def rewind(self) -> _LatentCursor[_DocumentType]: ...
class AgnosticLatentCommandCursor(AgnosticCommandCursor[_DocumentType]):
__motor_class_name__: str
def __init__(
self, collection: AgnosticCollection[_DocumentType], start: Any, *args: Any, **kwargs: Any
): ...
def _on_started(self, original_future: Any, future: Any) -> None: ...
class AgnosticChangeStream(AgnosticBase, Generic[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[ChangeStream]
async def _close(self) -> None: ...
@property
def resume_token(self) -> Optional[Mapping[str, Any]]: ...
def __init__(
self,
target: Union[
pymongo.MongoClient[_DocumentType], Database[_DocumentType], Collection[_DocumentType]
],
pipeline: Optional[_Pipeline],
full_document: Optional[str],
resume_after: Optional[Mapping[str, Any]],
max_await_time_ms: Optional[int],
batch_size: Optional[int],
collation: Optional[_CollationIn],
start_at_operation_time: Optional[Timestamp],
session: Optional[AgnosticClientSession],
start_after: Optional[Mapping[str, Any]],
comment: Optional[Any] = None,
full_document_before_change: Optional[str] = None,
show_expanded_events: Optional[bool] = None,
): ...
def _lazy_init(self) -> None: ...
def _try_next(self) -> Optional[_DocumentType]: ...
def alive(self) -> bool: ...
async def next(self) -> _DocumentType: ...
async def try_next(self) -> Optional[_DocumentType]: ...
async def close(self) -> None: ...
def __aiter__(self) -> AgnosticChangeStream[_DocumentType]: ...
__anext__ = next
async def __aenter__(self) -> AgnosticChangeStream[_DocumentType]: ...
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
def get_io_loop(self) -> Any: ...
def __enter__(self) -> None: ...
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
class AgnosticClientEncryption(AgnosticBase, Generic[_DocumentType]):
__motor_class_name__: str
__delegate_class__: type[ClientEncryption]
def __init__(
self,
kms_providers: Mapping[str, Any],
key_vault_namespace: str,
key_vault_client: AgnosticClient[_DocumentTypeArg],
codec_options: CodecOptions,
io_loop: Optional[Any] = None,
kms_tls_options: Optional[Mapping[str, Any]] = None,
): ...
async def create_data_key(
self,
kms_provider: str,
master_key: Optional[Mapping[str, Any]] = None,
key_alt_names: Optional[Sequence[str]] = None,
key_material: Optional[bytes] = None,
) -> Binary: ...
async def encrypt(
self,
value: Any,
algorithm: str,
key_id: Optional[Binary] = None,
key_alt_name: Optional[str] = None,
query_type: Optional[str] = None,
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
) -> Binary: ...
async def decrypt(self, value: Binary) -> Any: ...
async def close(self) -> None: ...
async def rewrap_many_data_key(
self,
filter: Mapping[str, Any],
provider: Optional[str] = None,
master_key: Optional[Mapping[str, Any]] = None,
) -> RewrapManyDataKeyResult: ...
async def delete_key(self, id: Binary) -> DeleteResult: ...
async def get_key(self, id: Binary) -> Optional[RawBSONDocument]: ...
async def add_key_alt_name(self, id: Binary, key_alt_name: str) -> Any: ...
async def get_key_by_alt_name(self, key_alt_name: str) -> Optional[RawBSONDocument]: ...
async def remove_key_alt_name(
self, id: Binary, key_alt_name: str
) -> Optional[RawBSONDocument]: ...
async def encrypt_expression(
self,
expression: Mapping[str, Any],
algorithm: str,
key_id: Optional[Binary] = None,
key_alt_name: Optional[str] = None,
query_type: Optional[str] = None,
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
) -> RawBSONDocument: ...
@property
def io_loop(self) -> Any: ...
def get_io_loop(self) -> Any: ...
async def __aenter__(self) -> AgnosticClientEncryption[_DocumentType]: ...
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
def __enter__(self) -> NoReturn: ...
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
async def get_keys(self) -> AgnosticCursor[RawBSONDocument]: ...
async def create_encrypted_collection(
self,
database: AgnosticDatabase[_DocumentTypeArg],
name: str,
encrypted_fields: Mapping[str, Any],
kms_provider: Optional[str] = None,
master_key: Optional[Mapping[str, Any]] = None,
**kwargs: Any,
) -> tuple[AgnosticCollection[_DocumentTypeArg], Mapping[str, Any]]: ...

File diff suppressed because it is too large Load Diff

View File

@ -16,33 +16,26 @@
See "Frameworks" in the Developer Guide.
"""
import asyncio
import asyncio.tasks
import os
import functools
import multiprocessing
import os
import warnings
from asyncio import get_event_loop # noqa: F401 - For framework interface.
from concurrent.futures import ThreadPoolExecutor
# mypy: ignore-errors
try:
import contextvars
from asyncio import ensure_future
except ImportError:
contextvars = None
from asyncio import async as ensure_future
try:
from asyncio import coroutine
except ImportError:
def coroutine():
raise RuntimeError(
"The coroutine decorator was removed in Python 3.11. Use 'async def' instead"
)
CLASS_PREFIX = 'AsyncIO'
CLASS_PREFIX = "AsyncIO"
def get_event_loop():
return asyncio.get_event_loop()
def is_event_loop(loop):
@ -51,79 +44,66 @@ def is_event_loop(loop):
def check_event_loop(loop):
if not is_event_loop(loop):
raise TypeError("io_loop must be instance of asyncio-compatible event loop, not %r" % loop)
raise TypeError(
"io_loop must be instance of asyncio-compatible event loop,"
"not %r" % loop)
def get_future(loop):
return loop.create_future()
return asyncio.Future(loop=loop)
if "MOTOR_MAX_WORKERS" in os.environ:
max_workers = int(os.environ["MOTOR_MAX_WORKERS"])
if 'MOTOR_MAX_WORKERS' in os.environ:
max_workers = int(os.environ['MOTOR_MAX_WORKERS'])
else:
max_workers = multiprocessing.cpu_count() * 5
_EXECUTOR = ThreadPoolExecutor(max_workers=max_workers)
def _reset_global_executor():
"""Re-initialize the global ThreadPoolExecutor"""
global _EXECUTOR # noqa: PLW0603
_EXECUTOR = ThreadPoolExecutor(max_workers=max_workers)
def run_on_executor(loop, fn, self, *args, **kwargs):
# Ensures the wrapped future is resolved on the main thread, though the
# executor's future is resolved on a worker thread.
return asyncio.futures.wrap_future(
_EXECUTOR.submit(functools.partial(fn, self, *args, **kwargs)),
loop=loop)
if hasattr(os, "register_at_fork"):
# We need this to make sure that creating new clients in subprocesses doesn't deadlock.
os.register_at_fork(after_in_child=_reset_global_executor)
_DEFAULT = object()
def run_on_executor(loop, fn, *args, **kwargs):
if contextvars:
context = contextvars.copy_context()
fn = functools.partial(context.run, fn)
return loop.run_in_executor(_EXECUTOR, functools.partial(fn, *args, **kwargs))
# Adapted from tornado.gen.
def chain_future(a, b):
def copy(future):
assert future is a
if b.done():
return
if a.exception() is not None:
b.set_exception(a.exception())
else:
b.set_result(a.result())
a.add_done_callback(copy)
def chain_return_value(future, loop, return_value):
def future_or_callback(future, callback, loop, return_value=_DEFAULT):
"""Compatible way to return a value in all Pythons.
PEP 479, raise StopIteration(value) from a coroutine won't work forever,
but "return value" doesn't work in Python 2. Instead, Motor methods that
return values resolve a Future with it, and are implemented with callbacks
rather than a coroutine internally.
return values either execute a callback with the value or resolve a Future
with it, and are implemented with callbacks rather than a coroutine
internally.
"""
chained = loop.create_future()
if callback:
raise NotImplementedError("Motor with asyncio prohibits callbacks")
def copy(_future):
# Return early if the task was cancelled.
if chained.done():
return
if _future.exception() is not None:
chained.set_exception(_future.exception())
else:
chained.set_result(return_value)
if return_value is _DEFAULT:
return future
future.add_done_callback(functools.partial(loop.call_soon_threadsafe, copy))
chained = asyncio.Future(loop=loop)
def done_callback(_future):
try:
result = _future.result()
chained.set_result(result if return_value is _DEFAULT
else return_value)
except Exception as exc:
chained.set_exception(exc)
future.add_done_callback(functools.partial(loop.call_soon_threadsafe,
done_callback))
return chained
def is_future(f):
return asyncio.isfuture(f)
return isinstance(f, asyncio.Future)
def call_soon(loop, callback, *args, **kwargs):
@ -134,7 +114,11 @@ def call_soon(loop, callback, *args, **kwargs):
def add_future(loop, future, callback, *args):
future.add_done_callback(functools.partial(loop.call_soon_threadsafe, callback, *args))
future.add_done_callback(
functools.partial(loop.call_soon_threadsafe, callback, *args))
coroutine = asyncio.coroutine
def pymongo_class_wrapper(f, pymongo_class):
@ -142,10 +126,10 @@ def pymongo_class_wrapper(f, pymongo_class):
See WrapAsync.
"""
@functools.wraps(f)
async def _wrapper(self, *args, **kwargs):
result = await f(self, *args, **kwargs)
@asyncio.coroutine
def _wrapper(self, *args, **kwargs):
result = yield from f(self, *args, **kwargs)
# Don't call isinstance(), not checking subclasses.
if result.__class__ == pymongo_class:
@ -158,13 +142,5 @@ def pymongo_class_wrapper(f, pymongo_class):
def yieldable(future):
warnings.warn(
"The yieldable function is deprecated and may be removed in a future major release",
DeprecationWarning,
stacklevel=2,
)
# TODO: really explain.
return next(iter(future))
def platform_info():
return "asyncio"

View File

@ -12,29 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tornado compatibility layer for Motor, an asynchronous MongoDB driver.
from __future__ import absolute_import, unicode_literals
See "Frameworks" in the Developer Guide.
"""
"""Tornado compatibility layer for MongoDB, an asynchronous MongoDB driver."""
import functools
import os
import warnings
from concurrent.futures import ThreadPoolExecutor
import tornado.process
from tornado import concurrent, ioloop
from tornado import version as tornado_version
from tornado.gen import chain_future, coroutine # noqa: F401 - For framework interface.
from tornado import concurrent, gen, ioloop
try:
import contextvars
except ImportError:
contextvars = None
from motor.motor_common import callback_type_error
# mypy: ignore-errors
CLASS_PREFIX = ""
CLASS_PREFIX = ''
def get_event_loop():
@ -47,61 +38,92 @@ def is_event_loop(loop):
def check_event_loop(loop):
if not is_event_loop(loop):
raise TypeError("io_loop must be instance of IOLoop, not %r" % loop)
raise TypeError(
"io_loop must be instance of IOLoop, not %r" % loop)
# Beginning in Tornado 4, TracebackFuture is a deprecated alias for Future.
# Future-proof Motor in case TracebackFuture is some day removed. Remove this
# Future-proofing once we drop support for Tornado 3.
try:
_TornadoFuture = concurrent.TracebackFuture
except AttributeError:
_TornadoFuture = concurrent.Future
def get_future(loop):
return concurrent.Future()
return _TornadoFuture()
if "MOTOR_MAX_WORKERS" in os.environ:
max_workers = int(os.environ["MOTOR_MAX_WORKERS"])
if 'MOTOR_MAX_WORKERS' in os.environ:
max_workers = int(os.environ['MOTOR_MAX_WORKERS'])
else:
max_workers = tornado.process.cpu_count() * 5
_EXECUTOR = ThreadPoolExecutor(max_workers=max_workers)
def _reset_global_executor():
"""Re-initialize the global ThreadPoolExecutor"""
global _EXECUTOR # noqa: PLW0603
_EXECUTOR = ThreadPoolExecutor(max_workers=max_workers)
def run_on_executor(loop, fn, self, *args, **kwargs):
# Need a Tornado Future for "await" expressions. exec_fut is resolved on a
# worker thread, loop.add_future ensures "future" is resolved on main.
future = _TornadoFuture()
exec_fut = _EXECUTOR.submit(fn, self, *args, **kwargs)
def copy(_):
if future.done():
return
if exec_fut.exception() is not None:
future.set_exception(exec_fut.exception())
else:
future.set_result(exec_fut.result())
# Ensure copy runs on main thread.
loop.add_future(exec_fut, copy)
return future
_DEFAULT = object()
if hasattr(os, "register_at_fork"):
# We need this to make sure that creating new clients in subprocesses doesn't deadlock.
os.register_at_fork(after_in_child=_reset_global_executor)
def run_on_executor(loop, fn, *args, **kwargs):
if contextvars:
context = contextvars.copy_context()
fn = functools.partial(context.run, fn)
return loop.run_in_executor(_EXECUTOR, functools.partial(fn, *args, **kwargs))
def chain_return_value(future, loop, return_value):
def future_or_callback(future, callback, io_loop, return_value=_DEFAULT):
"""Compatible way to return a value in all Pythons.
PEP 479, raise StopIteration(value) from a coroutine won't work forever,
but "return value" doesn't work in Python 2. Instead, Motor methods that
return values resolve a Future with it, and are implemented with callbacks
rather than a coroutine internally.
return values either execute a callback with the value or resolve a Future
with it, and are implemented with callbacks rather than a coroutine
internally.
"""
chained = concurrent.Future()
if callback:
if not callable(callback):
raise callback_type_error
def copy(_future):
# Return early if the task was cancelled.
if chained.done():
return
if _future.exception() is not None:
chained.set_exception(_future.exception())
else:
chained.set_result(return_value)
# Motor's callback convention is "callback(result, error)".
def done_callback(_future):
try:
result = _future.result()
callback(result if return_value is _DEFAULT else return_value,
None)
except Exception as exc:
callback(None, exc)
future.add_done_callback(functools.partial(loop.add_callback, copy))
return chained
io_loop.add_future(future, done_callback)
elif return_value is not _DEFAULT:
chained = _TornadoFuture()
def done_callback(_future):
try:
_future.result()
except Exception as exc:
chained.set_exception(exc)
else:
chained.set_result(return_value)
io_loop.add_future(future, done_callback)
return chained
else:
return future
def is_future(f):
@ -119,34 +141,53 @@ def add_future(loop, future, callback, *args):
loop.add_future(future, functools.partial(callback, *args))
def coroutine(f):
"""A coroutine that accepts an optional callback.
Given a callback, the function returns None, and the callback is run
with (result, error). Without a callback the function returns a Future.
"""
coro = gen.coroutine(f)
@functools.wraps(f)
def wrapper(*args, **kwargs):
callback = kwargs.pop('callback', None)
if callback and not callable(callback):
raise callback_type_error
future = coro(*args, **kwargs)
if callback:
def _callback(_future):
try:
result = _future.result()
callback(result, None)
except Exception as e:
callback(None, e)
future.add_done_callback(_callback)
else:
return future
return wrapper
def pymongo_class_wrapper(f, pymongo_class):
"""Executes the coroutine f and wraps its result in a Motor class.
See WrapAsync.
"""
@functools.wraps(f)
async def _wrapper(self, *args, **kwargs):
result = await f(self, *args, **kwargs)
@coroutine
def _wrapper(self, *args, **kwargs):
result = yield f(self, *args, **kwargs)
# Don't call isinstance(), not checking subclasses.
if result.__class__ == pymongo_class:
# Delegate to the current object to wrap the result.
return self.wrap(result)
raise gen.Return(self.wrap(result))
else:
return result
raise gen.Return(result)
return _wrapper
def yieldable(future):
warnings.warn(
"The yieldable function is deprecated and may be removed in a future major release.",
DeprecationWarning,
stacklevel=2,
)
# TODO: really explain.
return future
def platform_info():
return f"Tornado {tornado_version}"

View File

@ -12,21 +12,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import unicode_literals, absolute_import
"""Dynamic class-creation for Motor."""
import functools
import inspect
from collections.abc import Callable
from typing import Any, TypeVar
import functools
_class_cache: dict[Any, Any] = {}
from pymongo.cursor import Cursor
# mypy: ignore-errors
from . import motor_py3_compat
_class_cache = {}
def asynchronize(framework, sync_method: Callable, doc=None, wrap_class=None, unwrap_class=None):
"""Decorate `sync_method` so it returns a Future.
def asynchronize(framework, sync_method, doc=None):
"""Decorate `sync_method` so it accepts a callback or returns a Future.
The method runs on a thread and resolves the Future when it completes.
The method runs on a thread and calls the callback or resolves
the Future when the thread completes.
:Parameters:
- `motor_class`: Motor class being created, e.g. MotorClient.
@ -34,101 +38,62 @@ def asynchronize(framework, sync_method: Callable, doc=None, wrap_class=None, un
- `sync_method`: Unbound method of pymongo Collection, Database,
MongoClient, etc.
- `doc`: Optionally override sync_method's docstring
- `wrap_class`: Optional PyMongo class, wrap a returned object of
this PyMongo class in the equivalent Motor class
- `unwrap_class` Optional Motor class name, unwrap an argument with
this Motor class name and pass the wrapped PyMongo
object instead
"""
@functools.wraps(sync_method)
def method(self, *args, **kwargs):
if unwrap_class is not None:
# Don't call isinstance(), not checking subclasses.
unwrapped_args = [
obj.delegate
if obj.__class__.__name__.endswith((unwrap_class, "MotorClientSession"))
else obj
for obj in args
]
unwrapped_kwargs = {
key: (
obj.delegate
if obj.__class__.__name__.endswith((unwrap_class, "MotorClientSession"))
else obj
)
for key, obj in kwargs.items()
}
else:
# For speed, don't call unwrap_args_session/unwrap_kwargs_session.
unwrapped_args = [
obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj
for obj in args
]
unwrapped_kwargs = {
key: (
obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj
)
for key, obj in kwargs.items()
}
loop = self.get_io_loop()
return framework.run_on_executor(
loop, sync_method, self.delegate, *unwrapped_args, **unwrapped_kwargs
)
callback = kwargs.pop('callback', None)
future = framework.run_on_executor(loop,
sync_method,
self.delegate,
*args,
**kwargs)
if wrap_class is not None:
method = framework.pymongo_class_wrapper(method, wrap_class)
method.is_wrap_method = True # For Synchro.
return framework.future_or_callback(future, callback, loop)
# This is for the benefit of motor_extensions.py, which needs this info to
# generate documentation with Sphinx.
method.is_async_method = True
name = sync_method.__name__
method.pymongo_method_name = name
if doc is not None:
method.__doc__ = doc
return method
def unwrap_args_session(args):
return (
obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj
for obj in args
)
def unwrap_kwargs_session(kwargs):
return {
key: (obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj)
for key, obj in kwargs.items()
}
_coro_token = object()
def motor_coroutine(f):
"""Used by Motor classes to mark functions as coroutines.
create_class_with_framework will decorate the function with a framework-
specific coroutine decorator, like asyncio.coroutine or Tornado's
gen.coroutine.
You cannot return a value from a motor_coroutine, the syntax differences
between Tornado on Python 2 and asyncio with Python 3.5 are impossible to
bridge.
"""
f._is_motor_coroutine = _coro_token
return f
def coroutine_annotation(f):
"""In docs, annotate a function that returns a Future with 'coroutine'.
This doesn't affect behavior.
Unlike @motor_coroutine, this doesn't affect behavior.
"""
# Like:
# @coroutine_annotation
# def method(self):
#
f.coroutine_annotation = True
return f
class MotorAttributeFactory:
class MotorAttributeFactory(object):
"""Used by Motor classes to mark attributes that delegate in some way to
PyMongo. At module import time, create_class_with_framework calls
create_attribute() for each attr to create the final class attribute.
"""
def __init__(self, doc=None):
self.doc = doc
@ -138,98 +103,53 @@ class MotorAttributeFactory:
class Async(MotorAttributeFactory):
def __init__(self, attr_name, doc=None):
"""A descriptor that wraps a PyMongo method, such as insert_one,
and returns an asynchronous version of the method that returns a Future.
"""A descriptor that wraps a PyMongo method, such as insert or remove,
and returns an asynchronous version of the method, which accepts a
callback or returns a Future.
:Parameters:
- `attr_name`: The name of the attribute on the PyMongo class, if
different from attribute on the Motor class
"""
super().__init__(doc)
super(Async, self).__init__(doc)
self.attr_name = attr_name
self.wrap_class = None
self.unwrap_class = None
def create_attribute(self, cls, attr_name):
name = self.attr_name or attr_name
method = getattr(cls.__delegate_class__, name)
return asynchronize(
framework=cls._framework,
sync_method=method,
doc=self.doc,
wrap_class=self.wrap_class,
unwrap_class=self.unwrap_class,
)
return asynchronize(framework=cls._framework,
sync_method=method,
doc=self.doc)
def wrap(self, original_class):
self.wrap_class = original_class
return self
return WrapAsync(self, original_class)
def unwrap(self, class_name):
self.unwrap_class = class_name
return self
return Unwrap(self, class_name)
class AsyncRead(Async):
def __init__(self, attr_name=None, doc=None):
"""A descriptor that wraps a PyMongo read method like find_one() that
returns a Future.
class WrapBase(MotorAttributeFactory):
def __init__(self, prop, doc=None):
super(WrapBase, self).__init__(doc)
self.property = prop
class Wrap(WrapBase):
def __init__(self, prop, original_class, doc=None):
"""Calls a synchronous method and wraps the PyMongo class instance it
returns in a Motor class instance.
:Parameters:
- `prop`: A DelegateMethod, the method to call before wrapping its
result in a Motor class.
- `original_class`: A PyMongo class to be wrapped.
"""
Async.__init__(self, attr_name=attr_name, doc=doc)
class AsyncWrite(Async):
def __init__(self, attr_name=None, doc=None):
"""A descriptor that wraps a PyMongo write method like update_one() that
accepts getLastError options and returns a Future.
"""
Async.__init__(self, attr_name=attr_name, doc=doc)
class AsyncCommand(Async):
def __init__(self, attr_name=None, doc=None):
"""A descriptor that wraps a PyMongo command like copy_database() that
returns a Future and does not accept getLastError options.
"""
Async.__init__(self, attr_name=attr_name, doc=doc)
class ReadOnlyProperty(MotorAttributeFactory):
"""Creates a readonly attribute on the wrapped PyMongo object."""
super(Wrap, self).__init__(prop, doc=doc)
self.original_class = original_class
def create_attribute(self, cls, attr_name):
def fget(obj):
return getattr(obj.delegate, attr_name)
if self.doc:
doc = self.doc
else:
doc = getattr(cls.__delegate_class__, attr_name).__doc__
if doc:
return property(fget=fget, doc=doc)
else:
return property(fget=fget)
class DelegateMethod(ReadOnlyProperty):
"""A method on the wrapped PyMongo object that does no I/O and can be called
synchronously"""
def __init__(self, doc=None):
ReadOnlyProperty.__init__(self, doc)
self.wrap_class = None
def wrap(self, original_class):
self.wrap_class = original_class
return self
def create_attribute(self, cls, attr_name):
if self.wrap_class is None:
return ReadOnlyProperty.create_attribute(self, cls, attr_name)
method = getattr(cls.__delegate_class__, attr_name)
original_class = self.wrap_class
original_class = self.original_class
@functools.wraps(method)
def wrapper(self_, *args, **kwargs):
@ -249,9 +169,134 @@ class DelegateMethod(ReadOnlyProperty):
return wrapper
class WrapAsync(WrapBase):
def __init__(self, prop, original_class):
"""Like Async, but before it executes the callback or resolves the
Future, checks if result is a PyMongo class and wraps it in a Motor
class. E.g., Motor's map_reduce should pass a MotorCollection instead
of a PyMongo Collection to the Future. Uses the wrap() method on the
owner object to do the actual wrapping. E.g.,
Database.create_collection returns a Collection, so MotorDatabase has:
create_collection = AsyncCommand().wrap(Collection)
Once Database.create_collection is done, Motor calls
MotorDatabase.wrap() on its result, transforming the result from
Collection to MotorCollection, which is passed to the callback or
Future.
:Parameters:
- `prop`: An Async, the async method to call before wrapping its result
in a Motor class.
- `original_class`: A PyMongo class to be wrapped.
"""
super(WrapAsync, self).__init__(prop)
self.original_class = original_class
def create_attribute(self, cls, attr_name):
async_method = self.property.create_attribute(cls, attr_name)
original_class = self.original_class
wrapper = cls._framework.pymongo_class_wrapper(async_method,
original_class)
if self.doc:
wrapper.__doc__ = self.doc
return wrapper
class Unwrap(WrapBase):
def __init__(self, prop, motor_class_name, doc=None):
"""A descriptor that checks if arguments are Motor classes and unwraps
them. E.g., Motor's drop_database takes a MotorDatabase, unwraps it,
and passes a PyMongo Database instead.
:Parameters:
- `prop`: An Async or DelegateMethod, the method to call with
unwrapped arguments.
- `motor_class_name`: Like 'MotorDatabase' or 'MotorCollection'.
"""
super(Unwrap, self).__init__(prop, doc=doc)
assert isinstance(motor_class_name, motor_py3_compat.text_type)
self.motor_class_name = motor_class_name
def create_attribute(self, cls, attr_name):
f = self.property.create_attribute(cls, attr_name)
name = self.motor_class_name
@functools.wraps(f)
def _f(self, *args, **kwargs):
# Don't call isinstance(), not checking subclasses.
unwrapped_args = [
obj.delegate if obj.__class__.__name__.endswith(name) else obj
for obj in args]
unwrapped_kwargs = dict([
(key, obj.delegate if obj.__class__.__name__ == name else obj)
for key, obj in kwargs.items()])
return f(self, *unwrapped_args, **unwrapped_kwargs)
if self.doc:
_f.__doc__ = self.doc
_f.is_unwrap_method = True # For Synchro.
return _f
class AsyncRead(Async):
def __init__(self, attr_name=None, doc=None):
"""A descriptor that wraps a PyMongo read method like find_one() that
returns a Future.
"""
Async.__init__(self, attr_name=attr_name, doc=doc)
class AsyncWrite(Async):
def __init__(self, attr_name=None, doc=None):
"""A descriptor that wraps a PyMongo write method like update() that
accepts getLastError options and returns a Future.
"""
Async.__init__(self, attr_name=attr_name, doc=doc)
class AsyncCommand(Async):
def __init__(self, attr_name=None, doc=None):
"""A descriptor that wraps a PyMongo command like copy_database() that
returns a Future and does not accept getLastError options.
"""
Async.__init__(self, attr_name=attr_name, doc=doc)
class ReadOnlyProperty(MotorAttributeFactory):
"""Creates a readonly attribute on the wrapped PyMongo object."""
def create_attribute(self, cls, attr_name):
def fget(obj):
return getattr(obj.delegate, attr_name)
if self.doc:
doc = self.doc
else:
doc = getattr(cls.__delegate_class__, attr_name).__doc__
if doc:
return property(fget=fget, doc=doc)
else:
return property(fget=fget)
class DelegateMethod(ReadOnlyProperty):
"""A method on the wrapped PyMongo object that does no I/O and can be called
synchronously"""
def wrap(self, original_class):
return Wrap(self, original_class, doc=self.doc)
def unwrap(self, class_name):
return Unwrap(self, class_name, doc=self.doc)
class MotorCursorChainingMethod(MotorAttributeFactory):
def create_attribute(self, cls, attr_name):
cursor_method = getattr(cls.__delegate_class__, attr_name)
cursor_method = getattr(Cursor, attr_name)
@functools.wraps(cursor_method)
def return_clone(self, *args, **kwargs):
@ -267,21 +312,18 @@ class MotorCursorChainingMethod(MotorAttributeFactory):
return return_clone
T = TypeVar("T")
def create_class_with_framework(cls: T, framework: Any, module_name: str) -> T:
def create_class_with_framework(cls, framework, module_name):
motor_class_name = framework.CLASS_PREFIX + cls.__motor_class_name__
cache_key = (cls, motor_class_name, framework)
cached_class = _class_cache.get(cache_key)
if cached_class:
return cached_class
new_class = type(str(motor_class_name), (cls,), {})
new_class = type(str(motor_class_name), cls.__bases__, cls.__dict__.copy())
new_class.__module__ = module_name
new_class._framework = framework
assert hasattr(new_class, "__delegate_class__")
assert hasattr(new_class, '__delegate_class__')
# If we're constructing MotorClient from AgnosticClient, for example,
# the method resolution order is (AgnosticClient, AgnosticBase, object).
@ -294,5 +336,11 @@ def create_class_with_framework(cls: T, framework: Any, module_name: str) -> T:
new_class_attr = attr.create_attribute(new_class, name)
setattr(new_class, name, new_class_attr)
elif getattr(attr, '_is_motor_coroutine', None) is _coro_token:
coro = framework.coroutine(attr)
del coro._is_motor_coroutine
coro.coroutine_annotation = True
setattr(new_class, name, coro)
_class_cache[cache_key] = new_class
return new_class

View File

@ -13,64 +13,60 @@
# limitations under the License.
"""Asyncio support for Motor, an asynchronous driver for MongoDB."""
__all__ = ['AsyncIOMotorClient']
from . import core, motor_gridfs
from .frameworks import asyncio as asyncio_framework
from .metaprogramming import T, create_class_with_framework
__all__ = [
"AsyncIOMotorClient",
"AsyncIOMotorClientSession",
"AsyncIOMotorDatabase",
"AsyncIOMotorCollection",
"AsyncIOMotorCursor",
"AsyncIOMotorCommandCursor",
"AsyncIOMotorChangeStream",
"AsyncIOMotorGridFSBucket",
"AsyncIOMotorGridIn",
"AsyncIOMotorGridOut",
"AsyncIOMotorGridOutCursor",
"AsyncIOMotorClientEncryption",
]
from .metaprogramming import create_class_with_framework
def create_asyncio_class(cls: T) -> T:
return create_class_with_framework(cls, asyncio_framework, "motor.motor_asyncio")
def create_asyncio_class(cls):
return create_class_with_framework(cls, asyncio_framework, 'motor_asyncio')
AsyncIOMotorClient = create_asyncio_class(core.AgnosticClient)
AsyncIOMotorClientSession = create_asyncio_class(core.AgnosticClientSession)
AsyncIOMotorDatabase = create_asyncio_class(
core.AgnosticDatabase)
AsyncIOMotorDatabase = create_asyncio_class(core.AgnosticDatabase)
AsyncIOMotorCollection = create_asyncio_class(
core.AgnosticCollection)
AsyncIOMotorCollection = create_asyncio_class(core.AgnosticCollection)
AsyncIOMotorCursor = create_asyncio_class(
core.AgnosticCursor)
AsyncIOMotorCursor = create_asyncio_class(core.AgnosticCursor)
AsyncIOMotorCommandCursor = create_asyncio_class(
core.AgnosticCommandCursor)
AsyncIOMotorCommandCursor = create_asyncio_class(core.AgnosticCommandCursor)
AsyncIOMotorLatentCommandCursor = create_asyncio_class(
core.AgnosticLatentCommandCursor)
AsyncIOMotorLatentCommandCursor = create_asyncio_class(core.AgnosticLatentCommandCursor)
AsyncIOMotorBulkOperationBuilder = create_asyncio_class(
core.AgnosticBulkOperationBuilder)
AsyncIOMotorChangeStream = create_asyncio_class(core.AgnosticChangeStream)
AsyncIOMotorGridFS = create_asyncio_class(
motor_gridfs.AgnosticGridFS)
AsyncIOMotorGridFSBucket = create_asyncio_class(motor_gridfs.AgnosticGridFSBucket)
AsyncIOMotorGridFSBucket = create_asyncio_class(
motor_gridfs.AgnosticGridFSBucket)
AsyncIOMotorGridIn = create_asyncio_class(motor_gridfs.AgnosticGridIn)
AsyncIOMotorGridIn = create_asyncio_class(
motor_gridfs.AgnosticGridIn)
AsyncIOMotorGridOut = create_asyncio_class(motor_gridfs.AgnosticGridOut)
AsyncIOMotorGridOut = create_asyncio_class(
motor_gridfs.AgnosticGridOut)
AsyncIOMotorGridOutCursor = create_asyncio_class(motor_gridfs.AgnosticGridOutCursor)
AsyncIOMotorClientEncryption = create_asyncio_class(core.AgnosticClientEncryption)
AsyncIOMotorGridOutCursor = create_asyncio_class(
motor_gridfs.AgnosticGridOutCursor)

View File

@ -1,264 +0,0 @@
from collections.abc import Mapping, MutableMapping
from typing import Any, Optional, Union
from bson import Code, CodecOptions, Timestamp
from bson.raw_bson import RawBSONDocument
from pymongo.client_session import TransactionOptions
from pymongo.cursor_shared import _Hint, _Sort
from pymongo.read_concern import ReadConcern
from pymongo.read_preferences import ReadPreference, _ServerMode
from pymongo.typings import _CollationIn, _DocumentType, _DocumentTypeArg, _Pipeline
from pymongo.write_concern import WriteConcern
from motor import core, motor_gridfs
__all__: list[str] = [
"AsyncIOMotorClient",
"AsyncIOMotorClientSession",
"AsyncIOMotorDatabase",
"AsyncIOMotorCollection",
"AsyncIOMotorCursor",
"AsyncIOMotorCommandCursor",
"AsyncIOMotorChangeStream",
"AsyncIOMotorGridFSBucket",
"AsyncIOMotorGridIn",
"AsyncIOMotorGridOut",
"AsyncIOMotorGridOutCursor",
"AsyncIOMotorClientEncryption",
"AsyncIOMotorLatentCommandCursor",
]
class AsyncIOMotorClient(core.AgnosticClient[_DocumentType]):
def get_database(
self,
name: Optional[str] = None,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AsyncIOMotorDatabase[_DocumentType]: ...
def get_default_database(
self,
default: Optional[str] = None,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AsyncIOMotorDatabase[_DocumentType]: ...
async def list_databases(
self,
session: Optional[core.AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> AsyncIOMotorCommandCursor[dict[str, Any]]: ...
async def start_session(
self,
causal_consistency: Optional[bool] = None,
default_transaction_options: Optional[TransactionOptions] = None,
snapshot: Optional[bool] = False,
) -> AsyncIOMotorClientSession: ...
def watch(
self,
pipeline: Optional[_Pipeline] = None,
full_document: Optional[str] = None,
resume_after: Optional[Mapping[str, Any]] = None,
max_await_time_ms: Optional[int] = None,
batch_size: Optional[int] = None,
collation: Optional[_CollationIn] = None,
start_at_operation_time: Optional[Timestamp] = None,
session: Optional[core.AgnosticClientSession] = None,
start_after: Optional[Mapping[str, Any]] = None,
comment: Optional[str] = None,
full_document_before_change: Optional[str] = None,
show_expanded_events: Optional[bool] = None,
) -> AsyncIOMotorChangeStream[_DocumentType]: ...
def __getattr__(self, name: str) -> AsyncIOMotorDatabase[_DocumentType]: ...
def __getitem__(self, name: str) -> AsyncIOMotorDatabase[_DocumentType]: ...
class AsyncIOMotorClientSession(core.AgnosticClientSession):
@property
def client(self) -> AsyncIOMotorClient: ...
async def __aenter__(self) -> AsyncIOMotorClientSession: ...
class AsyncIOMotorDatabase(core.AgnosticDatabase[_DocumentType]):
async def cursor_command(
self,
command: Union[str, MutableMapping[str, Any]],
value: Any = 1,
read_preference: Optional[_ServerMode] = None,
codec_options: Optional[CodecOptions[core._CodecDocumentType]] = None,
session: Optional[core.AgnosticClientSession] = None,
comment: Optional[Any] = None,
max_await_time_ms: Optional[int] = None,
**kwargs: Any,
) -> AsyncIOMotorCommandCursor[_DocumentType]: ...
async def create_collection(
self,
name: str,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
session: Optional[core.AgnosticClientSession] = None,
check_exists: Optional[bool] = True,
**kwargs: Any,
) -> AsyncIOMotorCollection[_DocumentType]: ...
def get_collection(
self,
name: str,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AsyncIOMotorCollection[_DocumentType]: ...
async def list_collections(
self,
session: Optional[core.AgnosticClientSession] = None,
filter: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> AsyncIOMotorCommandCursor[MutableMapping[str, Any]]: ...
def with_options(
self,
codec_options: Optional[CodecOptions[_DocumentTypeArg]] = None,
read_preference: Optional[_ServerMode] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AsyncIOMotorDatabase[_DocumentType]: ...
def aggregate(
self, pipeline: _Pipeline, *args: Any, **kwargs: Any
) -> AsyncIOMotorLatentCommandCursor[_DocumentType]: ...
def watch(
self,
pipeline: Optional[_Pipeline] = None,
full_document: Optional[str] = None,
resume_after: Optional[Mapping[str, Any]] = None,
max_await_time_ms: Optional[int] = None,
batch_size: Optional[int] = None,
collation: Optional[_CollationIn] = None,
start_at_operation_time: Optional[Timestamp] = None,
session: Optional[core.AgnosticClientSession] = None,
start_after: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
full_document_before_change: Optional[str] = None,
show_expanded_events: Optional[bool] = None,
) -> AsyncIOMotorChangeStream[_DocumentType]: ...
@property
def client(self) -> AsyncIOMotorClient[_DocumentType]: ...
def __getattr__(self, name: str) -> AsyncIOMotorCollection[_DocumentType]: ...
def __getitem__(self, name: str) -> AsyncIOMotorCollection[_DocumentType]: ...
class AsyncIOMotorCollection(core.AgnosticCollection[_DocumentType]):
def with_options(
self,
codec_options: Optional[CodecOptions] = None,
read_preference: Optional[ReadPreference] = None,
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AsyncIOMotorCollection[_DocumentType]: ...
def list_search_indexes(
self,
name: Optional[str] = None,
session: Optional[core.AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> AsyncIOMotorLatentCommandCursor[Mapping[str, Any]]: ...
def __getattr__(self, name: str) -> AsyncIOMotorCollection[_DocumentType]: ...
def __getitem__(self, name: str) -> AsyncIOMotorCollection[_DocumentType]: ...
def find(self, *args: Any, **kwargs: Any) -> AsyncIOMotorCursor[_DocumentType]: ...
def find_raw_batches(
self, *args: Any, **kwargs: Any
) -> AsyncIOMotorRawBatchCursor[_DocumentType]: ...
def aggregate(
self, pipeline: _Pipeline, *args: Any, **kwargs: Any
) -> AsyncIOMotorCommandCursor[_DocumentType]: ...
def aggregate_raw_batches(
self, pipeline: _Pipeline, **kwargs: Any
) -> AsyncIOMotorRawBatchCursor[_DocumentType]: ...
def list_indexes(
self, session: Optional[core.AgnosticClientSession] = None, **kwargs: Any
) -> AsyncIOMotorLatentCommandCursor[MutableMapping[str, Any]]: ...
class AsyncIOMotorLatentCommandCursor(core.AgnosticLatentCommandCursor[_DocumentType]): ...
class AsyncIOMotorCursor(core.AgnosticCursor[_DocumentType]):
def collation(self, collation: Optional[_CollationIn]) -> AsyncIOMotorCursor[_DocumentType]: ...
def add_option(self, mask: int) -> AsyncIOMotorCursor[_DocumentType]: ...
def remove_option(self, mask: int) -> AsyncIOMotorCursor[_DocumentType]: ...
def limit(self, limit: int) -> AsyncIOMotorCursor[_DocumentType]: ...
def skip(self, skip: int) -> AsyncIOMotorCursor[_DocumentType]: ...
def max_scan(self, max_scan: Optional[int]) -> AsyncIOMotorCursor[_DocumentType]: ...
def sort(
self, key_or_list: _Hint, direction: Optional[Union[int, str]] = None
) -> AsyncIOMotorCursor[_DocumentType]: ...
def hint(self, index: Optional[_Hint]) -> AsyncIOMotorCursor[_DocumentType]: ...
def where(self, code: Union[str, Code]) -> AsyncIOMotorCursor[_DocumentType]: ...
def max_await_time_ms(
self, max_await_time_ms: Optional[int]
) -> AsyncIOMotorCursor[_DocumentType]: ...
def max_time_ms(self, max_time_ms: Optional[int]) -> AsyncIOMotorCursor[_DocumentType]: ...
def min(self, spec: _Sort) -> AsyncIOMotorCursor[_DocumentType]: ...
def max(self, spec: _Sort) -> AsyncIOMotorCursor[_DocumentType]: ...
def comment(self, comment: Any) -> AsyncIOMotorCursor[_DocumentType]: ...
def allow_disk_use(self, allow_disk_use: bool) -> AsyncIOMotorCursor[_DocumentType]: ...
def rewind(self) -> AsyncIOMotorCursor[_DocumentType]: ...
def clone(self) -> AsyncIOMotorCursor[_DocumentType]: ...
def __copy__(self) -> AsyncIOMotorCursor[_DocumentType]: ...
def __deepcopy__(self, memo: Any) -> AsyncIOMotorCursor[_DocumentType]: ...
class AsyncIOMotorRawBatchCursor(core.AgnosticRawBatchCursor[_DocumentType]): ...
class AsyncIOMotorCommandCursor(core.AgnosticCommandCursor[_DocumentType]): ...
class AsyncIOMotorRawBatchCommandCursor(core.AgnosticRawBatchCommandCursor[_DocumentType]): ...
class AsyncIOMotorChangeStream(core.AgnosticChangeStream[_DocumentType]):
def __aiter__(self) -> AsyncIOMotorChangeStream[_DocumentType]: ...
async def __aenter__(self) -> AsyncIOMotorChangeStream[_DocumentType]: ...
class AsyncIOMotorClientEncryption(core.AgnosticClientEncryption[_DocumentType]):
async def __aenter__(self) -> AsyncIOMotorClientEncryption[_DocumentType]: ...
async def get_keys(self) -> AsyncIOMotorCursor[RawBSONDocument]: ...
async def create_encrypted_collection(
self,
database: core.AgnosticDatabase[_DocumentTypeArg],
name: str,
encrypted_fields: Mapping[str, Any],
kms_provider: Optional[str] = None,
master_key: Optional[Mapping[str, Any]] = None,
**kwargs: Any,
) -> tuple[AsyncIOMotorCollection[_DocumentTypeArg], Mapping[str, Any]]: ...
class AsyncIOMotorGridOutCursor(motor_gridfs.AgnosticGridOutCursor):
def next_object(self) -> AsyncIOMotorGridOutCursor: ...
class AsyncIOMotorGridOut(motor_gridfs.AgnosticGridOut):
def __aiter__(self) -> AsyncIOMotorGridOut: ...
class AsyncIOMotorGridIn(motor_gridfs.AgnosticGridIn):
async def __aenter__(self) -> AsyncIOMotorGridIn: ...
class AsyncIOMotorGridFSBucket(motor_gridfs.AgnosticGridFSBucket):
async def open_download_stream_by_name(
self,
filename: str,
revision: int = -1,
session: Optional[core.AgnosticClientSession] = None,
) -> AsyncIOMotorGridOut: ...
async def open_download_stream(
self, file_id: Any, session: Optional[core.AgnosticClientSession] = None
) -> AsyncIOMotorGridOut: ...
def open_upload_stream(
self,
filename: str,
chunk_size_bytes: Optional[int] = None,
metadata: Optional[Mapping[str, Any]] = None,
session: Optional[core.AgnosticClientSession] = None,
) -> AsyncIOMotorGridIn: ...
def open_upload_stream_with_id(
self,
file_id: Any,
filename: str,
chunk_size_bytes: Optional[int] = None,
metadata: Optional[Mapping[str, Any]] = None,
session: Optional[core.AgnosticClientSession] = None,
) -> AsyncIOMotorGridIn: ...
def find(self, *args: Any, **kwargs: Any) -> AsyncIOMotorGridOutCursor: ...

View File

@ -12,5 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import unicode_literals, absolute_import
"""Common code to support all async frameworks."""
callback_type_error = TypeError("callback must be a callable")

View File

@ -12,56 +12,113 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import unicode_literals, absolute_import
"""GridFS implementation for Motor, an asynchronous driver for MongoDB."""
import hashlib
import warnings
import textwrap
import gridfs
import pymongo
import pymongo.errors
from gridfs import DEFAULT_CHUNK_SIZE, grid_file
from gridfs import grid_file
from motor import docstrings
from motor.core import AgnosticCollection, AgnosticCursor, AgnosticDatabase
from motor.metaprogramming import (
AsyncCommand,
AsyncRead,
DelegateMethod,
ReadOnlyProperty,
coroutine_annotation,
create_class_with_framework,
)
from motor.core import (AgnosticBaseCursor,
AgnosticCollection,
AgnosticDatabase,
PY35,
PY352)
from motor.docstrings import *
from motor.metaprogramming import (AsyncCommand,
AsyncRead,
coroutine_annotation,
create_class_with_framework,
DelegateMethod,
motor_coroutine,
MotorCursorChainingMethod,
ReadOnlyProperty)
class AgnosticGridOutCursor(AgnosticCursor):
__motor_class_name__ = "MotorGridOutCursor"
class AgnosticGridOutCursor(AgnosticBaseCursor):
__motor_class_name__ = 'MotorGridOutCursor'
__delegate_class__ = gridfs.GridOutCursor
add_option = MotorCursorChainingMethod()
address = ReadOnlyProperty()
collation = ReadOnlyProperty()
comment = MotorCursorChainingMethod()
count = AsyncRead()
distinct = AsyncRead()
explain = AsyncRead()
hint = MotorCursorChainingMethod()
limit = MotorCursorChainingMethod()
max = MotorCursorChainingMethod()
max_await_time_ms = MotorCursorChainingMethod()
max_scan = MotorCursorChainingMethod()
max_time_ms = MotorCursorChainingMethod()
min = MotorCursorChainingMethod()
remove_option = MotorCursorChainingMethod()
skip = MotorCursorChainingMethod()
sort = MotorCursorChainingMethod(doc=cursor_sort_doc)
where = MotorCursorChainingMethod()
# PyMongo's GridOutCursor inherits __die from Cursor.
_Cursor__die = AsyncCommand()
def clone(self):
"""Get a clone of this cursor."""
return self.__class__(self.delegate.clone(), self.collection)
def next_object(self):
"""**DEPRECATED** - Get next GridOut object from cursor."""
# Note: the super() call will raise a warning for the deprecation.
grid_out = super().next_object()
"""Get next GridOut object from cursor."""
grid_out = super(self.__class__, self).next_object()
if grid_out:
grid_out_class = create_class_with_framework(
AgnosticGridOut, self._framework, self.__module__
)
AgnosticGridOut, self._framework, self.__module__)
return grid_out_class(self.collection, delegate=grid_out)
else:
# Exhausted.
return None
def rewind(self):
"""Rewind this cursor to its unevaluated state."""
self.delegate.rewind()
self.started = False
return self
def _empty(self):
return self.delegate._Cursor__empty
def _query_flags(self):
return self.delegate._Cursor__query_flags
def _data(self):
return self.delegate._Cursor__data
def _clear_cursor_id(self):
self.delegate._Cursor__id = 0
def _close_exhaust_cursor(self):
# Exhaust MotorGridOutCursors are prohibited.
pass
def _killed(self):
return self.delegate._Cursor__killed
@motor_coroutine
def _close(self):
yield self._framework.yieldable(self._Cursor__die())
class MotorGridOutProperty(ReadOnlyProperty):
"""Creates a readonly attribute on the wrapped PyMongo GridOut."""
def create_attribute(self, cls, attr_name):
def fget(obj):
if not obj.delegate._file:
raise pymongo.errors.InvalidOperation(
"You must call MotorGridOut.open() before accessing "
"the %s property" % attr_name
)
"the %s property" % attr_name)
return getattr(obj.delegate, attr_name)
@ -69,11 +126,11 @@ class MotorGridOutProperty(ReadOnlyProperty):
return property(fget=fget, doc=doc)
class AgnosticGridOut:
class AgnosticGridOut(object):
"""Class to read data out of GridFS.
MotorGridOut supports the same attributes as PyMongo's
:class:`~pymongo.grid_file.GridOut`, such as ``_id``, ``content_type``,
:class:`~gridfs.grid_file.GridOut`, such as ``_id``, ``content_type``,
etc.
You don't need to instantiate this class directly - use the
@ -81,88 +138,109 @@ class AgnosticGridOut:
instantiated directly, call :meth:`open`, :meth:`read`, or
:meth:`readline` before accessing its attributes.
"""
__motor_class_name__ = "MotorGridOut"
__motor_class_name__ = 'MotorGridOut'
__delegate_class__ = gridfs.GridOut
_id = MotorGridOutProperty()
aliases = MotorGridOutProperty()
chunk_size = MotorGridOutProperty()
close = MotorGridOutProperty()
_ensure_file = AsyncCommand()
_id = MotorGridOutProperty()
aliases = MotorGridOutProperty()
chunk_size = MotorGridOutProperty()
close = MotorGridOutProperty()
content_type = MotorGridOutProperty()
filename = MotorGridOutProperty()
length = MotorGridOutProperty()
metadata = MotorGridOutProperty()
name = MotorGridOutProperty()
_open = AsyncCommand(attr_name="open")
read = AsyncRead()
readable = DelegateMethod()
readchunk = AsyncRead()
readline = AsyncRead()
seek = DelegateMethod()
seekable = DelegateMethod()
tell = DelegateMethod()
upload_date = MotorGridOutProperty()
write = DelegateMethod()
filename = MotorGridOutProperty()
length = MotorGridOutProperty()
md5 = MotorGridOutProperty()
metadata = MotorGridOutProperty()
name = MotorGridOutProperty()
read = AsyncRead()
readchunk = AsyncRead()
readline = AsyncRead()
seek = DelegateMethod()
tell = DelegateMethod()
upload_date = MotorGridOutProperty()
def __init__(
self, root_collection, file_id=None, file_document=None, delegate=None, session=None
self,
root_collection,
file_id=None,
file_document=None,
delegate=None,
):
collection_class = create_class_with_framework(
AgnosticCollection, self._framework, self.__module__
)
AgnosticCollection, self._framework, self.__module__)
if not isinstance(root_collection, collection_class):
raise TypeError(
"First argument to MotorGridOut must be "
"MotorCollection, not %r" % root_collection
)
"MotorCollection, not %r" % root_collection)
if delegate:
self.delegate = delegate
else:
self.delegate = self.__delegate_class__(
root_collection.delegate, file_id, file_document, session=session
)
root_collection.delegate,
file_id,
file_document)
self.io_loop = root_collection.get_io_loop()
def __aiter__(self):
return self
# python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions
if PY352:
exec(textwrap.dedent("""
def __aiter__(self):
return self
async def __anext__(self):
chunk = await self.readchunk()
if chunk:
return chunk
raise StopAsyncIteration()
async def __anext__(self):
chunk = await self.readchunk()
if chunk:
return chunk
raise StopAsyncIteration()
"""), globals(), locals())
elif PY35:
# In Python 3.5.0 and 3.5.1, __aiter__ is a coroutine.
exec(textwrap.dedent("""
async def __aiter__(self):
return self
async def __anext__(self):
chunk = await self.readchunk()
if chunk:
return chunk
raise StopAsyncIteration()
"""), globals(), locals())
def __getattr__(self, item):
if not self.delegate._file:
raise pymongo.errors.InvalidOperation(
"You must call MotorGridOut.open() before accessing the %s property" % item
)
"You must call MotorGridOut.open() before accessing "
"the %s property" % item)
return getattr(self.delegate, item)
@coroutine_annotation
def open(self):
def open(self, callback=None):
"""Retrieve this file's attributes from the server.
Returns a Future.
Takes an optional callback, or returns a Future.
.. versionchanged:: 2.0
No longer accepts a callback argument.
:Parameters:
- `callback`: Optional function taking parameters (self, error)
.. versionchanged:: 0.2
:class:`~motor.MotorGridOut` now opens itself on demand, calling
``open`` explicitly is rarely needed.
"""
return self._framework.chain_return_value(self._open(), self.get_io_loop(), self)
return self._framework.future_or_callback(self._ensure_file(),
callback,
self.get_io_loop(),
self)
def get_io_loop(self):
return self.io_loop
async def stream_to_handler(self, request_handler):
@motor_coroutine
def stream_to_handler(self, request_handler):
"""Write the contents of this file to a
:class:`tornado.web.RequestHandler`. This method calls
:meth:`~tornado.web.RequestHandler.flush` on
@ -176,16 +254,16 @@ class AgnosticGridOut:
@tornado.web.asynchronous
@gen.coroutine
def get(self, filename):
db = self.settings["db"]
fs = await motor.MotorGridFSBucket(db())
db = self.settings['db']
fs = yield motor.MotorGridFSBucket(db())
try:
gridout = await fs.open_download_stream_by_name(filename)
gridout = yield fs.open_download_stream_by_name(filename)
except gridfs.NoFile:
raise tornado.web.HTTPError(404)
self.set_header("Content-Type", gridout.content_type)
self.set_header("Content-Length", gridout.length)
await gridout.stream_to_handler(self)
yield gridout.stream_to_handler(self)
self.finish()
.. seealso:: Tornado `RequestHandler <http://tornadoweb.org/en/stable/web.html#request-handlers>`_
@ -193,7 +271,9 @@ class AgnosticGridOut:
written = 0
while written < self.length:
# Reading chunk_size at a time minimizes buffering.
chunk = await self.read(self.chunk_size)
f = self._framework.yieldable(self.read(self.chunk_size))
yield f
chunk = f.result()
# write() simply appends the output to a list; flush() sends it
# over the network and minimizes buffering in the handler.
@ -202,31 +282,25 @@ class AgnosticGridOut:
written += len(chunk)
class AgnosticGridIn:
__motor_class_name__ = "MotorGridIn"
class AgnosticGridIn(object):
__motor_class_name__ = 'MotorGridIn'
__delegate_class__ = gridfs.GridIn
__getattr__ = DelegateMethod()
_id = ReadOnlyProperty()
abort = AsyncCommand()
chunk_size = ReadOnlyProperty()
closed = ReadOnlyProperty()
close = AsyncCommand()
__getattr__ = DelegateMethod()
abort = AsyncCommand()
closed = ReadOnlyProperty()
close = AsyncCommand()
write = AsyncCommand().unwrap('MotorGridOut')
writelines = AsyncCommand().unwrap('MotorGridOut')
_id = ReadOnlyProperty()
md5 = ReadOnlyProperty()
filename = ReadOnlyProperty()
name = ReadOnlyProperty()
content_type = ReadOnlyProperty()
filename = ReadOnlyProperty()
length = ReadOnlyProperty()
name = ReadOnlyProperty()
read = DelegateMethod()
readable = DelegateMethod()
seekable = DelegateMethod()
upload_date = ReadOnlyProperty()
write = AsyncCommand().unwrap("MotorGridOut")
writeable = DelegateMethod()
writelines = AsyncCommand().unwrap("MotorGridOut")
_exit = AsyncCommand("__exit__")
set = AsyncCommand(
attr_name="__setattr__",
doc="""
length = ReadOnlyProperty()
chunk_size = ReadOnlyProperty()
upload_date = ReadOnlyProperty()
set = AsyncCommand(attr_name='__setattr__', doc="""
Set an arbitrary metadata attribute on the file. Stores value on the server
as a key-value pair within the file document once the file is closed. If
the file is already closed, calling :meth:`set` will immediately update the file
@ -239,23 +313,22 @@ Metadata set on the file appears as attributes on a
- `name`: Name of the attribute, will be stored as a key in the file
document on the server
- `value`: Value of the attribute
""",
)
""")
def __init__(self, root_collection, delegate=None, session=None, **kwargs):
def __init__(self, root_collection, delegate=None, **kwargs):
"""
Class to write data to GridFS. Application developers should not
generally need to instantiate this class - see
:meth:`~motor.MotorGridFSBucket.open_upload_stream`.
Any of the file level options specified in the `GridFS Spec
<http://dochub.mongodb.org/core/gridfs/>`_ may be passed as
<http://dochub.mongodb.org/core/gridfs>`_ may be passed as
keyword arguments. Any additional keyword arguments will be
set as additional fields on the file document. Valid keyword
arguments include:
- ``"_id"``: unique ID for this file (default:
:class:`~pymongo.objectid.ObjectId`) - this ``"_id"`` must
:class:`~bson.objectid.ObjectId`) - this ``"_id"`` must
not have already been used for another file
- ``"filename"``: human name for the file
@ -273,83 +346,210 @@ Metadata set on the file appears as attributes on a
:class:`bytes`.
:Parameters:
- `root_collection`: root collection to write to
- `session` (optional): a
:class:`~pymongo.client_session.ClientSession` to use for all
commands
- `root_collection`: A :class:`~motor.MotorCollection`, the root
collection to write to
- `**kwargs` (optional): file level options (see above)
.. versionchanged:: 3.0
Removed support for the `disable_md5` parameter (to match the
GridIn class in PyMongo).
.. versionchanged:: 0.2
``open`` method removed, no longer needed.
"""
collection_class = create_class_with_framework(
AgnosticCollection, self._framework, self.__module__
)
AgnosticCollection, self._framework, self.__module__)
if not isinstance(root_collection, collection_class):
raise TypeError(
"First argument to MotorGridIn must be MotorCollection, not %r" % root_collection
)
"First argument to MotorGridIn must be "
"MotorCollection, not %r" % root_collection)
self.io_loop = root_collection.get_io_loop()
# Short cut.
self.delegate = delegate or self.__delegate_class__(
root_collection.delegate, session=session, **kwargs
)
if delegate:
# Short cut.
self.delegate = delegate
else:
self.delegate = self.__delegate_class__(
root_collection.delegate,
**kwargs)
# Support "async with bucket.open_upload_stream() as f:"
async def __aenter__(self):
return self
if PY35:
# Support "async with fs.new_file() as f:"
exec(textwrap.dedent("""
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self._exit(exc_type, exc_val, exc_tb)
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
"""), globals(), locals())
def get_io_loop(self):
return self.io_loop
class AgnosticGridFSBucket:
__motor_class_name__ = "MotorGridFSBucket"
class _GFSBase(object):
__delegate_class__ = None
def __init__(self, database, collection="fs"):
db_class = create_class_with_framework(
AgnosticDatabase, self._framework, self.__module__)
if not isinstance(database, db_class):
raise TypeError(
"First argument to %s must be MotorDatabase, not %r" % (
self.__class__, database))
self.io_loop = database.get_io_loop()
self.collection = database[collection]
self.delegate = self.__delegate_class__(
database.delegate,
collection)
def get_io_loop(self):
return self.io_loop
def wrap(self, obj):
if obj.__class__ is grid_file.GridIn:
grid_in_class = create_class_with_framework(
AgnosticGridIn, self._framework, self.__module__)
return grid_in_class(
root_collection=self.collection,
delegate=obj)
elif obj.__class__ is grid_file.GridOut:
grid_out_class = create_class_with_framework(
AgnosticGridOut, self._framework, self.__module__)
return grid_out_class(
root_collection=self.collection,
delegate=obj)
elif obj.__class__ is gridfs.GridOutCursor:
grid_out_class = create_class_with_framework(
AgnosticGridOutCursor, self._framework, self.__module__)
return grid_out_class(
cursor=obj,
collection=self.collection)
class AgnosticGridFS(_GFSBase):
__motor_class_name__ = 'MotorGridFS'
__delegate_class__ = gridfs.GridFS
find_one = AsyncRead().wrap(grid_file.GridOut)
new_file = AsyncRead().wrap(grid_file.GridIn)
get = AsyncRead().wrap(grid_file.GridOut)
get_version = AsyncRead().wrap(grid_file.GridOut)
get_last_version = AsyncRead().wrap(grid_file.GridOut)
list = AsyncRead()
exists = AsyncRead(doc=exists_doc)
delete = AsyncCommand()
put = AsyncCommand()
def __init__(self, database, collection="fs"):
"""**DEPRECATED**: Use :class:`MotorGridFSBucket` or
:class:`AsyncIOMotorGridFSBucket`.
An instance of GridFS on top of a single Database.
:Parameters:
- `database`: a :class:`~motor.MotorDatabase`
- `collection` (optional): A string, name of root collection to use,
such as "fs" or "my_files"
.. mongodoc:: gridfs
.. versionchanged:: 0.2
``open`` method removed; no longer needed.
"""
super(self.__class__, self).__init__(database, collection)
def find(self, *args, **kwargs):
"""Query GridFS for files.
Returns a cursor that iterates across files matching
arbitrary queries on the files collection. Can be combined
with other modifiers for additional control. For example::
cursor = fs.find({"filename": "lisa.txt"}, no_cursor_timeout=True)
while (yield cursor.fetch_next):
grid_out = cursor.next_object()
data = yield grid_out.read()
This iterates through all versions of "lisa.txt" stored in GridFS.
Note that setting no_cursor_timeout may be important to prevent
the cursor from timing out during long multi-file processing work.
As another example, the call::
most_recent_three = fs.find().sort("uploadDate", -1).limit(3)
would return a cursor to the three most recently uploaded files
in GridFS.
:meth:`~motor.MotorGridFS.find` follows a similar
interface to :meth:`~motor.MotorCollection.find`
in :class:`~motor.MotorCollection`.
:Parameters:
- `filter` (optional): a SON object specifying elements which
must be present for a document to be included in the
result set
- `skip` (optional): the number of files to omit (from
the start of the result set) when returning the results
- `limit` (optional): the maximum number of results to
return
- `no_cursor_timeout` (optional): if False (the default), any
returned cursor is closed by the server after 10 minutes of
inactivity. If set to True, the returned cursor will never
time out on the server. Care should be taken to ensure that
cursors with no_cursor_timeout turned on are properly closed.
- `sort` (optional): a list of (key, direction) pairs
specifying the sort order for this query. See
:meth:`~pymongo.cursor.Cursor.sort` for details.
Raises :class:`TypeError` if any of the arguments are of
improper type. Returns an instance of
:class:`~gridfs.grid_file.GridOutCursor`
corresponding to this query.
.. versionchanged:: 1.0
Removed the read_preference, tag_sets, and
secondary_acceptable_latency_ms options.
.. versionadded:: 0.2
.. mongodoc:: find
"""
cursor = self.delegate.find(*args, **kwargs)
grid_out_cursor = create_class_with_framework(
AgnosticGridOutCursor, self._framework, self.__module__)
return grid_out_cursor(cursor, self.collection)
class AgnosticGridFSBucket(_GFSBase):
__motor_class_name__ = 'MotorGridFSBucket'
__delegate_class__ = gridfs.GridFSBucket
delete = AsyncCommand(doc=docstrings.gridfs_delete_doc)
download_to_stream = AsyncCommand(doc=docstrings.gridfs_download_to_stream_doc)
download_to_stream_by_name = AsyncCommand(doc=docstrings.gridfs_download_to_stream_by_name_doc)
open_download_stream = AsyncCommand(doc=docstrings.gridfs_open_download_stream_doc).wrap(
gridfs.GridOut
)
open_download_stream_by_name = AsyncCommand(
doc=docstrings.gridfs_open_download_stream_by_name_doc
).wrap(gridfs.GridOut)
open_upload_stream = DelegateMethod(doc=docstrings.gridfs_open_upload_stream_doc).wrap(
gridfs.GridIn
)
open_upload_stream_with_id = DelegateMethod(
doc=docstrings.gridfs_open_upload_stream_with_id_doc
).wrap(gridfs.GridIn)
rename = AsyncCommand(doc=docstrings.gridfs_rename_doc)
upload_from_stream = AsyncCommand(doc=docstrings.gridfs_upload_from_stream_doc)
upload_from_stream_with_id = AsyncCommand(doc=docstrings.gridfs_upload_from_stream_with_id_doc)
delete = AsyncCommand()
download_to_stream = AsyncCommand()
download_to_stream_by_name = AsyncCommand()
open_download_stream = AsyncCommand().wrap(gridfs.GridOut)
open_download_stream_by_name = AsyncCommand().wrap(gridfs.GridOut)
open_upload_stream = DelegateMethod().wrap(gridfs.GridIn)
open_upload_stream_with_id = DelegateMethod().wrap(gridfs.GridIn)
rename = AsyncCommand()
upload_from_stream = AsyncCommand()
upload_from_stream_with_id = AsyncCommand()
def __init__(
self,
database,
bucket_name="fs",
chunk_size_bytes=DEFAULT_CHUNK_SIZE,
write_concern=None,
read_preference=None,
collection=None,
):
def __init__(self, database, collection="fs"):
"""Create a handle to a GridFS bucket.
Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern`
is not acknowledged.
This class conforms to the `GridFS API Spec
<https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.rst>`_
This class is a replacement for :class:`.MotorGridFS`; it conforms to the
`GridFS API Spec <https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.rst>`_
for MongoDB drivers.
:Parameters:
@ -362,73 +562,12 @@ class AgnosticGridFSBucket:
(the default) db.write_concern is used.
- `read_preference` (optional): The read preference to use. If
``None`` (the default) db.read_preference is used.
- `collection` (optional): Deprecated, an alias for `bucket_name`
that exists solely to provide backwards compatibility.
.. versionchanged:: 3.0
Removed support for the `disable_md5` parameter (to match the
GridFSBucket class in PyMongo).
.. versionchanged:: 2.1
Added support for the `bucket_name`, `chunk_size_bytes`,
`write_concern`, and `read_preference` parameters.
Deprecated the `collection` parameter which is now an alias to
`bucket_name` (to match the GridFSBucket class in PyMongo).
.. versionadded:: 1.0
.. mongodoc:: gridfs
"""
# Preserve backwards compatibility of "collection" parameter
if collection is not None:
warnings.warn(
'the "collection" parameter is deprecated, use "bucket_name" instead',
DeprecationWarning,
stacklevel=2,
)
bucket_name = collection
db_class = create_class_with_framework(AgnosticDatabase, self._framework, self.__module__)
if not isinstance(database, db_class):
raise TypeError(
f"First argument to {self.__class__} must be MotorDatabase, not {database!r}"
)
self.io_loop = database.get_io_loop()
self.collection = database.get_collection(
bucket_name, write_concern=write_concern, read_preference=read_preference
)
self.delegate = self.__delegate_class__(
database.delegate,
bucket_name,
chunk_size_bytes=chunk_size_bytes,
write_concern=write_concern,
read_preference=read_preference,
)
def get_io_loop(self):
return self.io_loop
def wrap(self, obj):
if obj.__class__ is grid_file.GridIn:
grid_in_class = create_class_with_framework(
AgnosticGridIn, self._framework, self.__module__
)
return grid_in_class(root_collection=self.collection, delegate=obj)
elif obj.__class__ is grid_file.GridOut:
grid_out_class = create_class_with_framework(
AgnosticGridOut, self._framework, self.__module__
)
return grid_out_class(root_collection=self.collection, delegate=obj)
elif obj.__class__ is gridfs.GridOutCursor:
grid_out_class = create_class_with_framework(
AgnosticGridOutCursor, self._framework, self.__module__
)
return grid_out_class(cursor=obj, collection=self.collection)
super(self.__class__, self).__init__(database, collection)
def find(self, *args, **kwargs):
"""Find and return the files collection documents that match ``filter``.
@ -440,9 +579,9 @@ class AgnosticGridFSBucket:
For example::
cursor = bucket.find({"filename": "lisa.txt"}, no_cursor_timeout=True)
while (await cursor.fetch_next):
while (yield cursor.fetch_next):
grid_out = cursor.next_object()
data = await grid_out.read()
data = yield grid_out.read()
This iterates through all versions of "lisa.txt" stored in GridFS.
Note that setting no_cursor_timeout to True may be important to
@ -472,33 +611,9 @@ class AgnosticGridFSBucket:
returning.
- `sort` (optional): The order by which to sort results. Defaults to
None.
- `session` (optional): a
:class:`~pymongo.client_session.ClientSession`, created with
:meth:`~MotorClient.start_session`.
If a :class:`~pymongo.client_session.ClientSession` is passed to
:meth:`find`, all returned :class:`MotorGridOut` instances
are associated with that session.
.. versionchanged:: 1.2
Added session parameter.
"""
cursor = self.delegate.find(*args, **kwargs)
grid_out_cursor = create_class_with_framework(
AgnosticGridOutCursor, self._framework, self.__module__
)
AgnosticGridOutCursor, self._framework, self.__module__)
return grid_out_cursor(cursor, self.collection)
def _hash_gridout(gridout):
"""Compute the effective hash of a GridOut object for use with an Etag header.
Create a FIPS-compliant Etag HTTP header hash using sha256
We use the _id + length + upload_date as a proxy for
uniqueness to avoid reading the entire file.
"""
grid_hash = hashlib.sha256(str(gridout._id).encode("utf8"))
grid_hash.update(str(gridout.length).encode("utf8"))
grid_hash.update(str(gridout.upload_date).encode("utf8"))
return grid_hash.hexdigest()

View File

@ -1,182 +0,0 @@
# Copyright 2023-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 datetime
import os
from collections.abc import Iterable, Mapping
from typing import Any, NoReturn, Optional
from bson import ObjectId
from gridfs import DEFAULT_CHUNK_SIZE, GridFSBucket, GridIn, GridOut, GridOutCursor # noqa: F401
from pymongo import WriteConcern
from pymongo.read_preferences import _ServerMode
from motor.core import (
AgnosticClientSession,
AgnosticCollection,
AgnosticCursor,
AgnosticDatabase,
)
_SEEK_SET = os.SEEK_SET
_SEEK_CUR = os.SEEK_CUR
_SEEK_END = os.SEEK_END
class AgnosticGridOutCursor(AgnosticCursor):
__motor_class_name__: str
__delegate_class__: type[GridOutCursor]
def next_object(self) -> AgnosticGridOutCursor: ...
class AgnosticGridOut:
__motor_class_name__: str
__delegate_class__: type[GridOut]
_id: Any
aliases: Optional[list[str]]
chunk_size: int
filename: Optional[str]
name: Optional[str]
content_type: Optional[str]
length: int
upload_date: datetime.datetime
metadata: Optional[Mapping[str, Any]]
async def _open(self) -> None: ...
def close(self) -> None: ...
async def read(self, size: int = -1) -> NoReturn: ...
def readable(self) -> bool: ...
async def readchunk(self) -> bytes: ...
async def readline(self, size: int = -1) -> bytes: ...
def seek(self, pos: int, whence: int = ...) -> int: ...
def seekable(self) -> bool: ...
def tell(self) -> int: ...
def write(self, data: Any) -> None: ...
def __init__(
self,
root_collection: AgnosticCollection,
file_id: Optional[int] = None,
file_document: Optional[Any] = None,
delegate: Any = None,
session: Optional[AgnosticClientSession] = None,
) -> None: ...
def __aiter__(self) -> AgnosticGridOut: ...
async def __anext__(self) -> bytes: ...
def __getattr__(self, item: str) -> Any: ...
def open(self) -> Any: ...
def get_io_loop(self) -> Any: ...
async def stream_to_handler(self, request_handler: Any) -> None: ...
class AgnosticGridIn:
__motor_class_name__: str
__delegate_class__: type[GridIn]
__getattr__: Any
_id: Any
filename: str
name: str
content_type: Optional[str]
length: int
chunk_size: int
upload_date: datetime.datetime
async def abort(self) -> None: ...
def closed(self) -> bool: ...
async def close(self) -> None: ...
def read(self, size: int = -1) -> NoReturn: ...
def readable(self) -> bool: ...
def seekable(self) -> bool: ...
async def write(self, data: Any) -> None: ...
def writeable(self) -> bool: ...
async def writelines(self, sequence: Iterable[Any]) -> None: ...
async def _exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> Any: ...
async def set(self, name: str, value: Any) -> None: ...
def __init__(
self,
root_collection: AgnosticCollection,
delegate: Any = None,
session: Optional[AgnosticClientSession] = None,
**kwargs: Any,
) -> None: ...
async def __aenter__(self) -> AgnosticGridIn: ...
async def __aexit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
def get_io_loop(self) -> Any: ...
class AgnosticGridFSBucket:
__motor_class_name__: str
__delegate_class__: type[GridFSBucket]
async def delete(
self, file_id: Any, session: Optional[AgnosticClientSession] = None
) -> None: ...
async def download_to_stream(
self, file_id: Any, destination: Any, session: Optional[AgnosticClientSession] = None
) -> None: ...
async def download_to_stream_by_name(
self,
filename: str,
destination: Any,
revision: int = -1,
session: Optional[AgnosticClientSession] = None,
) -> None: ...
async def open_download_stream_by_name(
self, filename: str, revision: int = -1, session: Optional[AgnosticClientSession] = None
) -> AgnosticGridOut: ...
async def open_download_stream(
self, file_id: Any, session: Optional[AgnosticClientSession] = None
) -> AgnosticGridOut: ...
def open_upload_stream(
self,
filename: str,
chunk_size_bytes: Optional[int] = None,
metadata: Optional[Mapping[str, Any]] = None,
session: Optional[AgnosticClientSession] = None,
) -> AgnosticGridIn: ...
def open_upload_stream_with_id(
self,
file_id: Any,
filename: str,
chunk_size_bytes: Optional[int] = None,
metadata: Optional[Mapping[str, Any]] = None,
session: Optional[AgnosticClientSession] = None,
) -> AgnosticGridIn: ...
async def rename(
self, file_id: Any, new_filename: str, session: Optional[AgnosticClientSession] = None
) -> None: ...
async def upload_from_stream(
self,
filename: str,
source: Any,
chunk_size_bytes: Optional[int] = None,
metadata: Optional[Mapping[str, Any]] = None,
session: Optional[AgnosticClientSession] = None,
) -> ObjectId: ...
async def upload_from_stream_with_id(
self,
file_id: Any,
filename: str,
source: Any,
chunk_size_bytes: Optional[int] = None,
metadata: Optional[Mapping[str, Any]] = None,
session: Optional[AgnosticClientSession] = None,
) -> None: ...
def __init__(
self,
database: AgnosticDatabase,
bucket_name: str = "fs",
chunk_size_bytes: int = ...,
write_concern: Optional[WriteConcern] = None,
read_preference: Optional[_ServerMode] = None,
collection: Optional[str] = None,
) -> None: ...
def get_io_loop(self) -> Any: ...
def wrap(self, obj: Any) -> Any: ...
def find(self, *args: Any, **kwargs: Any) -> AgnosticGridOutCursor: ...
def _hash_gridout(gridout: AgnosticGridOut) -> str: ...

Some files were not shown because too many files have changed in this diff Show More