Compare commits

..

7 Commits
master ... v2.5

Author SHA1 Message Date
Steven Silvester
0cbd45f581
MOTOR-902 [v2.5] Since Python 3.10 asyncio.get_event_loop() is deprecated (#159) 2022-04-21 12:50:37 -05:00
Shane Harvey
88119c849c MOTOR-911 Skip version API test for count (#152)
(cherry picked from commit 57dd5773a3)
2022-03-18 15:54:00 -07:00
Steven Silvester
057b1afa06
MOTOR-906 [v2.5] Doc Build and Test are Failing (#151) 2022-03-16 17:05:39 -05:00
Amin Alaee
1f6a3d692c MOTOR-857 Test Python 3.10 support
(cherry picked from commit 66fde7189d)
2022-03-15 13:52:22 -07:00
Shane Harvey
aac3bc527f BUMP 2.5.2.dev0 2022-03-15 13:50:52 -07:00
Shane Harvey
fd4eceddae MOTOR-852 Use https:// instead of unauthenticated git:// for git clone
(cherry picked from commit e4b0591538)
2022-03-15 13:49:51 -07:00
Steven Silvester
5ded4425fb
MOTOR-884 [v2.5] Mirror all PyMongo extras (#149)
Co-authored-by: Tushar Singh <tusharvickey1999@gmail.com>
2022-03-09 16:10:42 -06:00
166 changed files with 7154 additions and 11242 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,8 @@
# 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"
@ -25,6 +23,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,19 @@
#!/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"
# TOX_BINARY Path to tox executable
# INSTALL_TOX Whether to install tox in a virtualenv
# PYTHON_BINARY Path to python
# VIRTUALENV Path to virtualenv script
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 +24,24 @@ if [ "$SSL" != "nossl" ]; then
export CA_PEM="$DRIVERS_TOOLS/.evergreen/x509gen/ca.pem"
fi
if [ -f secrets-export.sh ]; then
source secrets-export.sh
if [ "$TOX_ENV" = "synchro37" ]; then
SETUP_ARGS="-- --check-exclude-patterns"
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"
if [ "${INSTALL_TOX}" = "true" ]; then
$VIRTUALENV motorenv
set +o xtrace
if [ -f motorenv/bin/activate ]; then
source motorenv/bin/activate
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
# Windows.
ls -l motorenv
source motorenv/Scripts/activate
fi
set -o xtrace
pip install tox>=3.18
TOX_BINARY=tox
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} ${SETUP_ARGS} "$@"

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 +1,5 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v3.4.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"]
- id: debug-statements

View File

@ -11,13 +11,9 @@ sphinx:
# Set the version of Python and requirements required to build the docs.
python:
version: 3.7
install:
# Install motor itself.
- method: pip
path: .
- requirements: requirements/docs.txt
build:
os: ubuntu-22.04
tools:
python: "3.11"
- requirements: doc/docs-requirements.txt

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 :)

48
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,48 @@
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`_ 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 3.9, and run::
> tox -e asyncio-py39
The doctests pass with Python 3.7 and a MongoDB 5.0 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.

179
README.rst Normal file
View File

@ -0,0 +1,179 @@
=====
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.
:Documentation: Available at `motor.readthedocs.io <https://motor.readthedocs.io/en/stable/>`_
:Author: A\. Jesse Jiryu Davis
About
=====
Motor presents a coroutine-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*
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 `MongoDB Community
Forums <https://developer.mongodb.com/community/forums/tag/motor-driver>`_.
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::
$ python -c "import sys; print(sys.version)"
- The exact version of Motor used, with patch level::
$ python -c "import motor; print(motor.version)"
- The exact version of PyMongo used, with patch level::
$ 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, ...)
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
<http://docs.mongodb.org/manual/tutorial/create-a-vulnerability-report>`_.
Installation
============
Motor can be installed with `pip <http://pypi.python.org/pypi/pip>`_::
$ 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_ >=3.12,<4
* Python 3.5+
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 authentication requires ``aws`` extra dependency::
$ pip install "motor[aws]"
Support for mongodb+srv:// URIs requires ``srv`` extra dependency::
$ pip install "motor[srv]"
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 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.
Examples
========
See the `examples on ReadTheDocs <https://motor.readthedocs.io/en/stable/examples/index.html>`_.
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``.
Testing
=======
Run ``python setup.py test``.
Tests are located in the ``test/`` directory.
.. _PyMongo: http://pypi.python.org/pypi/pymongo/
.. _MongoDB: http://mongodb.org/
.. _Tornado: http://tornadoweb.org/
.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _aiohttp: https://github.com/aio-libs/aiohttp
.. _ReadTheDocs: https://motor.readthedocs.io/en/stable/
.. _sphinx: http://sphinx.pocoo.org/

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."

68
RELEASE.rst Normal file
View File

@ -0,0 +1,68 @@
==============
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>`_.
#. Motor is tested on Evergreen. Ensure that the latest commit is passing CI as
expected: https://evergreen.mongodb.com/waterfall/motor.
#. Check JIRA to ensure all the tickets in this version have been completed.
#. 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.
#. Search and replace the `devN` version number w/ the new version number (see
note above in `Versioning`_). Make sure version number is updated in
`setup.py` and `motor/__init__.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
#. Build the release packages by running the `release.sh`
script on macOS::
$ git clone git@github.com:mongodb/motor.git
$ cd motor
$ git checkout "<release version number>"
$ ./release.sh
This will create the following distributions::
$ ls dist
motor-<version>.tar.gz
motor-<version>-py3-none-any.whl
#. Upload all the release packages to PyPI with twine::
$ python3 -m twine upload dist/*
#. Trigger a build of the docs on https://readthedocs.org/.
#. Announce!

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,366 @@ 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
:members:
.. 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 = fs.open_upload_stream(
"test_file", 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.open_upload_stream(
"test_file", metadata={"contentType": "text/plain"}) 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 = fs.open_upload_stream_with_id(
ObjectId(), "test_file",
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!",
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!",
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:: AsyncIOMotorGridIn
:members:

View File

@ -1,12 +1,6 @@
: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

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,12 +1,6 @@
: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

View File

@ -1,11 +1,5 @@
: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

@ -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,12 +1,6 @@
: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

View File

@ -1,12 +1,6 @@
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
@ -23,3 +17,4 @@ Motor asyncio API
This page describes using Motor with asyncio. For Tornado integration, see
:doc:`../api-tornado/index`.

View File

@ -1,12 +1,6 @@
: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

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,380 @@ Store blobs of data in `GridFS <http://dochub.mongodb.org/core/gridfs>`_.
.. seealso:: :doc:`web`
.. autoclass:: MotorGridFSBucket
:members:
.. 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))
Delete a file's metadata and data chunks from a GridFS bucket::
async def delete():
my_db = MotorClient().test
fs = MotorGridFSBucket(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.
Returns a Future.
.. 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 = MotorClient().test
fs = MotorGridFSBucket(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`.
Returns a Future.
.. 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 = MotorClient().test
fs = MotorGridFSBucket(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:`~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.
Returns a Future.
.. 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 = MotorClient().test
fs = MotorGridFSBucket(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 Future that resolves to a :class:`MotorGridOut`.
.. 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 = MotorClient().test
fs = MotorGridFSBucket(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 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::
async def upload():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
grid_in = 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:`MotorGridIn`.
Raises :exc:`~gridfs.errors.NoFile` if no such version of
that file exists.
Raises :exc:`~ValueError` if `filename` is not a string.
Using the "async with" statement calls :meth:`~MotorGridIn.close`
automatically::
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:
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::
async def upload():
my_db = MotorClient().test
fs = MotorGridFSBucket(my_db)
grid_in = 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:`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))
Renames the stored file with the specified file_id.
For example::
async def rename():
my_db = MotorClient().test
fs = MotorGridFSBucket(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.
Returns a Future.
.. 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 = MotorClient().test
fs = MotorGridFSBucket(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:`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.
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))
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 = MotorClient().test
fs = MotorGridFSBucket(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:`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.
Returns a Future.
.. autoclass:: MotorGridIn
:members:

View File

@ -1,12 +1,6 @@
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

View File

@ -1,12 +1,6 @@
: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

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 +1,7 @@
: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:
:members:

View File

@ -1,12 +1,6 @@
: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

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,114 @@
The :class:`MotorDatabase` that this
:class:`MotorCollection` 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
Returns a Future.
.. 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)
Returns a Future.
.. _map reduce command: http://docs.mongodb.org/manual/reference/command/mapReduce/
.. mongodoc:: mapreduce

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,296 +3,6 @@ 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
-----------
@ -316,7 +26,7 @@ later.
New features:
- Added support for MongoDB 5.0.
- Support for MongoDB Stable API, see :class:`~pymongo.server_api.ServerApi`.
- Support for MongoDB Versioned 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
@ -479,7 +189,7 @@ Deprecations:
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/>`_
`fsync command <https://docs.mongodb.com/manual/reference/command/fsync/>`_
directly with :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command`
instead.
@ -579,10 +289,10 @@ 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/
.. _createUser: https://docs.mongodb.com/manual/reference/command/createUser/
.. _usersInfo: https://docs.mongodb.com/manual/reference/command/usersInfo/
.. _updateUser: https://docs.mongodb.com/manual/reference/command/updateUser/
.. _dropUser: https://docs.mongodb.com/manual/reference/command/createUser/
The deprecated GridFS classes ``MotorGridFS`` and ``AsyncIOMotorGridFS`` are
deleted in favor of :class:`~motor.motor_tornado.MotorGridFSBucket` and
@ -709,9 +419,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 +450,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. ``{}``).
@ -765,7 +475,7 @@ 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.
.. _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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -812,8 +522,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 +572,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:
@ -1089,20 +799,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 +838,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 +866,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 +889,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 +927,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 +938,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 +946,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 +975,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 +1011,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 +1023,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 +1115,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 +1301,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 +1320,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"
root_doc = 'index'
# General information about the project.
project = "Motor"
copyright = "2016-present MongoDB, Inc."
project = u'Motor'
copyright = u'2016-present 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
@ -130,106 +114,104 @@ from motor import MotorClient
html_copy_source = False
try:
import furo # noqa: F401
# Theme gratefully vendored from CPython source.
html_theme = "pydoctheme"
html_theme_path = ["."]
html_theme_options = {'collapsiblesidebar': True}
html_static_path = ['static']
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",
'inherited-members': True,
'member-order': 'groupwise',
}
pymongo_version = metadata("pymongo")["version"]
pymongo_inventory = ("https://pymongo.readthedocs.io/en/%s/" % pymongo_version, None)
pymongo_inventory = ('https://pymongo.readthedocs.io/en/%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,13 +1,6 @@
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
''''''''''''''''''''
@ -35,14 +28,14 @@ and executing the following command::
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`_.
You can read more about TLS versions and their security implications here:
`<https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet#Rule_-_Only_Support_Strong_Protocols>`_
.. _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
''''''''''''''''
@ -53,17 +46,3 @@ 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

@ -15,7 +15,3 @@ The following is a list of people who have contributed to
- 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 import addnodes
from sphinx.domains.python import PyFunction, PyMethod
from sphinx import addnodes
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 "))
signode.insert(0, addnodes.desc_annotation('coroutine ', 'coroutine '))
return ret
class PyCoroutineFunction(PyCoroutineMixin, PyFunction):
def run(self):
self.name = "py:function"
self.name = 'py:function'
return PyFunction.run(self)
class PyCoroutineMethod(PyCoroutineMixin, PyMethod):
def run(self):
self.name = "py:method"
self.name = 'py:method'
return PyMethod.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,13 +2,6 @@
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
@ -52,7 +45,7 @@ See the ``frameworks/tornado`` and ``frameworks/asyncio`` modules.
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.
will be removed in Motor 3.0.
A framework-specific class, like ``MotorClient`` for Tornado or
``AsyncIOMotorClient`` for asyncio, is created by the
@ -112,5 +105,5 @@ synchronous and Motor is async; how can Motor pass PyMongo's tests?
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
runs PyMongo's test suite against it. Use ``tox -e synchro37`` to check out
PyMongo's test suite and run it with Synchro.

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.
@ -49,8 +43,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,7 +53,7 @@ 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)
@ -96,9 +90,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
@ -122,7 +116,7 @@ In Motor, however, no exception is raised. The query simply has no results:
async def f():
cursor = db.collection.find()[100]
# Iterates zero or one time.
# Iterates zero or one times.
async for doc in cursor:
print(doc)
@ -139,10 +133,17 @@ 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:
@ -150,4 +151,7 @@ only the typical style is allowed:
.. code-block:: python
async def f():
await db.create_collection("collection1", capped=True, size=1000)
await db.create_collection(
'collection1',
capped=True,
size=1000)

View File

@ -0,0 +1,5 @@
tornado
aiohttp
Sphinx~=4.1
sphinx_rtd_theme~=0.5
readthedocs-sphinx-search~=0.1

View File

@ -1,59 +1,48 @@
# 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():
db = AsyncIOMotorClient().test
await db.pages.drop()
html = "<html><body>{}</body></html>"
await db.pages.insert_one({"_id": "page-one", "body": html.format("Hello!")})
html = '<html><body>{}</body></html>'
await db.pages.insert_one({'_id': 'page-one',
'body': html.format('Hello!')})
await db.pages.insert_one({"_id": "page-two", "body": html.format("Goodbye.")})
await db.pages.insert_one({'_id': 'page-two',
'body': html.format('Goodbye.')})
return db
# -- setup-end --
# -- handler-start --
async 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)
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()
db = asyncio.run(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

@ -22,33 +22,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)
tmp.seek(0)
await gfs.upload_from_stream(
filename="my_file", source=tmp, metadata={"contentType": "text", "compressed": True}
)
await gfs.upload_from_stream(filename='my_file',
source=tmp,
metadata={'contentType': 'text',
'compressed': True})
asyncio.run(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,7 +11,7 @@ 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/>`_
To create an authenticated connection use a `MongoDB connection URI`_::
@ -27,4 +20,4 @@ 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/
.. _MongoDB connection URI: http://docs.mongodb.org/manual/reference/connection-string/

View File

@ -1,15 +1,17 @@
import asyncio
import os
import asyncio
from bson import json_util
from bson.codec_options import CodecOptions
from bson import json_util
from motor.motor_asyncio import (AsyncIOMotorClient,
AsyncIOMotorClientEncryption)
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):
async def create_json_schema_file(kms_providers, key_vault_namespace,
key_vault_client):
client_encryption = AsyncIOMotorClientEncryption(
kms_providers,
key_vault_namespace,
@ -19,31 +21,31 @@ async def create_json_schema_file(kms_providers, key_vault_namespace, key_vault_
# on MotorClient, Database, or Collection. We will not be calling
# encrypt() or decrypt() in this example so we can use any
# CodecOptions.
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"]
)
'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,
"algorithm":
Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic
}
}
},
"bsonType": "object",
"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)
json_schema_string = json_util.dumps(
schema, json_options=json_util.CANONICAL_JSON_OPTIONS)
with open("jsonSchema.json", "w") as file:
with open('jsonSchema.json', 'w') as file:
file.write(json_schema_string)
@ -68,20 +70,21 @@ async def main():
# 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}}
)
"keyAltNames",
unique=True,
partialFilterExpression={"keyAltNames": {"$exists": True}})
await create_json_schema_file(kms_providers, key_vault_namespace, key_vault_client)
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:
with open('jsonSchema.json', 'r') 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
)
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)
@ -91,10 +94,10 @@ async def main():
await coll.insert_one({"encryptedField": "123456789"})
decrypted_doc = await coll.find_one()
print(f"Decrypted document: {decrypted_doc}")
print('Decrypted document: %s' % (decrypted_doc,))
unencrypted_coll = AsyncIOMotorClient()[db_name][coll_name]
encrypted_doc = await unencrypted_coll.find_one()
print(f"Encrypted document: {encrypted_doc}")
print('Encrypted document: %s' % (encrypted_doc,))
if __name__ == "__main__":

View File

@ -5,13 +5,6 @@
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()
@ -38,10 +31,9 @@ bulk insert operations.
.. doctest::
>>> async def f():
... await db.test.insert_many(({"i": i} for i in range(10000)))
... await db.test.insert_many(({'i': i} for i in range(10000)))
... count = await db.test.count_documents({})
... print("Final count: %d" % count)
...
>>>
>>> IOLoop.current().run_sync(f)
Final count: 10000
@ -69,17 +61,14 @@ of operations performed.
>>> 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}),
... ]
... )
... 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)
...
>>> IOLoop.current().run_sync(f)
@ -106,10 +95,9 @@ the failure.
>>> 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}),
... ]
... 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:
@ -148,11 +136,10 @@ and fourth operations succeed.
>>> async def f():
... requests = [
... InsertOne({"_id": 1}),
... DeleteOne({"_id": 2}),
... InsertOne({"_id": 3}),
... ReplaceOne({"_id": 4}, {"i": 1}),
... ]
... InsertOne({'_id': 1}),
... DeleteOne({'_id': 2}),
... InsertOne({'_id': 3}),
... ReplaceOne({'_id': 4}, {'i': 1})]
... try:
... await db.test.bulk_write(requests, ordered=False)
... except BulkWriteError as bwe:
@ -194,9 +181,10 @@ after all operations are attempted, regardless of execution order.
>>> from pymongo import WriteConcern
>>> async def f():
... coll = db.get_collection("test", write_concern=WriteConcern(w=4, wtimeout=1))
... coll = db.get_collection(
... 'test', write_concern=WriteConcern(w=4, wtimeout=1))
... try:
... await coll.bulk_write([InsertOne({"a": i}) for i in range(4)])
... await coll.bulk_write([InsertOne({'a': i}) for i in range(4)])
... except BulkWriteError as bwe:
... pprint(bwe.details)
...

View File

@ -3,13 +3,6 @@
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

View File

@ -1,11 +1,11 @@
import asyncio
import os
from motor.motor_asyncio import (AsyncIOMotorClient,
AsyncIOMotorClientEncryption)
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
@ -22,8 +22,7 @@ async def main():
# 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
)
kms_providers, key_vault_namespace, bypass_auto_encryption=True)
client = AsyncIOMotorClient(auto_encryption_opts=auto_encryption_opts)
coll = client.test.coll
@ -35,8 +34,9 @@ async def main():
# 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}}
)
"keyAltNames",
unique=True,
partialFilterExpression={"keyAltNames": {"$exists": True}})
client_encryption = AsyncIOMotorClientEncryption(
kms_providers,
@ -47,26 +47,23 @@ async def main():
# 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,
)
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"]
)
data_key_id = 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",
)
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}")
print('Decrypted document: %s' % (doc,))
unencrypted_coll = AsyncIOMotorClient().test.coll
print(f"Encrypted document: {await unencrypted_coll.find_one()}")
print('Encrypted document: %s' % (await unencrypted_coll.find_one(),))
# Cleanup resources.
await client_encryption.close()

View File

@ -1,67 +1,67 @@
import asyncio
import os
from motor.motor_asyncio import (AsyncIOMotorClient,
AsyncIOMotorClientEncryption)
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}}
# 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 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()
# 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}}
)
# 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,
)
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"]
)
# 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 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('Encrypted document: %s' % (doc,))
# Explicitly decrypt the field:
doc["encryptedField"] = await client_encryption.decrypt(doc["encryptedField"])
print(f"Decrypted document: {doc}")
# Explicitly decrypt the field:
doc["encryptedField"] = await client_encryption.decrypt(doc["encryptedField"])
print('Decrypted document: %s' % (doc,))
# Cleanup resources.
await client_encryption.close()
# Cleanup resources.
await client_encryption.close()
if __name__ == "__main__":

View File

@ -1,13 +1,6 @@
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::
@ -19,7 +12,5 @@ Motor Examples
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.
@ -91,6 +84,6 @@ See also:
* The `Topology Monitoring`_ Spec
* The `monitoring_example.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
.. _Command Monitoring: https://github.com/mongodb/specifications/blob/master/source/command-monitoring/command-monitoring.rst
.. _Topology Monitoring: https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring-monitoring.rst
.. _monitoring_example.py: https://github.com/mongodb/motor/blob/master/doc/examples/monitoring_example.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_one({'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,15 +1,16 @@
import asyncio
import os
import asyncio
from bson.binary import STANDARD
from bson.codec_options import CodecOptions
from bson.binary import STANDARD
from motor.motor_asyncio import (AsyncIOMotorClient,
AsyncIOMotorClientEncryption)
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
@ -32,8 +33,9 @@ async def main():
# 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}}
)
"keyAltNames",
unique=True,
partialFilterExpression={"keyAltNames": {"$exists": True}})
client_encryption = AsyncIOMotorClientEncryption(
kms_providers,
@ -44,27 +46,27 @@ async def main():
# on MotorClient, Database, or Collection. We will not be calling
# encrypt() or decrypt() in this example so we can use any
# CodecOptions.
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"]
)
'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,
"algorithm":
Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic
}
}
},
"bsonType": "object",
"bsonType": "object"
}
auto_encryption_opts = AutoEncryptionOpts(kms_providers, key_vault_namespace)
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]
@ -80,20 +82,19 @@ async def main():
# JSON Schema.
codec_options=CodecOptions(uuid_representation=STANDARD),
write_concern=WriteConcern(w="majority"),
validator={"$jsonSchema": json_schema},
)
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}")
print('Decrypted document: %s' % (decrypted_doc,))
unencrypted_coll = AsyncIOMotorClient()[db_name][coll_name]
encrypted_doc = await unencrypted_coll.find_one()
print(f"Encrypted document: {encrypted_doc}")
print('Encrypted document: %s' % (encrypted_doc,))
try:
await unencrypted_coll.insert_one({"encryptedField": "123456789"})
except OperationFailure as exc:
print(f"Unencrypted insert failed: {exc.details}")
print('Unencrypted insert failed: %s' % (exc.details,))
if __name__ == "__main__":

View File

@ -3,13 +3,6 @@
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/>`_.
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
@ -23,12 +16,11 @@ of a replica set member:
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()
first = await oplog.find().sort('$natural', pymongo.ASCENDING).limit(-1).next()
print(first)
ts = first["ts"]
ts = first['ts']
while True:
# For a regular capped collection CursorType.TAILABLE_AWAIT is the
@ -38,14 +30,12 @@ of a replica set member:
# 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,
)
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"]
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

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

@ -4,16 +4,16 @@ import sys
from base64 import urlsafe_b64encode
from pprint import pformat
from motor.motor_tornado import MotorClient
from bson import json_util # Installed with PyMongo.
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")
@ -22,13 +22,17 @@ define("ns", default="test.test", help="database and collection name")
class Application(tornado.web.Application):
def __init__(self):
handlers = [(r"/", MainHandler), (r"/socket", ChangesHandler)]
handlers = [
(r"/", MainHandler),
(r"/socket", ChangesHandler)]
templates = os.path.join(os.path.dirname(__file__), "tornado_change_stream_templates")
templates = os.path.join(os.path.dirname(__file__),
"tornado_change_stream_templates")
super().__init__(
handlers, template_path=templates, template_whitespace="all", debug=options.debug
)
super().__init__(handlers,
template_path=templates,
template_whitespace="all",
debug=options.debug)
class MainHandler(tornado.web.RequestHandler):
@ -51,7 +55,7 @@ class ChangesHandler(tornado.websocket.WebSocketHandler):
def update_cache(cls, change):
cls.cache.append(change)
if len(cls.cache) > cls.cache_size:
cls.cache = cls.cache[-cls.cache_size :]
cls.cache = cls.cache[-cls.cache_size:]
@classmethod
def send_change(cls, change):
@ -59,23 +63,22 @@ class ChangesHandler(tornado.websocket.WebSocketHandler):
for waiter in cls.waiters:
try:
waiter.write_message(change_json)
except Exception as exc:
logging.exception(exc)
except Exception:
logging.error("Error sending message", exc_info=True)
@classmethod
def on_change(cls, change):
logging.info("got change of type '%s'", change.get("operationType"))
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
html_id = urlsafe_b64encode(change['_id']['_data']).decode().rstrip('=')
change.pop('_id')
change['html'] = '<div id="change-%s"><pre>%s</pre></div>' % (
html_id,
tornado.escape.xhtml_escape(pformat(change)))
change['html_id'] = html_id
ChangesHandler.send_change(change)
ChangesHandler.update_cache(change)
@ -93,24 +96,26 @@ async def watch(collection):
def main():
tornado.options.parse_command_line()
if "." not in options.ns:
sys.stderr.write(f'Invalid ns "{options.ns}", must contain a "."')
if '.' not in options.ns:
sys.stderr.write('Invalid ns "%s", must contain a "."' % (options.ns,))
sys.exit(1)
db_name, collection_name = options.ns.split(".", 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.
loop.add_callback(watch, collection)
try:
loop.run_sync(lambda: watch(collection))
loop.start()
except KeyboardInterrupt:
if change_stream:
loop.run_sync(change_stream.close)
pass
finally:
if change_stream is not None:
change_stream.close()
if __name__ == "__main__":

View File

@ -3,17 +3,10 @@
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.
each change notification on a web page using web sockets.
Instructions
------------
@ -25,7 +18,7 @@ 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
.. code-block:: none
> use test
switched to db test
@ -36,7 +29,7 @@ Open a ``mongo`` shell in the terminal and perform some operations on the
The application receives each change notification and displays it as JSON on
the web page:
.. code-block:: text
.. code-block:: none
Changes

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,8 +20,8 @@ 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.

View File

@ -4,12 +4,6 @@ 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
-----
@ -60,7 +54,7 @@ project.
Feature Requests / Feedback
---------------------------
Use our `feedback engine <https://feedback.mongodb.com/?category=7548141816650747033>`_
Use our `feedback engine <https://feedback.mongodb.com/forums/924286-drivers>`_
to send us feature requests and general feedback about PyMongo.
Contributing
@ -91,7 +85,6 @@ Contents
examples/index
changelog
migrate-to-motor-2
migrate-to-motor-3
developer-guide
contributors

View File

@ -1,13 +1,6 @@
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
@ -26,8 +19,8 @@ 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+
* PyMongo_ >=3.12,<4
* Python 3.5+
Optional dependencies:
@ -41,7 +34,7 @@ dependency can be installed automatically along with Motor::
similarly,
`MONGODB-AWS <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/aws-iam/#std-label-pymongo-mongodb-aws>`_
`MONGODB-AWS <https://pymongo.readthedocs.io/en/stable/examples/authentication.html#mongodb-aws>`_
authentication requires ``aws`` extra dependency::
$ pip install "motor[aws]"

View File

@ -1,13 +1,6 @@
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
@ -125,7 +118,6 @@ used callbacks:
else:
print(result)
collection.find_one({}, callback=callback)
Callbacks have been largely superseded by a Futures API intended for use with
@ -142,7 +134,6 @@ a parameter:
except Exception as exc:
print(exc)
future = collection.find_one({})
future.add_done_callback(callback)
@ -190,7 +181,7 @@ Or:
.. code-block:: python3
with client.start_session() as session:
doc = client.db.collection.find_one({}, session=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
@ -212,4 +203,4 @@ Or:
.. code-block:: python3
async with client.start_session() as session:
doc = await client.db.collection.find_one({}, session=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

@ -36,17 +36,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):
has_content = True
required_arguments = 0
optional_arguments = 0
@ -55,7 +58,7 @@ class MongodocDirective(rst.Directive):
def run(self):
node = mongodoc()
title = "The MongoDB documentation on"
title = 'The MongoDB documentation on'
node += nodes.title(title, title)
self.state.nested_parse(self.content, self.content_offset, node)
return [node]
@ -83,13 +86,12 @@ 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

@ -18,7 +18,11 @@ import re
from docutils.nodes import doctest_block, literal_block
from sphinx import addnodes
from sphinx.addnodes import desc, desc_content, desc_signature, seealso, versionmodified
from sphinx.addnodes import (desc,
desc_content,
desc_signature,
seealso,
versionmodified)
from sphinx.util.inspect import safe_getattr
import motor
@ -32,7 +36,7 @@ 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
@ -62,7 +66,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
@ -79,31 +83,34 @@ 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 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)
@ -125,16 +132,19 @@ def get_motor_attr(motor_class, name, *defargs):
attr = safe_getattr(motor_class, name, *defargs)
# 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 +152,27 @@ 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)
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)
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")
lines[:] = subbed.split('\n')
def build_finished(app, exception):
@ -174,7 +184,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('doctree-read', process_motor_nodes)
app.connect('build-finished', build_finished)
return {'parallel_write_safe': True, 'parallel_read_safe': False}

View File

@ -1,27 +1,20 @@
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 3.5 and later.
* PyMongo_ 3.12 and later.
Motor can integrate with either Tornado or asyncio.
The default authentication mechanism for MongoDB is SCRAM-SHA-1.
The default authentication mechanism for MongoDB 3.0+ is SCRAM-SHA-1.
Building the docs requires `sphinx`_.
.. _PyMongo: https://pypi.python.org/pypi/pymongo/
.. _sphinx: https://www.sphinx-doc.org/
.. _sphinx: http://sphinx.pocoo.org/
.. _compatibility-matrix:
@ -34,51 +27,55 @@ Motor and PyMongo
+-------------------+-----------------+
| Motor Version | PyMongo Version |
+===================+=================+
| 1.0 | 3.3+ |
+-------------------+-----------------+
| 1.1 | 3.4+ |
+-------------------+-----------------+
| 1.2 | 3.6+ |
+-------------------+-----------------+
| 1.3 | 3.6+ |
+-------------------+-----------------+
| 2.0 | 3.7+ |
+-------------------+-----------------+
| 2.1 | 3.10+ |
+-------------------+-----------------+
| 2.2 | 3.11+ |
+-------------------+-----------------+
| 2.3 | 3.11+ |
+-------------------+-----------------+
| 2.4 | 3.11+ |
+-------------------+-----------------+
| 2.5 | 3.12+ |
+-------------------+-----------------+
| 3.0 | 4.1+ |
+-------------------+-----------------+
| 3.1 | 4.2+ |
+-------------------+-----------------+
| 3.2 | 4.4+ |
+-------------------+-----------------+
| 3.3 | 4.5+ |
+-------------------+-----------------+
| 3.4 | 4.5+ |
+-------------------+-----------------+
| 3.5 | 4.5+ |
+-------------------+-----------------+
| 3.6 | 4.9 |
+-------------------+-----------------+
| 3.7 | 4.9+ |
+-------------------+-----------------+
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 |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+---------------------------------------------------------------------------------------+
| MongoDB Version |
+=====================+=====+=====+=====+=====+=====+=====+=====+=====+=====+=====+=====+
| | 2.2 | 2.4 | 2.6 | 3.0 | 3.2 | 3.4 | 3.6 | 4.0 | 4.2 | 4.4 | 5.0 |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Motor Version | 1.0 | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 1.1 | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 1.2 |**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 1.3 |**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.0 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.1 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.2 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.3 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.4 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| | 2.5 |**N**|**N**|**N**| Y | Y | 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 +83,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.com/drivers/pymongo#mongodb-compatibility
Motor and Tornado
`````````````````
@ -97,79 +94,61 @@ known to be incompatible, or have not been tested together.
+---------------------------------------------+
| Tornado Version |
+=====================+=====+=====+=====+=====+
| Motor Version | 2.5 |**N**|**N**| Y | Y |
| | 3.x | 4.x | 5.x | 6.x |
+---------------+-----+-----+-----+-----+-----+
| | 3.0 |**N**|**N**|**N**| Y |
| Motor Version | 1.0 | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+
| | 3.1 |**N**|**N**|**N**| Y |
| | 1.1 | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+
| | 3.2 |**N**|**N**|**N**| Y |
| | 1.2 |**N**| Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+
| | 3.3 |**N**|**N**|**N**| Y |
| | 1.3 |**N**| Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+
| | 3.4 |**N**|**N**|**N**| Y |
| | 2.0 |**N**| Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+
| | 2.1 |**N**| Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+
| | 2.2 |**N**|**N**| Y | Y |
+---------------+-----+-----+-----+-----+-----+
| | 2.3 |**N**|**N**| Y | Y |
+---------------+-----+-----+-----+-----+-----+
Motor and Python
````````````````
Motor 2.5 deprecated support for Python 3.5.
Motor 1.2 dropped support for the short-lived version of
the "async for" protocol implemented in Python 3.5.0 and 3.5.1. Motor continues
to work with "async for" loops in Python 3.5.2 and later.
Motor 3.0 dropped support for Pythons older than 3.7.
Motor 1.2.5 and 1.3.1 add compatibility with Python 3.7, but at the cost of
dropping Python 3.4.3 and older. Motor 2.2 dropped support for Pythons older
than 3.5.2. Motor 2.5 deprecates support for Python 3.5.
Motor 3.1.1 added support for Python 3.11.
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 |
+---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+-------------------------------------------------------------------------------------------+
| Python Version |
+=====================+=====+=====+=====+=====+=====+=======+=======+=====+=====+=====+=====+
| | 2.5 | 2.6 | 2.7 | 3.3 | 3.4 | 3.5.0 | 3.5.2 | 3.6 | 3.7 | 3.8 | 3.9 |
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| Motor Version | 1.0 |**N**| Y | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 1.1 |**N**| Y | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 1.2 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 1.3 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 2.0 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y |**N**|**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 2.1 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 2.2 |**N**|**N**|**N**|**N**|**N**|**N** | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 2.3 |**N**|**N**|**N**|**N**|**N**|**N** | Y | Y | Y | Y |**N**|
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 2.4 |**N**|**N**|**N**|**N**|**N**|**N** | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
| | 2.5 |**N**|**N**|**N**|**N**|**N**|**N** | Y | Y | Y | Y | Y |
+---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+-----+
Not Supported
-------------

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,7 +12,6 @@ Tutorial: Using Motor With :mod:`asyncio`
import pymongo
import motor.motor_asyncio
import asyncio
client = motor.motor_asyncio.AsyncIOMotorClient()
db = client.test_database
@ -28,17 +20,14 @@ Tutorial: Using Motor With :mod:`asyncio`
import pymongo
import motor.motor_asyncio
import asyncio
client = motor.motor_asyncio.AsyncIOMotorClient()
db = client.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,9 +140,9 @@ 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
@ -170,22 +157,23 @@ 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`:
.. 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),))
... result = await db.test_collection.insert_many(
... [{'i': i} for i in range(2000)])
... print('inserted %d docs' % (len(result.inserted_ids),))
...
>>> loop = client.get_io_loop()
>>> loop.run_until_complete(do_insert())
inserted 2000 docs
Getting a Single Document With ``find_one``
-------------------------------------------
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,7 +182,7 @@ 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()
@ -221,7 +209,7 @@ 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)
...
@ -245,7 +233,7 @@ 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()
@ -258,9 +246,9 @@ 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': 4}})
... # Modify the query before iterating
... cursor.sort("i", -1).skip(1).limit(2)
... cursor.sort('i', -1).skip(1).limit(2)
... async for document in cursor:
... pprint.pprint(document)
...
@ -272,7 +260,7 @@ 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
Counting Documents
------------------
@ -284,9 +272,9 @@ that match a query:
>>> 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)
... print('%s documents in collection' % n)
... n = await db.test_collection.count_documents({'i': {'$gt': 1000}})
... print('%s documents where i > 1000' % n)
...
>>> loop = client.get_io_loop()
>>> loop.run_until_complete(do_count())
@ -305,13 +293,13 @@ 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.run_until_complete(do_replace())
@ -331,10 +319,10 @@ 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.run_until_complete(do_update())
@ -354,24 +342,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.
@ -381,13 +351,13 @@ 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({})))
... 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({})))
...
>>> loop = client.get_io_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
@ -402,7 +372,8 @@ the :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command` method on
>>> from bson import SON
>>> async def use_distinct_command():
... response = await db.command(SON([("distinct", "test_collection"), ("key", "i")]))
... response = await db.command(SON([("distinct", "test_collection"),
... ("key", "i")]))
...
>>> loop = client.get_io_loop()
>>> loop.run_until_complete(use_distinct_command())
@ -461,9 +432,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 +458,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,31 +96,31 @@ 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.
@ -159,10 +149,10 @@ 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.
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
@ -189,7 +179,7 @@ methods than ``HTTPServer.start()``. See Tornado's guide to
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,7 +187,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.
@ -211,9 +201,9 @@ store a document in MongoDB, call :meth:`~MotorCollection.insert_one` in an
.. 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))
...
>>>
>>> IOLoop.current().run_sync(do_insert)
@ -226,7 +216,7 @@ store a document in MongoDB, call :meth:`~MotorCollection.insert_one` in an
>>> # Clean up from previous insert
>>> pymongo.MongoClient().test_database.test_collection.delete_many({})
DeleteResult({'n': 1, 'ok': 1.0}, acknowledged=True)
<pymongo.results.DeleteResult ...>
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::
@ -246,7 +236,7 @@ sequence, use ``await``:
>>> async def do_insert():
... for i in range(2000):
... await db.test_collection.insert_one({"i": i})
... await db.test_collection.insert_one({'i': i})
...
>>> IOLoop.current().run_sync(do_insert)
@ -259,7 +249,7 @@ 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`:
@ -267,8 +257,9 @@ For better performance, insert documents in large batches with
.. 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),))
... result = await db.test_collection.insert_many(
... [{'i': i} for i in range(2000)])
... print('inserted %d docs' % (len(result.inserted_ids),))
...
>>> IOLoop.current().run_sync(do_insert)
inserted 2000 docs
@ -282,7 +273,7 @@ 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)
...
>>> IOLoop.current().run_sync(do_find_one)
@ -311,7 +302,7 @@ 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)
...
@ -334,7 +325,7 @@ 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)
...
>>> IOLoop.current().run_sync(do_find)
@ -346,9 +337,9 @@ 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': 4}})
... # Modify the query before iterating
... cursor.sort("i", -1).skip(1).limit(2)
... cursor.sort('i', -1).skip(1).limit(2)
... async for document in cursor:
... pprint.pprint(document)
...
@ -359,7 +350,7 @@ 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
Counting Documents
------------------
@ -370,9 +361,9 @@ documents in a collection, or the number of documents that match a query:
>>> 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)
... print('%s documents in collection' % n)
... n = await db.test_collection.count_documents({'i': {'$gt': 1000}})
... print('%s documents where i > 1000' % n)
...
>>> IOLoop.current().run_sync(do_count)
2000 documents in collection
@ -390,13 +381,13 @@ 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))
...
>>> IOLoop.current().run_sync(do_replace)
found document: {'_id': ObjectId('...'), 'i': 50}
@ -415,10 +406,10 @@ 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))
...
>>> IOLoop.current().run_sync(do_update)
updated 1 document
@ -437,23 +428,6 @@ 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.
@ -463,12 +437,12 @@ Removing 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({})))
... 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({})))
...
>>> IOLoop.current().run_sync(do_delete_many)
1999 documents before calling delete_many()
2000 documents before calling delete_many()
1000 documents after
.. mongodoc:: remove
@ -483,7 +457,8 @@ the :meth:`~motor.motor_tornado.MotorDatabase.command` method on
>>> from bson import SON
>>> async def use_distinct_command():
... response = await db.command(SON([("distinct", "test_collection"), ("key", "i")]))
... response = await db.command(SON([("distinct", "test_collection"),
... ("key", "i")]))
...
>>> IOLoop.current().run_sync(use_distinct_command)
@ -508,4 +483,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().__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

@ -13,17 +13,33 @@
# limitations under the License.
"""Motor, an asynchronous driver for MongoDB."""
from ._version import get_version_string, version, version_tuple # noqa: F401
version_tuple = (2, 5, 2, 'dev0')
def get_version_string():
return '.'.join(map(str, version_tuple))
version = get_version_string()
"""Current version of Motor."""
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 the future. 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,
MotorGridFSBucket,
MotorGridIn,
MotorGridOut)
# 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

@ -24,11 +24,8 @@ import mimetypes
import aiohttp.web
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)
@ -178,26 +174,23 @@ class AIOHTTPGridFS:
async 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 = await 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)
self._set_standard_headers(request.path, resp, gridout)
# Overridable method set_extra_headers.
self._set_extra_headers(resp, gridout)
@ -216,14 +209,14 @@ 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)
if request.method == "GET":
if request.method == 'GET':
written = 0
while written < gridout.length:
# Reading chunk_size at a time minimizes buffering.
@ -232,7 +225,7 @@ class AIOHTTPGridFS:
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 +234,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,34 @@
See "Frameworks" in the Developer Guide.
"""
import asyncio
import asyncio.tasks
import functools
import multiprocessing
import os
import warnings
from asyncio import get_event_loop # noqa: F401 - For framework interface.
from asyncio import coroutine # For framework interface.
from concurrent.futures import ThreadPoolExecutor
# mypy: ignore-errors
try:
import contextvars
except ImportError:
contextvars = None
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():
try:
return asyncio.get_running_loop()
except RuntimeError:
# Workaround for bugs.python.org/issue39529.
return asyncio.get_event_loop_policy().get_event_loop()
def is_event_loop(loop):
@ -51,38 +52,30 @@ 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()
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)
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))
return loop.run_in_executor(
_EXECUTOR, functools.partial(fn, *args, **kwargs))
# Adapted from tornado.gen.
@ -118,7 +111,8 @@ def chain_return_value(future, loop, return_value):
else:
chained.set_result(return_value)
future.add_done_callback(functools.partial(loop.call_soon_threadsafe, copy))
future.add_done_callback(
functools.partial(loop.call_soon_threadsafe, copy))
return chained
@ -134,7 +128,8 @@ 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))
def pymongo_class_wrapper(f, pymongo_class):
@ -142,7 +137,6 @@ def pymongo_class_wrapper(f, pymongo_class):
See WrapAsync.
"""
@functools.wraps(f)
async def _wrapper(self, *args, **kwargs):
result = await f(self, *args, **kwargs)
@ -159,12 +153,10 @@ 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,
)
"The yieldable function is deprecated and will be removed in "
"Motor 3.0", DeprecationWarning, stacklevel=2)
return next(iter(future))
def platform_info():
return "asyncio"
return 'asyncio'

View File

@ -19,22 +19,21 @@ See "Frameworks" in the Developer Guide.
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.
import warnings
from concurrent.futures import ThreadPoolExecutor
from tornado import concurrent, gen, ioloop, version as tornado_version
from tornado.gen import chain_future, coroutine # For framework interface.
try:
import contextvars
except ImportError:
contextvars = None
# mypy: ignore-errors
CLASS_PREFIX = ""
CLASS_PREFIX = ''
def get_event_loop():
@ -47,38 +46,29 @@ 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)
def get_future(loop):
return concurrent.Future()
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)
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))
return loop.run_in_executor(
_EXECUTOR, functools.partial(fn, *args, **kwargs))
def chain_return_value(future, loop, return_value):
@ -124,7 +114,6 @@ def pymongo_class_wrapper(f, pymongo_class):
See WrapAsync.
"""
@functools.wraps(f)
async def _wrapper(self, *args, **kwargs):
result = await f(self, *args, **kwargs)
@ -141,12 +130,10 @@ 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,
)
"The yieldable function is deprecated and will be removed in "
"Motor 3.0", DeprecationWarning, stacklevel=2)
return future
def platform_info():
return f"Tornado {tornado_version}"
return 'Tornado %s' % (tornado_version,)

View File

@ -13,17 +13,17 @@
# limitations under the License.
"""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
_class_cache = {}
def asynchronize(framework, sync_method: Callable, doc=None, wrap_class=None, unwrap_class=None):
def asynchronize(
framework, sync_method, doc=None, wrap_class=None, unwrap_class=None):
"""Decorate `sync_method` so it returns a Future.
The method runs on a thread and resolves the Future when it completes.
@ -40,42 +40,41 @@ def asynchronize(framework, sync_method: Callable, doc=None, wrap_class=None, un
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"))
if obj.__class__.__name__.endswith(
(unwrap_class, 'MotorClientSession'))
else obj
for obj in args
]
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()
}
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
]
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()
}
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
)
return framework.run_on_executor(loop,
sync_method,
self.delegate,
*unwrapped_args,
**unwrapped_kwargs)
if wrap_class is not None:
method = framework.pymongo_class_wrapper(method, wrap_class)
@ -95,16 +94,18 @@ def asynchronize(framework, sync_method: Callable, doc=None, wrap_class=None, un
def unwrap_args_session(args):
return (
obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj
for obj in args
)
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()
}
key: (obj.delegate
if obj.__class__.__name__.endswith('MotorClientSession')
else obj)
for key, obj in kwargs.items()}
_coro_token = object()
@ -123,12 +124,11 @@ def coroutine_annotation(f):
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
@ -153,13 +153,11 @@ class Async(MotorAttributeFactory):
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,
wrap_class=self.wrap_class,
unwrap_class=self.unwrap_class)
def wrap(self, original_class):
self.wrap_class = original_class
@ -180,7 +178,7 @@ class AsyncRead(Async):
class AsyncWrite(Async):
def __init__(self, attr_name=None, doc=None):
"""A descriptor that wraps a PyMongo write method like update_one() that
"""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)
@ -196,7 +194,6 @@ class AsyncCommand(Async):
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)
@ -267,10 +264,7 @@ 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)
@ -281,7 +275,7 @@ def create_class_with_framework(cls: T, framework: Any, module_name: str) -> T:
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).

View File

@ -13,28 +13,17 @@
# limitations under the License.
"""Asyncio support for Motor, an asynchronous driver for MongoDB."""
from . import core, motor_gridfs
from .frameworks import asyncio as asyncio_framework
from .metaprogramming import T, create_class_with_framework
from .metaprogramming import create_class_with_framework
__all__ = [
"AsyncIOMotorClient",
"AsyncIOMotorClientSession",
"AsyncIOMotorDatabase",
"AsyncIOMotorCollection",
"AsyncIOMotorCursor",
"AsyncIOMotorCommandCursor",
"AsyncIOMotorChangeStream",
"AsyncIOMotorGridFSBucket",
"AsyncIOMotorGridIn",
"AsyncIOMotorGridOut",
"AsyncIOMotorGridOutCursor",
"AsyncIOMotorClientEncryption",
]
__all__ = ['AsyncIOMotorClient','AsyncIOMotorClientEncryption']
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.motor_asyncio')
AsyncIOMotorClient = create_asyncio_class(core.AgnosticClient)
@ -43,34 +32,45 @@ 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)
AsyncIOMotorChangeStream = create_asyncio_class(core.AgnosticChangeStream)
AsyncIOMotorChangeStream = create_asyncio_class(
core.AgnosticChangeStream)
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)
AsyncIOMotorGridOutCursor = create_asyncio_class(
motor_gridfs.AgnosticGridOutCursor)
AsyncIOMotorClientEncryption = create_asyncio_class(core.AgnosticClientEncryption)
AsyncIOMotorClientEncryption = create_asyncio_class(
core.AgnosticClientEncryption)

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

@ -13,4 +13,5 @@
# limitations under the License.
"""Common code to support all async frameworks."""
callback_type_error = TypeError("callback must be a callable")

View File

@ -13,38 +13,41 @@
# limitations under the License.
"""GridFS implementation for Motor, an asynchronous driver for MongoDB."""
import hashlib
import warnings
import gridfs
import pymongo
import pymongo.errors
from gridfs import DEFAULT_CHUNK_SIZE, grid_file
from gridfs import (DEFAULT_CHUNK_SIZE,
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 (AgnosticCursor,
AgnosticCollection,
AgnosticDatabase)
from motor.docstrings import *
from motor.metaprogramming import (AsyncCommand,
AsyncRead,
coroutine_annotation,
create_class_with_framework,
DelegateMethod,
MotorCursorChainingMethod,
ReadOnlyProperty)
class AgnosticGridOutCursor(AgnosticCursor):
__motor_class_name__ = "MotorGridOutCursor"
__motor_class_name__ = 'MotorGridOutCursor'
__delegate_class__ = gridfs.GridOutCursor
# PyMongo's GridOutCursor inherits __die from Cursor.
_Cursor__die = AsyncCommand()
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()
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:
@ -54,14 +57,12 @@ class AgnosticGridOutCursor(AgnosticCursor):
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 +70,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,49 +82,48 @@ 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()
readable = DelegateMethod()
readchunk = AsyncRead()
readline = AsyncRead()
seek = DelegateMethod()
seekable = DelegateMethod()
tell = DelegateMethod()
upload_date = MotorGridOutProperty()
write = DelegateMethod()
def __init__(
self, root_collection, file_id=None, file_document=None, delegate=None, session=None
):
def __init__(self, root_collection, file_id=None, file_document=None,
delegate=None, session=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,
session=session)
self.io_loop = root_collection.get_io_loop()
@ -139,8 +139,8 @@ class AgnosticGridOut:
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)
@ -157,7 +157,9 @@ class AgnosticGridOut:
: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.chain_return_value(self._ensure_file(),
self.get_io_loop(),
self)
def get_io_loop(self):
return self.io_loop
@ -176,7 +178,7 @@ class AgnosticGridOut:
@tornado.web.asynchronous
@gen.coroutine
def get(self, filename):
db = self.settings["db"]
db = self.settings['db']
fs = await motor.MotorGridFSBucket(db())
try:
gridout = await fs.open_download_stream_by_name(filename)
@ -202,31 +204,29 @@ 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()
_id = ReadOnlyProperty()
abort = AsyncCommand()
chunk_size = ReadOnlyProperty()
closed = ReadOnlyProperty()
close = AsyncCommand()
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="""
filename = ReadOnlyProperty()
length = ReadOnlyProperty()
md5 = ReadOnlyProperty()
name = ReadOnlyProperty()
read = DelegateMethod()
readable = DelegateMethod()
seekable = DelegateMethod()
upload_date = ReadOnlyProperty()
write = AsyncCommand().unwrap('MotorGridOut')
writeable = DelegateMethod()
writelines = AsyncCommand().unwrap('MotorGridOut')
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 +239,23 @@ 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, session=None,
disable_md5=False, **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
@ -277,72 +277,63 @@ Metadata set on the file appears as attributes on a
- `session` (optional): a
:class:`~pymongo.client_session.ClientSession` to use for all
commands
- `disable_md5` (optional): When True, an MD5 checksum will not be
computed for the uploaded file. Useful in environments where
MD5 cannot be used for regulatory or other reasons. Defaults to
False.
- `**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,
session=session,
disable_md5=disable_md5,
**kwargs)
# Support "async with bucket.open_upload_stream() as f:"
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)
await self.close()
def get_io_loop(self):
return self.io_loop
class AgnosticGridFSBucket:
__motor_class_name__ = "MotorGridFSBucket"
class AgnosticGridFSBucket(object):
__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, bucket_name="fs", disable_md5=False,
chunk_size_bytes=DEFAULT_CHUNK_SIZE, write_concern=None,
read_preference=None, collection=None):
"""Create a handle to a GridFS bucket.
Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern`
@ -362,12 +353,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.
- `disable_md5` (optional): When True, MD5 checksums will not be
computed for uploaded files. Useful in environments where MD5
cannot be used for regulatory or other reasons. Defaults to False.
- `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.
@ -379,31 +370,31 @@ class AgnosticGridFSBucket:
"""
# 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,
)
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__)
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}"
)
"First argument to %s must be MotorDatabase, not %r" % (
self.__class__, database))
self.io_loop = database.get_io_loop()
self.collection = database.get_collection(
bucket_name, write_concern=write_concern, read_preference=read_preference
)
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,
)
disable_md5=disable_md5)
def get_io_loop(self):
return self.io_loop
@ -411,24 +402,27 @@ class AgnosticGridFSBucket:
def wrap(self, obj):
if obj.__class__ is grid_file.GridIn:
grid_in_class = create_class_with_framework(
AgnosticGridIn, self._framework, self.__module__
)
AgnosticGridIn, self._framework, self.__module__)
return grid_in_class(root_collection=self.collection, delegate=obj)
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__
)
AgnosticGridOut, self._framework, self.__module__)
return grid_out_class(root_collection=self.collection, delegate=obj)
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__
)
AgnosticGridOutCursor, self._framework, self.__module__)
return grid_out_class(cursor=obj, collection=self.collection)
return grid_out_class(
cursor=obj,
collection=self.collection)
def find(self, *args, **kwargs):
"""Find and return the files collection documents that match ``filter``.
@ -485,20 +479,6 @@ class AgnosticGridFSBucket:
"""
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: ...

View File

@ -16,26 +16,14 @@
from . import core, motor_gridfs
from .frameworks import tornado as tornado_framework
from .metaprogramming import T, create_class_with_framework
from .metaprogramming import create_class_with_framework
__all__ = [
"MotorClient",
"MotorClientSession",
"MotorDatabase",
"MotorCollection",
"MotorCursor",
"MotorCommandCursor",
"MotorChangeStream",
"MotorGridFSBucket",
"MotorGridIn",
"MotorGridOut",
"MotorGridOutCursor",
"MotorClientEncryption",
]
__all__ = ['MotorClient', 'MotorClientEncryption']
def create_motor_class(cls: T) -> T:
return create_class_with_framework(cls, tornado_framework, "motor.motor_tornado")
def create_motor_class(cls):
return create_class_with_framework(cls, tornado_framework,
'motor.motor_tornado')
MotorClient = create_motor_class(core.AgnosticClient)

View File

@ -1,259 +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] = [
"MotorClient",
"MotorClientSession",
"MotorDatabase",
"MotorCollection",
"MotorCursor",
"MotorCommandCursor",
"MotorChangeStream",
"MotorGridFSBucket",
"MotorGridIn",
"MotorGridOut",
"MotorGridOutCursor",
"MotorClientEncryption",
]
class MotorClient(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,
) -> MotorDatabase[_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,
) -> MotorDatabase[_DocumentType]: ...
async def list_databases(
self,
session: Optional[core.AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> MotorCommandCursor[dict[str, Any]]: ...
async def start_session(
self,
causal_consistency: Optional[bool] = None,
default_transaction_options: Optional[TransactionOptions] = None,
snapshot: Optional[bool] = False,
) -> MotorClientSession: ...
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,
) -> MotorChangeStream[_DocumentType]: ...
def __getattr__(self, name: str) -> MotorDatabase[_DocumentType]: ...
def __getitem__(self, name: str) -> MotorDatabase[_DocumentType]: ...
class MotorClientSession(core.AgnosticClientSession):
@property
def client(self) -> MotorClient: ...
async def __aenter__(self) -> MotorClientSession: ...
class MotorDatabase(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,
) -> MotorCommandCursor[_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,
) -> MotorCollection[_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,
) -> MotorCollection[_DocumentType]: ...
async def list_collections(
self,
session: Optional[core.AgnosticClientSession] = None,
filter: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> MotorCommandCursor[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,
) -> MotorDatabase[_DocumentType]: ...
def aggregate(
self, pipeline: _Pipeline, *args: Any, **kwargs: Any
) -> MotorLatentCommandCursor[_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,
) -> MotorChangeStream[_DocumentType]: ...
@property
def client(self) -> MotorClient[_DocumentType]: ...
def __getattr__(self, name: str) -> MotorCollection[_DocumentType]: ...
def __getitem__(self, name: str) -> MotorCollection[_DocumentType]: ...
class MotorCollection(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,
) -> MotorCollection[_DocumentType]: ...
def list_search_indexes(
self,
name: Optional[str] = None,
session: Optional[core.AgnosticClientSession] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> MotorLatentCommandCursor[Mapping[str, Any]]: ...
def __getattr__(self, name: str) -> MotorCollection[_DocumentType]: ...
def __getitem__(self, name: str) -> MotorCollection[_DocumentType]: ...
def find(self, *args: Any, **kwargs: Any) -> MotorCursor[_DocumentType]: ...
def find_raw_batches(self, *args: Any, **kwargs: Any) -> MotorRawBatchCursor[_DocumentType]: ...
def aggregate(
self, pipeline: _Pipeline, *args: Any, **kwargs: Any
) -> MotorCommandCursor[_DocumentType]: ...
def aggregate_raw_batches(
self, pipeline: _Pipeline, **kwargs: Any
) -> MotorRawBatchCursor[_DocumentType]: ...
def list_indexes(
self, session: Optional[core.AgnosticClientSession] = None, **kwargs: Any
) -> MotorLatentCommandCursor[MutableMapping[str, Any]]: ...
class MotorLatentCommandCursor(core.AgnosticLatentCommandCursor[_DocumentType]): ...
class MotorCursor(core.AgnosticCursor[_DocumentType]):
def collation(self, collation: Optional[_CollationIn]) -> MotorCursor[_DocumentType]: ...
def add_option(self, mask: int) -> MotorCursor[_DocumentType]: ...
def remove_option(self, mask: int) -> MotorCursor[_DocumentType]: ...
def limit(self, limit: int) -> MotorCursor[_DocumentType]: ...
def skip(self, skip: int) -> MotorCursor[_DocumentType]: ...
def max_scan(self, max_scan: Optional[int]) -> MotorCursor[_DocumentType]: ...
def sort(
self, key_or_list: _Hint, direction: Optional[Union[int, str]] = None
) -> MotorCursor[_DocumentType]: ...
def hint(self, index: Optional[_Hint]) -> MotorCursor[_DocumentType]: ...
def where(self, code: Union[str, Code]) -> MotorCursor[_DocumentType]: ...
def max_await_time_ms(self, max_await_time_ms: Optional[int]) -> MotorCursor[_DocumentType]: ...
def max_time_ms(self, max_time_ms: Optional[int]) -> MotorCursor[_DocumentType]: ...
def min(self, spec: _Sort) -> MotorCursor[_DocumentType]: ...
def max(self, spec: _Sort) -> MotorCursor[_DocumentType]: ...
def comment(self, comment: Any) -> MotorCursor[_DocumentType]: ...
def allow_disk_use(self, allow_disk_use: bool) -> MotorCursor[_DocumentType]: ...
def rewind(self) -> MotorCursor[_DocumentType]: ...
def clone(self) -> MotorCursor[_DocumentType]: ...
def __copy__(self) -> MotorCursor[_DocumentType]: ...
def __deepcopy__(self, memo: Any) -> MotorCursor[_DocumentType]: ...
class MotorRawBatchCursor(core.AgnosticRawBatchCursor[_DocumentType]): ...
class MotorCommandCursor(core.AgnosticCommandCursor[_DocumentType]): ...
class MotorRawBatchCommandCursor(core.AgnosticRawBatchCommandCursor[_DocumentType]): ...
class MotorChangeStream(core.AgnosticChangeStream[_DocumentType]):
def __aiter__(self) -> MotorChangeStream[_DocumentType]: ...
async def __aenter__(self) -> MotorChangeStream[_DocumentType]: ...
class MotorClientEncryption(core.AgnosticClientEncryption[_DocumentType]):
async def __aenter__(self) -> MotorClientEncryption[_DocumentType]: ...
async def get_keys(self) -> MotorCursor[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[MotorCollection[_DocumentTypeArg], Mapping[str, Any]]: ...
class MotorGridOutCursor(motor_gridfs.AgnosticGridOutCursor):
def next_object(self) -> MotorGridOutCursor: ...
class MotorGridOut(motor_gridfs.AgnosticGridOut):
def __aiter__(self) -> MotorGridOut: ...
class MotorGridIn(motor_gridfs.AgnosticGridIn):
async def __aenter__(self) -> MotorGridIn: ...
class MotorGridFSBucket(motor_gridfs.AgnosticGridFSBucket):
async def open_download_stream_by_name(
self,
filename: str,
revision: int = -1,
session: Optional[core.AgnosticClientSession] = None,
) -> MotorGridOut: ...
async def open_download_stream(
self, file_id: Any, session: Optional[core.AgnosticClientSession] = None
) -> MotorGridOut: ...
def open_upload_stream(
self,
filename: str,
chunk_size_bytes: Optional[int] = None,
metadata: Optional[Mapping[str, Any]] = None,
session: Optional[core.AgnosticClientSession] = None,
) -> MotorGridIn: ...
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,
) -> MotorGridIn: ...
def find(self, *args: Any, **kwargs: Any) -> MotorGridOutCursor: ...

View File

@ -1,2 +0,0 @@
# PEP-561 Support File.
# "Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing".

View File

@ -13,26 +13,26 @@
# limitations under the License.
"""Utilities for using Motor with Tornado web applications."""
import datetime
import email.utils
import mimetypes
import time
import gridfs
import tornado.web
from tornado import gen
import gridfs
import motor
from motor.motor_gridfs import _hash_gridout
# mypy: disable-error-code="no-untyped-def,no-untyped-call"
# TODO: this class is not a drop-in replacement for StaticFileHandler.
# StaticFileHandler provides class method make_static_url, which appends
# an checksum of the static file's contents. Templates thus can do
# an MD5 of the static file's contents. Templates thus can do
# {{ static_url('image.png') }} and get "/static/image.png?v=1234abcdef",
# which is cached forever. Problem is, it calculates the checksum synchronously.
# which is cached forever. Problem is, it calculates the MD5 synchronously.
# Two options: keep a synchronous GridFS available to get each grid file's
# checksum synchronously for every static_url call, or find some other idiom.
# MD5 synchronously for every static_url call, or find some other idiom.
class GridFSHandler(tornado.web.RequestHandler):
@ -42,19 +42,16 @@ class GridFSHandler(tornado.web.RequestHandler):
.. code-block:: python
db = motor.MotorClient().my_database
application = web.Application(
[
(r"/static/(.*)", web.GridFSHandler, {"database": db}),
]
)
application = web.Application([
(r"/static/(.*)", web.GridFSHandler, {"database": db}),
])
By default, requests' If-Modified-Since headers are honored, but no
specific cache-control timeout is sent to clients. Thus each request for
a GridFS file requires a quick check of the file's ``uploadDate`` in
MongoDB. Override :meth:`get_cache_time` in a subclass to customize this.
"""
def initialize(self, database, root_collection="fs"):
def initialize(self, database, root_collection='fs'):
self.database = database
self.root_collection = root_collection
@ -100,16 +97,14 @@ class GridFSHandler(tornado.web.RequestHandler):
try:
gridout = await self.get_gridfs_file(fs, path, self.request)
except gridfs.NoFile:
raise tornado.web.HTTPError(404) from None
raise tornado.web.HTTPError(404)
# If-Modified-Since header is only good to the second.
modified = gridout.upload_date.replace(microsecond=0)
self.set_header("Last-Modified", modified)
# Get the hash for the GridFS file.
checksum = _hash_gridout(gridout)
self.set_header("Etag", '"%s"' % checksum)
# MD5 is calculated on the MongoDB server when GridFS file is created
self.set_header("Etag", '"%s"' % gridout.md5)
mime_type = gridout.content_type
@ -124,11 +119,8 @@ class GridFSHandler(tornado.web.RequestHandler):
cache_time = self.get_cache_time(path, modified, mime_type)
if cache_time > 0:
self.set_header(
"Expires",
datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
+ datetime.timedelta(seconds=cache_time),
)
self.set_header("Expires", datetime.datetime.utcnow() +
datetime.timedelta(seconds=cache_time))
self.set_header("Cache-Control", "max-age=" + str(cache_time))
else:
self.set_header("Cache-Control", "public")
@ -140,13 +132,12 @@ class GridFSHandler(tornado.web.RequestHandler):
ims_value = self.request.headers.get("If-Modified-Since")
if ims_value is not None:
date_tuple = email.utils.parsedate(ims_value)
assert date_tuple is not None
# If our MotorClient is tz-aware, assume the naive ims_value is in
# its time zone.
if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple)).replace(
tzinfo=modified.tzinfo
)
if_since = datetime.datetime.fromtimestamp(
time.mktime(date_tuple)
).replace(tzinfo=modified.tzinfo)
if if_since >= modified:
self.set_status(304)
@ -154,7 +145,7 @@ class GridFSHandler(tornado.web.RequestHandler):
# Same for Etag
etag = self.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:
self.set_status(304)
return
@ -181,3 +172,4 @@ class GridFSHandler(tornado.web.RequestHandler):
def set_extra_headers(self, path, gridout):
"""For subclass to add extra headers to the response"""
pass

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