pax_global_header00006660000000000000000000000064146006052560014516gustar00rootroot0000000000000052 comment=19eeb29b98fac159952b6a0c3440c91d77ee276a motor-3.4.0/000077500000000000000000000000001460060525600126625ustar00rootroot00000000000000motor-3.4.0/.evergreen/000077500000000000000000000000001460060525600147225ustar00rootroot00000000000000motor-3.4.0/.evergreen/config.yml000066400000000000000000000756331460060525600167300ustar00rootroot00000000000000######################################## # Evergreen Template for MongoDB Drivers ######################################## # When a task that used to pass starts to fail # Go through all versions that may have been skipped to detect # when the task started failing stepback: true # Mark a failure as a system/bootstrap failure (purple box) rather then a task # failure by default. # Actual testing tasks are marked with `type: test` command_type: system # Protect ourself against rogue test case, or curl gone wild, that runs forever # Good rule of thumb: the averageish length a task takes, times 5 # That roughly accounts for variable system performance for various buildvariants exec_timeout_secs: 1800 # 6 minutes is the longest we'll ever run # What to do when evergreen hits the timeout (`post:` tasks are run automatically) timeout: - command: shell.exec params: script: | ls -la functions: "fetch source": # Executes git clone and applies the submitted patch, if any - command: git.get_project params: directory: "src" # Make an evergreen exapanstion file with dynamic values - command: shell.exec params: working_dir: "src" script: | set -o xtrace # Get the current unique version of this checkout if [ "${is_patch}" = "true" ]; then CURRENT_VERSION=$(git describe)-patch-${version_id} else CURRENT_VERSION=latest fi export DRIVERS_TOOLS="$(pwd)/../drivers-tools" export PROJECT_DIRECTORY="$(pwd)" # Python has cygwin path problems on Windows. Detect prospective mongo-orchestration home directory if [ "Windows_NT" = "$OS" ]; then # Magic variable in cygwin export DRIVERS_TOOLS=$(cygpath -m $DRIVERS_TOOLS) export PROJECT_DIRECTORY=$(cygpath -m $PROJECT_DIRECTORY) fi export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration" export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin" export UPLOAD_BUCKET="${project}" cat < expansion.yml CURRENT_VERSION: "$CURRENT_VERSION" DRIVERS_TOOLS: "$DRIVERS_TOOLS" MONGO_ORCHESTRATION_HOME: "$MONGO_ORCHESTRATION_HOME" MONGODB_BINARIES: "$MONGODB_BINARIES" UPLOAD_BUCKET: "$UPLOAD_BUCKET" PROJECT_DIRECTORY: "$PROJECT_DIRECTORY" PREPARE_SHELL: | set -o errexit export DRIVERS_TOOLS="$DRIVERS_TOOLS" export MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME" export MONGODB_BINARIES="$MONGODB_BINARIES" export UPLOAD_BUCKET="$UPLOAD_BUCKET" export PROJECT_DIRECTORY="$PROJECT_DIRECTORY" export TMPDIR="$MONGO_ORCHESTRATION_HOME/db" export PATH="$MONGODB_BINARIES:$PATH" export PROJECT="${project}" export ASYNC_TEST_TIMEOUT=30 export PIP_QUIET=1 EOT # See what we've done. cat expansion.yml # Load the expansion file to make an evergreen variable with the current unique version - command: expansions.update params: file: src/expansion.yml "prepare resources": - command: shell.exec params: working_dir: "src" script: | ${PREPARE_SHELL} rm -rf $DRIVERS_TOOLS if [ "${project}" = "drivers-tools" ]; then # If this was a patch build, doing a fresh clone would not actually test the patch cp -R ${PROJECT_DIRECTORY}/ $DRIVERS_TOOLS else git clone https://github.com/mongodb-labs/drivers-evergreen-tools.git $DRIVERS_TOOLS fi echo "{ \"releases\": { \"default\": \"$MONGODB_BINARIES\" }}" > $MONGO_ORCHESTRATION_HOME/orchestration.config "upload release": - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: ${project}.tar.gz remote_file: ${UPLOAD_BUCKET}/${project}-${CURRENT_VERSION}.tar.gz bucket: mciuploads permissions: public-read content_type: ${content_type|application/x-gzip} # Upload build artifacts that other tasks may depend on # Note this URL needs to be totally unique, while predictable for the next task # so it can automatically download the artifacts "upload build": # Compress and upload the entire build directory - command: archive.targz_pack params: # Example: mongo_c_driver_releng_9dfb7d741efbca16faa7859b9349d7a942273e43_16_11_08_19_29_52.tar.gz target: "${build_id}.tar.gz" source_dir: ${PROJECT_DIRECTORY}/ include: - "./**" - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: ${build_id}.tar.gz # Example: /mciuploads/${UPLOAD_BUCKET}/gcc49/9dfb7d741efbca16faa7859b9349d7a942273e43/debug-compile-nosasl-nossl/mongo_c_driver_releng_9dfb7d741efbca16faa7859b9349d7a942273e43_16_11_08_19_29_52.tar.gz remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${task_name}/${build_id}.tar.gz bucket: mciuploads permissions: public-read content_type: ${content_type|application/x-gzip} "fetch build": - command: shell.exec params: continue_on_err: true script: "set -o xtrace && rm -rf ${PROJECT_DIRECTORY}" - command: s3.get params: aws_key: ${aws_key} aws_secret: ${aws_secret} remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${BUILD_NAME}/${build_id}.tar.gz bucket: mciuploads local_file: build.tar.gz - command: shell.exec params: continue_on_err: true # EVG-1105: Use s3.get extract_to: ./ script: "set -o xtrace && cd .. && rm -rf ${PROJECT_DIRECTORY} && mkdir ${PROJECT_DIRECTORY}/ && tar xf build.tar.gz -C ${PROJECT_DIRECTORY}/" "exec compile script" : - command: shell.exec type: test params: working_dir: "src" script: | ${PREPARE_SHELL} [ -f ${PROJECT_DIRECTORY}/${file} ] && BUILDTOOL="${buildtool}" sh ${PROJECT_DIRECTORY}/${file} || echo "${PROJECT_DIRECTORY}/${file} not available, skipping" "exec script" : - command: shell.exec type: test params: working_dir: "src" script: | ${PREPARE_SHELL} [ -f ${PROJECT_DIRECTORY}/${file} ] && sh ${PROJECT_DIRECTORY}/${file} || echo "${PROJECT_DIRECTORY}/${file} not available, skipping" "upload docs" : - command: shell.exec params: silent: true script: | export AWS_ACCESS_KEY_ID=${aws_key} export AWS_SECRET_ACCESS_KEY=${aws_secret} aws s3 cp ${PROJECT_DIRECTORY}/doc/html s3://mciuploads/${UPLOAD_BUCKET}/docs/${CURRENT_VERSION} --recursive --acl public-read --region us-east-1 - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: ${PROJECT_DIRECTORY}/doc/html/index.html remote_file: ${UPLOAD_BUCKET}/docs/${CURRENT_VERSION}/index.html bucket: mciuploads permissions: public-read content_type: text/html display_name: "Rendered docs" "upload coverage" : - command: shell.exec params: silent: true script: | export AWS_ACCESS_KEY_ID=${aws_key} export AWS_SECRET_ACCESS_KEY=${aws_secret} aws s3 cp ${PROJECT_DIRECTORY}/coverage s3://mciuploads/${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/coverage/ --recursive --acl public-read --region us-east-1 - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: ${PROJECT_DIRECTORY}/coverage/index.html remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/coverage/index.html bucket: mciuploads permissions: public-read content_type: text/html display_name: "Coverage Report" "upload scan artifacts" : - command: shell.exec type: test params: script: | cd if find ${PROJECT_DIRECTORY}/scan -name \*.html | grep -q html; then (cd ${PROJECT_DIRECTORY}/scan && find . -name index.html -exec echo "
  • {}
  • " \;) >> scan.html else echo "No issues found" > scan.html fi - command: shell.exec params: silent: true script: | export AWS_ACCESS_KEY_ID=${aws_key} export AWS_SECRET_ACCESS_KEY=${aws_secret} aws s3 cp ${PROJECT_DIRECTORY}/scan s3://mciuploads/${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/scan/ --recursive --acl public-read --region us-east-1 - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: ${PROJECT_DIRECTORY}/scan.html remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/scan/index.html bucket: mciuploads permissions: public-read content_type: text/html display_name: "Scan Build Report" "upload mo artifacts": - command: shell.exec params: script: | ${PREPARE_SHELL} find $MONGO_ORCHESTRATION_HOME -name \*.log | xargs tar czf mongodb-logs.tar.gz - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: mongodb-logs.tar.gz remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/logs/${task_id}-${execution}-mongodb-logs.tar.gz bucket: mciuploads permissions: public-read content_type: ${content_type|application/x-gzip} display_name: "mongodb-logs.tar.gz" - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: ${DRIVERS_TOOLS}/.evergreen/orchestration/server.log remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/logs/${task_id}-${execution}-orchestration.log bucket: mciuploads permissions: public-read content_type: ${content_type|text/plain} display_name: "orchestration.log" "upload working dir": - command: archive.targz_pack params: target: "working-dir.tar.gz" source_dir: ${PROJECT_DIRECTORY}/ include: - "./**" - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: working-dir.tar.gz remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/artifacts/${task_id}-${execution}-working-dir.tar.gz bucket: mciuploads permissions: public-read content_type: ${content_type|application/x-gzip} display_name: "working-dir.tar.gz" - command: archive.targz_pack params: target: "drivers-dir.tar.gz" source_dir: ${DRIVERS_TOOLS} include: - "./**" exclude_files: # Windows cannot read the mongod *.lock files because they are locked. - "*.lock" - command: s3.put params: aws_key: ${aws_key} aws_secret: ${aws_secret} local_file: drivers-dir.tar.gz remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/artifacts/${task_id}-${execution}-drivers-dir.tar.gz bucket: mciuploads permissions: public-read content_type: ${content_type|application/x-gzip} display_name: "drivers-dir.tar.gz" "upload test results": - command: attach.results params: file_location: "${DRIVERS_TOOLS}/results.json" - command: attach.xunit_results params: files: - "src/xunit-results/TEST-*.xml" - "src/xunit-synchro-results" "bootstrap mongo-orchestration": - command: shell.exec params: script: | ${PREPARE_SHELL} MONGODB_VERSION=${VERSION} \ TOPOLOGY=${TOPOLOGY} \ AUTH=${AUTH} \ SSL=${SSL} \ STORAGE_ENGINE=${STORAGE_ENGINE} \ bash ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh # run-orchestration generates expansion file with the MONGODB_URI for the cluster - command: expansions.update params: file: mo-expansion.yml "stop mongo-orchestration": - command: shell.exec params: script: | ${PREPARE_SHELL} bash ${DRIVERS_TOOLS}/.evergreen/stop-orchestration.sh "run tox": - command: ec2.assume_role params: role_arn: ${aws_test_secrets_role} - command: subprocess.exec params: working_dir: "src" binary: bash include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] args: - ${DRIVERS_TOOLS}/.evergreen/csfle/setup-secrets.sh - command: subprocess.exec params: working_dir: "src" binary: bash background: true args: - ${DRIVERS_TOOLS}/.evergreen/csfle/start-servers.sh - command: subprocess.exec params: working_dir: "src" binary: bash args: - ${DRIVERS_TOOLS}/.evergreen/csfle/await-servers.sh - command: shell.exec type: test params: working_dir: "src" script: | ${PREPARE_SHELL} LIBMONGOCRYPT_URL="${libmongocrypt_url}" \ TEST_ENCRYPTION=1 \ PYTHON_BINARY="${PYTHON_BINARY}" \ TOX_ENV="${TOX_ENV}" \ VIRTUALENV="${VIRTUALENV}" \ CHECK_EXCLUDE_PATTERNS=1 \ AUTH="${AUTH}" \ SSL="${SSL}" \ CERT_DIR="${DRIVERS_TOOLS}/.evergreen/x509gen" \ bash ${PROJECT_DIRECTORY}/.evergreen/run-tox.sh "run enterprise auth tests": - command: ec2.assume_role params: role_arn: ${aws_test_secrets_role} - command: shell.exec type: test params: working_dir: "src" include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] script: | bash ${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh drivers/enterprise_auth - command: shell.exec type: test params: working_dir: "src" script: | set +x # disable xtrace CLIENT_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem \ CA_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem \ PYTHON_BINARY=${PYTHON_BINARY} \ TOX_ENV=${TOX_ENV} \ PROJECT_DIRECTORY=${PROJECT_DIRECTORY} \ bash ${PROJECT_DIRECTORY}/.evergreen/run-enterprise-auth-tests.sh "cleanup": - command: shell.exec params: script: | ${PREPARE_SHELL} rm -rf $DRIVERS_TOOLS || true "fix absolute paths": - command: shell.exec params: script: | ${PREPARE_SHELL} for filename in $(find ${DRIVERS_TOOLS} -name \*.json); do perl -p -i -e "s|ABSOLUTE_PATH_REPLACEMENT_TOKEN|${DRIVERS_TOOLS}|g" $filename done "windows fix": - command: shell.exec params: script: | ${PREPARE_SHELL} for i in $(find ${DRIVERS_TOOLS}/.evergreen ${PROJECT_DIRECTORY}/.evergreen -name \*.sh); do cat $i | tr -d '\r' > $i.new mv $i.new $i done # Copy client certificate because symlinks do not work on Windows. cp ${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem ${MONGO_ORCHESTRATION_HOME}/lib/client.pem "make files executable": - command: shell.exec params: script: | ${PREPARE_SHELL} for i in $(find ${DRIVERS_TOOLS}/.evergreen ${PROJECT_DIRECTORY}/.evergreen -name \*.sh); do chmod +x $i done "init test-results": - command: shell.exec params: script: | ${PREPARE_SHELL} echo '{"results": [{ "status": "FAIL", "test_file": "Build", "log_raw": "No test-results.json found was created" } ]}' > ${PROJECT_DIRECTORY}/test-results.json "install dependencies": - command: shell.exec params: working_dir: "src" script: | ${PREPARE_SHELL} file="${PROJECT_DIRECTORY}/.evergreen/install-dependencies.sh" # Don't use ${file} syntax here because evergreen treats it as an empty expansion. [ -f "$file" ] && sh $file || echo "$file not available, skipping" pre: - func: "fetch source" - func: "prepare resources" - func: "windows fix" - func: "fix absolute paths" - func: "init test-results" - func: "make files executable" - func: "install dependencies" post: # Disabled, causing timeouts # - func: "upload working dir" - func: "upload mo artifacts" - func: "upload test results" - func: "stop mongo-orchestration" - func: "cleanup" tasks: # Wildcard task. Do you need to find out what tools are available and where? # Throw it here, and execute this task on all buildvariants - name: getdata commands: - command: shell.exec type: test params: script: | set -o xtrace . ${DRIVERS_TOOLS}/.evergreen/download-mongodb.sh || true get_distro || true echo $DISTRO echo $MARCH echo $OS uname -a || true ls /etc/*release* || true cc --version || true gcc --version || true clang --version || true gcov --version || true lcov --version || true llvm-cov --version || true echo $PATH ls -la /usr/local/Cellar/llvm/*/bin/ || true ls -la /usr/local/Cellar/ || true scan-build --version || true genhtml --version || true valgrind --version || true # Test tasks {{{ - name: "test-3.6-standalone" tags: ["3.6", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.6" TOPOLOGY: "server" - func: "run tox" - name: "test-3.6-replica_set" tags: ["3.6", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.6" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-3.6-sharded_cluster" tags: ["3.6", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.6" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-4.0-standalone" tags: ["4.0", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.0" TOPOLOGY: "server" - func: "run tox" - name: "test-4.0-replica_set" tags: ["4.0", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.0" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-4.0-sharded_cluster" tags: ["4.0", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.0" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-4.2-standalone" tags: ["4.2", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.2" TOPOLOGY: "server" - func: "run tox" - name: "test-4.2-replica_set" tags: ["4.2", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.2" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-4.2-sharded_cluster" tags: ["4.2", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.2" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-4.4-standalone" tags: ["4.4", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.4" TOPOLOGY: "server" - func: "run tox" - name: "test-4.4-replica_set" tags: ["4.4", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.4" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-4.4-sharded_cluster" tags: ["4.4", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.4" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-5.0-standalone" tags: ["5.0", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "5.0" TOPOLOGY: "server" - func: "run tox" - name: "test-5.0-replica_set" tags: ["5.0", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "5.0" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-5.0-sharded_cluster" tags: ["5.0", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "5.0" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-6.0-standalone" tags: [ "6.0", "standalone" ] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "6.0" TOPOLOGY: "server" - func: "run tox" - name: "test-6.0-replica_set" tags: [ "6.0", "replica_set" ] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "6.0" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-6.0-sharded_cluster" tags: [ "6.0", "sharded_cluster" ] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "6.0" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-7.0-standalone" tags: [ "7.0", "standalone" ] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "7.0" TOPOLOGY: "server" - func: "run tox" - name: "test-7.0-replica_set" tags: [ "7.0", "replica_set" ] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "7.0" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-7.0-sharded_cluster" tags: [ "7.0", "sharded_cluster" ] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "7.0" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-latest-standalone" tags: ["latest", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "latest" TOPOLOGY: "server" - func: "run tox" - name: "test-latest-replica_set" tags: ["latest", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "latest" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-latest-sharded_cluster" tags: ["latest", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "latest" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-rapid-standalone" tags: ["rapid", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "rapid" TOPOLOGY: "server" - func: "run tox" - name: "test-rapid-replica_set" tags: ["rapid", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "rapid" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-rapid-sharded_cluster" tags: ["rapid", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "rapid" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-enterprise-auth" tags: ["pr"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "latest" TOPOLOGY: "server" - func: "run enterprise auth tests" - name: "docs" tags: ["pr"] commands: - func: "run tox" vars: TOX_ENV: docs - name: "doctest" tags: ["pr"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "5.0" TOPOLOGY: "server" - func: "run tox" vars: TOX_ENV: doctest - name: "assign-pr-reviewer" tags: ["pr"] allowed_requesters: ["patch", "github_pr"] commands: - command: shell.exec type: test params: shell: "bash" working_dir: src script: | ${PREPARE_SHELL} set -x export CONFIG=$PROJECT_DIRECTORY/.github/reviewers.txt export SCRIPT="$DRIVERS_TOOLS/.evergreen/github_app/assign-reviewer.sh" bash $SCRIPT -p $CONFIG -h ${github_commit} -o "mongodb" -n "motor" echo '{"results": [{ "status": "PASS", "test_file": "Build", "log_raw": "Test completed" } ]}' > ${PROJECT_DIRECTORY}/test-results.json # }}} axes: - id: ssl display_name: SSL values: - id: ssl display_name: SSL variables: SSL: "ssl" AUTH: "auth" - id: nossl display_name: NoSSL variables: SSL: "nossl" AUTH: "noauth" - id: tox-env display_name: "Tox Env RHEL8" values: - id: "test-pypy38" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/pypy3.8/bin/python3" - id: "test-py37" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "test-py37" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "test-py38" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.8/bin/python3" - id: "test-py39" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.9/bin/python3" - id: "test-py310" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.10/bin/python3" - id: "test-py311" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.11/bin/python3" - id: "test-py312" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.12/bin/python3" - id: "test-pymongo-latest" variables: TOX_ENV: "test-pymongo-latest" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "synchro-py37" variables: TOX_ENV: "synchro" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "synchro-py312" variables: TOX_ENV: "synchro" PYTHON_BINARY: "/opt/python/3.12/bin/python3" - id: tox-env-rhel7 display_name: "Tox Env RHEL7" values: - id: "test" variables: TOX_ENV: "test" PYTHON_BINARY: "/opt/python/3.9/bin/python3" # Test Python 3.8 only on Mac. - id: tox-env-osx display_name: "Tox Env OSX" values: - id: "test" variables: TOX_ENV: "test" PYTHON_BINARY: "/Library/Frameworks/Python.framework/Versions/3.8/bin/python3" - id: tox-env-win display_name: "Tox Env Windows" values: - id: "test-py37" variables: TOX_ENV: "test" PYTHON_BINARY: "c:/python/Python37/python.exe" - id: "test-py38" variables: TOX_ENV: "test" PYTHON_BINARY: "c:/python/Python39/python.exe" - id: "test-py39" variables: TOX_ENV: "test" PYTHON_BINARY: "c:/python/Python39/python.exe" - id: "test-py310" variables: TOX_ENV: "test" PYTHON_BINARY: "c:/python/Python310/python.exe" - id: "test-py311" variables: TOX_ENV: "test" PYTHON_BINARY: "c:/python/Python311/python.exe" - id: "test-py312" variables: TOX_ENV: "test" PYTHON_BINARY: "c:/python/Python312/python.exe" - id: os display_name: "Operating System" values: - id: "rhel84" display_name: "RHEL 8.4" run_on: "rhel84-small" - id: "rhel76" display_name: "RHEL 7.6" run_on: "rhel76-small" - id: "win" display_name: "Windows" run_on: "windows-64-vsMulti-small" - id: "macos-1100" display_name: "macOS 11.00" run_on: "macos-1100" buildvariants: # Main test matrix. - matrix_name: "main" display_name: "${os}-${tox-env}-${ssl}" matrix_spec: os: "rhel84" tox-env: "*" ssl: "*" exclude_spec: # TODO: synchro needs PyMongo's updated SSL test certs, # which may require Motor test suite changes. - os: "*" tox-env: ["synchro-py37", "synchro-py312"] ssl: "ssl" tasks: - ".rapid" - ".latest" - ".7.0" - ".6.0" - ".5.0" - ".4.4" - ".4.2" - ".4.0" - ".3.6" - matrix_name: "test-rhel7" display_name: "${os}-${tox-env-rhel7}-${ssl}" matrix_spec: os: "rhel76" tox-env-rhel7: "*" ssl: "*" tasks: - ".rapid" - ".latest" - ".7.0" - ".6.0" - ".5.0" - ".4.4" - matrix_name: "test-win" display_name: "${os}-${tox-env-win}-${ssl}" matrix_spec: os: "win" tox-env-win: "*" ssl: "*" tasks: - ".rapid" - ".latest" - ".7.0" - ".6.0" - ".5.0" - ".4.4" - ".4.2" - ".4.0" - ".3.6" - matrix_name: "test-macos" display_name: "${os}-${tox-env-osx}-${ssl}" matrix_spec: os: "macos-1100" tox-env-osx: "*" ssl: "*" tasks: - ".rapid" - ".latest" - ".7.0" - ".6.0" - matrix_name: "enterprise-auth" display_name: "Enterprise Auth-${tox-env}" matrix_spec: {"tox-env": ["synchro-py37", "synchro-py312"], ssl: "ssl"} run_on: - "rhel84-small" tasks: - name: "test-enterprise-auth" - name: "docs" display_name: "Docs - Build" run_on: - "rhel84-small" expansions: TOX_ENV: "docs" PYTHON_BINARY: "/opt/python/3.7/bin/python3" tasks: - name: "docs" - name: "doctests" display_name: "Docs - Test" run_on: - "rhel84-small" expansions: TOX_ENV: "doctest" PYTHON_BINARY: "/opt/python/3.7/bin/python3" tasks: - name: "doctest" - name: rhel8-pr-assign-reviewer display_name: Assign PR Reviewer run_on: rhel87-small tasks: - name: "assign-pr-reviewer" motor-3.4.0/.evergreen/install-dependencies.sh000066400000000000000000000005601460060525600213510ustar00rootroot00000000000000#!/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 if [ -w /etc/hosts ]; then SUDO="" else SUDO="sudo" fi # Add 'server' and 'hostname_not_in_cert' as a hostnames echo "127.0.0.1 server" | $SUDO tee -a /etc/hosts echo "127.0.0.1 hostname_not_in_cert" | $SUDO tee -a /etc/hosts motor-3.4.0/.evergreen/run-enterprise-auth-tests.sh000066400000000000000000000014701460060525600223410ustar00rootroot00000000000000#!/bin/bash # Don't trace to avoid secrets showing up in the logs set -o errexit set +x echo "Running enterprise authentication tests" source ./secrets-export.sh export DB_USER="bob" export DB_PASSWORD="pwd123" # BUILD-3830 touch ${PROJECT_DIRECTORY}/.evergreen/krb5.conf.empty export KRB5_CONFIG=${PROJECT_DIRECTORY}/.evergreen/krb5.conf.empty echo "Writing keytab" echo ${KEYTAB_BASE64} | base64 -d > ${PROJECT_DIRECTORY}/.evergreen/drivers.keytab echo "Running kinit" kinit -k -t ${PROJECT_DIRECTORY}/.evergreen/drivers.keytab -p ${PRINCIPAL} echo "Setting GSSAPI variables" export GSSAPI_HOST=${SASL_HOST} export GSSAPI_PORT=${SASL_PORT} export GSSAPI_PRINCIPAL=${PRINCIPAL} # Pass needed env variables to the test environment. export TOX_ENV="enterprise-synchro" bash ${PROJECT_DIRECTORY}/.evergreen/run-tox.sh motor-3.4.0/.evergreen/run-tox.sh000077500000000000000000000046131460060525600167010ustar00rootroot00000000000000#!/bin/bash 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. 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" fi if [ "$SSL" != "nossl" ]; then export CLIENT_PEM="$DRIVERS_TOOLS/.evergreen/x509gen/client.pem" export CA_PEM="$DRIVERS_TOOLS/.evergreen/x509gen/ca.pem" fi if [ -f secrets-export.sh ]; then source secrets-export.sh fi # Usage: # createvirtualenv /path/to/python /output/path/for/venv # * param1: Python binary to use for the virtualenv # * param2: Path to the virtualenv to create createvirtualenv () { PYTHON=$1 VENVPATH=$2 if $PYTHON -m virtualenv --version; then VIRTUALENV="$PYTHON -m virtualenv" elif $PYTHON -m venv -h > /dev/null; then # System virtualenv might not be compatible with the python3 on our path VIRTUALENV="$PYTHON -m venv" else echo "Cannot test without virtualenv" exit 1 fi # Workaround for bug in older versions of virtualenv. $VIRTUALENV $VENVPATH || $PYTHON -m venv $VENVPATH if [ "Windows_NT" = "$OS" ]; then # Workaround https://bugs.python.org/issue32451: # mongovenv/Scripts/activate: line 3: $'\r': command not found dos2unix $VENVPATH/Scripts/activate || true . $VENVPATH/Scripts/activate else . $VENVPATH/bin/activate fi python -m pip install -q --upgrade pip python -m pip install -q --upgrade setuptools wheel tox } if $PYTHON_BINARY -m tox --version; then run_tox() { $PYTHON_BINARY -m tox -m $TOX_ENV "$@" } else # No toolchain present, set up virtualenv before installing 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 "$@" } fi run_tox "${@:1}" motor-3.4.0/.git-blame-ignore-revs000066400000000000000000000001071460060525600167600ustar00rootroot00000000000000# Initial pre-commit reformat 1e62b868ea58afeb42b3d0346e33776561c16ab6 motor-3.4.0/.github/000077500000000000000000000000001460060525600142225ustar00rootroot00000000000000motor-3.4.0/.github/dependabot.yml000066400000000000000000000003061460060525600170510ustar00rootroot00000000000000version: 2 updates: # GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: actions: patterns: - "*" motor-3.4.0/.github/reviewers.txt000066400000000000000000000001351460060525600167750ustar00rootroot00000000000000# List of reviewers for auto-assignment of reviews. caseyclements blink1073 Jibola NoahStapp motor-3.4.0/.github/workflows/000077500000000000000000000000001460060525600162575ustar00rootroot00000000000000motor-3.4.0/.github/workflows/release.yml000066400000000000000000000021741460060525600204260ustar00rootroot00000000000000name: Build and Publish Dist Files to PyPI concurrency: group: release-${{ github.ref }} cancel-in-progress: true on: workflow_dispatch: 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@v4 - name: Set up Python uses: actions/setup-python@v5 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@v4 with: name: dist path: dist publish: needs: build name: "Publish Dist" if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - name: Retrieve package artifacts uses: actions/download-artifact@v4 - name: Upload packages uses: pypa/gh-action-pypi-publish@release/v1 motor-3.4.0/.github/workflows/test-python.yml000066400000000000000000000060001460060525600212740ustar00rootroot00000000000000name: 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-20.04] python-version: ["3.7", "3.12"] fail-fast: false name: CPython ${{ matrix.python-version }}-${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: 'pyproject.toml' allow-prereleases: true - name: Start MongoDB with Custom Options run: | mkdir data mongod --fork --dbpath=$(pwd)/data --logpath=$PWD/mongo.log --setParameter enableTestCommands=1 - name: Install Python dependencies run: | python -m pip install -U pip tox - name: Run tests run: | tox -m test lint: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.8 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 tox -m manifest docs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.7 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 with Custom Options run: | mkdir data mongod --fork --dbpath=$(pwd)/data --logpath=$PWD/mongo.log --setParameter enableTestCommands=1 - name: Run doctest run: tox -m doctest release: runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.7 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@v4 - name: Set up Python uses: actions/setup-python@v5 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 motor-3.4.0/.gitignore000066400000000000000000000002071460060525600146510ustar00rootroot00000000000000*~ *#* .DS* *.pyc *.pyd build/ dist/ motor.egg-info/ setup.cfg *.egg .tox doc/_build/ .idea/ xunit-results xunit-synchro-results .eggs motor-3.4.0/.pre-commit-config.yaml000066400000000000000000000043701460060525600171470ustar00rootroot00000000000000 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-toml - id: check-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.27.0 hooks: - id: check-jsonschema name: "Check GitHub Workflows" files: ^\.github/workflows/ types: [yaml] args: ["--schemafile", "https://json.schemastore.org/github-workflow"] - 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"] motor-3.4.0/.readthedocs.yaml000066400000000000000000000010001460060525600161000ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the doc/ directory with Sphinx sphinx: configuration: doc/conf.py # Set the version of Python and requirements required to build the docs. python: install: # Install motor itself. - method: pip path: . - requirements: doc/docs-requirements.txt build: os: ubuntu-22.04 tools: python: "3.11" motor-3.4.0/CONTRIBUTING.md000066400000000000000000000037201460060525600151150ustar00rootroot00000000000000# 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.7+ 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 :) motor-3.4.0/LICENSE000066400000000000000000000261351460060525600136760ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. motor-3.4.0/MANIFEST.in000066400000000000000000000013351460060525600144220ustar00rootroot00000000000000include README.md include LICENSE include tox.ini include pyproject.toml include doc/Makefile include doc/examples/tornado_change_stream_templates/index.html recursive-include doc *.rst recursive-include doc *.py recursive-include doc *.png recursive-include test *.py recursive-include test *.pem recursive-include doc *.conf recursive-include doc *.css recursive-include doc *.js recursive-include doc *.txt recursive-include doc *.bat recursive-include synchro *.py recursive-include motor *.pyi recursive-include motor *.typed recursive-include motor *.py exclude .readthedocs.yaml exclude .git-blame-ignore-revs exclude .pre-commit-config.yaml exclude release.sh exclude RELEASE.md exclude CONTRIBUTING.md exclude .evergreen/* motor-3.4.0/README.md000066400000000000000000000132011460060525600141360ustar00rootroot00000000000000# 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) ## 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.1,<5 - Python 3.7+ 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.7+. 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. motor-3.4.0/RELEASE.md000066400000000000000000000056041460060525600142710ustar00rootroot00000000000000# 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. ## 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: . 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 " $ git tag -a "" -m "BUMP " $ # Bump to dev version number $ git commit -a -m "BUMP " $ git push $ git push --tags 5. Bump the version number to `.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 . If the new version does not show up automatically, trigger a rebuild of "latest": 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: 10. Create a GitHub Release for the tag using . 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." motor-3.4.0/doc/000077500000000000000000000000001460060525600134275ustar00rootroot00000000000000motor-3.4.0/doc/Makefile000066400000000000000000000060641460060525600150750ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Motor.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Motor.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." motor-3.4.0/doc/__init__.py000066400000000000000000000000001460060525600155260ustar00rootroot00000000000000motor-3.4.0/doc/_static/000077500000000000000000000000001460060525600150555ustar00rootroot00000000000000motor-3.4.0/doc/_static/motor.png000066400000000000000000001576221460060525600167400ustar00rootroot00000000000000‰PNG  IHDR·aÆþtEXtSoftwareAdobe ImageReadyqÉe<ñiTXtXML:com.adobe.xmp ¡a‘ PLTE‡¥¶·Þõ˜Ëçù0.Þ‰,ÔÒÒ)KZ­j'ÿþjî,,º«Fûébhz‡þ­Pø–(Âìÿ1Zkÿõ_•„5jY#ÿÿn+ÞÒY‰§¸#"³±±¯ÖìéÚZÈ-*±ÃõN6ÿ÷‘ØÉT415¿çþ7EJ2ª)%Ç·K{k+ABIÃx)ZI»Î]itŸ;ì!(”´Èäãã‘Åàúi=~•¤ÿù«­C«¿ ÿ—K:cvޝÁ4! ÿ÷…ÿûj@gzÿûÆS\d•¶Éÿž)¼ãû77= —¶ËøïgÏêúPSZûüþ»âøWblFIP‰$ ‘¯Â³9*wœcr|ÿÙ^øñja8J|”òã_'=HJOW%KZÿÌ[nŽ«¼õõõ±Ûó!0:çôýªËßRt‹˜… ³’³Å'/ÿ§2¬Ð傜¬’³Çš]"ÿ›(ÂÝp…’ÿýâÿõt‹„…lcd*O_ÿùiðèg|’¡‹©ºÿòfûÊL5Rbü‚E£ÁÔöúü—˜rˆ•Þ2.½Õü›-xx~š¬ÿÿò† ±Çðÿ¶Ûïzž/*-Üïü'BOÿömÿïe'M]ÏÀPÁåù3JW +#'üõlóÊ[™³Ä‰µÍ=]llO>DmƒŠ«¼s•§(FU‰x1…šªœÏë3]oüõg÷—.„ž­ñ60"úólõ#*RJJî(*…wEkŠ›™¨÷ølaYZ==B¦¡£€§¼/DPtll‚˜¦ö,,«ÀÃÀÀ?,þ™'‰ž­úúm…¢´¨ÇÛÿ¾Pÿ—%é2/‡œ¬¡Öòÿ10w™®3Wi‹QãÞev †³È‹ °ê&Œ¹Ñïðm¢Êàž¯ÿõh8`r ÿõeÿ÷h‚œ¯ƒ ±„ž±„ž¯ž²ÿ÷hŒ£³ÿõhƒ ¯ííí„®Ãÿ¡*”ŽŽ¿¶WiOV_ìøý¨·ö‘&ðððÿõe”¬»ÿ÷eÑD-Hw¹à÷Ž­¿ÿôgÿÿÿÿôh„¼/ù\Ô=ºÞ¿ò>Õß™«ÞÝ;½íŠË®ºµô“/+%«.»‚ÌþTÝ÷œáS¥êÃòªoÉz.õîÐÜ,¼mê=mª¿Í{ìâ?Ä;Pš„ºžhˆúQy„Pu™V/NÑ7ëê×ÒçH!FðZ.¸T¼[/CôƒQ~¢ß•~ê2¡ÜzZ¹õ²ÛfÏžM½÷ DŸ]#rða\ïœèû'ò1ïNͧÔ_Ñ?@¼—¥ª!>š¼§Þ ðñtÕw'þ’è·Ò¢oFõI¦è,ˆï"T‹†¸žªsñõ‰Ñ)S'/ñƒú§€»úÑÙŸ^qÕ&È­_u™n€ç Ι«•|rs†¨znÞg#Þ‚ú¥Í€8®ÔHtmoˆz#bÞÑ~u ‰˜sE>õÆj[çju7§Ñ³ÆòtPW9Bá ð‰ {§í‚~ÕÙ³o»lBüà2w6…ŽšÏ¤ž$>ÀúDÔ<c°Á)õ>1ѽŽ9­Þ£ó„J]háqðŽ –ÏÓ8èä=˜GêyGלi#cØôãSVj·÷ ÊvRøœˆV]¸\óúˆ|:ûŠ«ÆÈ­WÌþÔ~¾ös§(‘k96c-õÓ¶šb³¤µŒ‚' ¬•¥QÖ½`U5 Ú+é÷&êü-UuÈ\¨Ëè™YgMë–çõÕ'41€ n’¢S«r¬îìÙ—ý`\Fg›h¬ˆ4ÏÔ…¶ Ù"×8z»PøÑ¨‹4 è](*`•àÏ¥»ÈØ)SOß38x>‚Ž¿<Ç ïÄŠFF”ºø'Ñá¤òz!Ñ&Ó•ïàQxÊù;íÖ’¹l¶ñW¶¬‘²ÀÃeÎ)¡ö!‡Î…ž‡‚X"Š<Œ‹nC?Sj¾…ªPËØ}ªßšx6“‚ØÜ O¸ ž¦õ© k‚§†q™hNÇáÊ,{OÄ—æQ©[óB¹Ù?¼ª„@¾:›Ú 6œÈAPŠOŽú̆*8äÆÝ²ô„*3 Íž%u uLÿ­ ü(ÕAá—N”57¾Ë5¾Ê2€^@O‘™$ ª' b?©xæ&ÕòBuu*½‡y•O RÆ…ªMü.›ZÍ ! G$® 6ð„ÈØÀ“ çìŒ#÷¢5cȰïD}“Ðx¡ŽýLÂéý‘Q§'b<ï €˜"¦ý·Q$dk©å׈‹ … ŠAŠÎ|;½ÊÈe³)°€ 9@NÄ“0’ƒ¶Ôê5ïxÏRqBñÍy.€Àü§ö&ש…oïݶ9+ĤÑÞó…É+U(û­H W͆îÉ…¯/ï›ÀÅíÐÙ‚ {qóÔô# æC`gµŽ€ …G™ ÝE•‚\ÅE†Ö«ÙPbÔÿ40Æ €Ê (H,rw@CÞEd”‚Ü@ª´¸ãÙ·ýäVò©KQ!ÈÄ&:Q% \àbÓ壃– Fhðû¼z™¼3в<  rl´(Nß´‰´ºèäP3¼LA’«*m(Â1£g„¡ŽéÄD×# (2)^FÝÙ_¹b¶É¡à®ˆ }‚‰X@)PûsTµ_p]xf¨U ‹NÇM½ÌûŒ(Fv LÃr.géW†¹ÏD00)÷Š )‡UùmY$r"Ç’Uv™}•ÈU³A5ŠzQ9EåÛPÃR-Š,¨‰Xl3ϵ•A;l4Š~ÆÈF¹ºˆKMq™/hÕaÚC\[ÁÑCk쥿ÚÄ벘• $>e3ªò)ùÈn›M-+ ª. ‡ß$…jçªÔ²÷ ï¢8ù&H_P͔‡oJç.>Á 5ò„ΖšLb'ä ³I°à`¾aJ<úÕÈ2pÖˆ@ I¹1 ×úc»³/S¹l6¼-Xº%*ñ™æQ«¸NMýû|B¨m¸A1’j£ëRËèrˆzÚžPx38…!(ñ>eE(j¹€ÊõÜIDÌ+y?GL.L­R‰ ²lŠl%Î÷\¾~JnùÁmŸºd¡þÁòÞ@A2 p¬KxpceÍMZ‰&T ¤jN;$/ $Úg(ª$ÀZ11’ .¶£ÔÊÑ}.Êë±Áö1%I”š{µ9X©!Æ}s)ãÄxètQ¼B½f—UpÓy!ž÷A•;ïAÖÇêR°n+û*Å©¸[Ð3÷ÌœvìÄ?áò8(uR×*|QÓB0…{±€#!hd$‘Áñ®„¹Ì‰D/8]Ë,•ÈhÉz EE%Škû¨¤êË¢,Ô’éëÂL6Ê¾Š ƒ~«ƒ’Q¤î8'Áµ£B× ¬$•ñÄÏñ]KÌÅÿ÷‰'λtåEͽ`& 0)2¹`õvm01°–ÀÜX6„û„PŒä%Q`5Là e` {*4„B'Šä‰]Òõiê%—ºÀcRÆPBˆÉ/±rDB‘@PÆ€H½ÁÄõ7gÑêbÄ9Ó+5PyôÑ9ç³p0ØëâV¡‹‘¸öŒ'Œc„°yèˆel]»!La›Îêy)žË„ ¡£^¤à/Ôö@ZèJЦ$C©¨†£zHâ¸Ä×\‘¿õo_¹d@<ÜȺ¤`Š«ç¬¼>˜€=jÅvŸ›øÍ¼k¡ø-ã@8Ž¿ÙE-48U®4Y0U1æÐ`')ôÄ×E9ØT%.qÁе1^¯@‡®Ö(Mµ"qõy•_T¶\*/¾ 8B)téàŠò0°ª”#pé>$… +<•A藍´rêvuÚ߆3 …°5jÍy z… )+Ñ€ÓeBQ›NéYâöGÿSÄ¡d2pÞHÂõK nÎF¶¡¦¥&&¸®¬Z‰jžˆÀ!S u#è± ÁJ Á@jw6aæe©t”ëa6IaKÍ.Šíí0¬ƒ½b` G!R(R¹\.†®œ¸ì¯ ¿H–ܞЭýÁ D>Pe±] Á§àãyj×)\-Æa§©«›a¯ ¹pÝÂ¥öIGXx˜oQŒãÅqªêÔÿ¢¨š¢‚âà9JÅXh˦_üÑY?:°ëηØu.¸ÞRÿ#øÚ®]?zqÓ–í‡b) gÉ/z­B1JË!>…Ä¥”Ú¸Dý ‰¯ £:{–Ìd«T:uÜ*2A4…ZšMÔî‘P•&ª·H©ÊSd )!Vi†h•xT$µåÀ¹O½÷”ºÎå¿ÐõÔ¹öWÄWùõÞ{O{ç.Ü+zy"‡Bhɦk ç 5F…_Hà¢)2×ׂ:¸È ‰ AQîƒkÈºï šÄŸõP c¦hâƒ\ÐL†ƒèÌ%.HñgÛõzÞo¥€×¹BIŒ¶Ñ¼÷Þ¹»¶ä¤DRs(O´=ŸN¨ ¢Î6BT† p,.õMc¹%ꞸXˆ­›¸t”\–Ÿë‰'¬¼Š[P„Žâo×Ê¡")ÙPp%WÂãïCÄOþ{ï½÷uv])ÉÔ/q±¿ê)ùý@(om—*rq ø˜[Á Á¬¦CGìÚ" ¤ ¾Š,¥¸‡áx.L ‰ÏaãY 2~â+QjÁ¹JcžAÁÃAFzaŸÙ…ÀËàÅ\ ™§´f¼wåc}æ]ÿ¤þ_úì³Ç{ìʯ¿g„òÔSB"Nåõ½®å¾„í  xº¨ /…wG^ú`)VmÆo{,ôƒtêÕu­Ž®I$² 1Œ±Aí ¬Q öo$Ñ4¦“Ö+\È…J O] žöé_}ݸzaµœs‚„ZY:IÈÌVÍÚ˜ð5œ½ÛA#/¨"@Õ*[ʹJAë•à© !_ÙUÀé2åÄ ô‰vnª„¸AGFTÉ•(œ˜µààŠYìq”°½û+™@Š»¤@žúì‹^L$òzïBi³°ÂÇþÛ 7¬Zqðð²e3fÒØM8H8€Œ?JwÑLz®•®)F&ËLýSŠ+³”Pß°kO—0]qžÔGûÝlu$n˜±Œ=‡7z½ò´É…s¹OÏÉóýÖc_X ŸýÓ•J"wòtŽþÎí‘Dpã¬3–-;{L d‹r"Þgè Bqôz)t`檃WìïõL®¥Gq1c‹N…©Eèm¡!*ž#õ¡Íd-°kèˆöÈ|С‰^åZ<°â°T‡™Õ¡Y¡¿JØÜçpl’ì׿”@>S~èAéD^ó*3ûW\6\‡ìWŸ)02k“Ô»{‚.¥Ý@¬‚"BèìÅ]±ÃŽùÁVµ,ê›Ý5'êoà¢4¡þúMܰ€=ø^Ò‘Àyß]0«7 ¾bx ×rÉ«¹@^|ê˺a³Þ‚N„Œð7IôŠg.ÏòƒùVôŠsÙ¼aÅaqzV\<»Ä‘ vëQœ¥ÀB0׋xÏÕ™:à$ –¦PHT@},z0±»ñà»WíÌÈL%a·‚ì¡;È//;8KúvùΉ xZ¸ë©·¾´ a6K…¾Â‰ \àê±qÅ2Ï`^0k#¿¹e{Fø£ÌÜ3Cþ…5 üîØ¸Ÿx”A<£¦Ê­òkÓy§®Š¿‚×1,U ta‘€âskLE#¯Ai.±qÁ»ËÌ xóñdæAi²øoÇVÝ d¿ÑA)&'æÛuô¸(b‚¬s?ûì?ʼnˆÜp˜9—Ì:¨Å±â &~sËV˜¤\ýWLB”h°„e?Ó«Lgðõh¶ µ´,+pàå £‹«½£¶)µ>>I¥ˆkÏ?Y¼ì‰Ò=ËXHûF0!Æ£™„øC_±â]ùðÜHìOaÅþ=Êt°/F” ^Ê-VL¥_R žÙ¤ Œ,àöÞ’‰ƒåA7Äÿ²{ ¼±â]íæ—­J€°i$ظгUû1Àeg£‡·\«”A©íJ€É"‹I&šX BÙ]ÄpÛ‘°2Õ›˜uŒÅ÷{6JK”¸AœÁ‘UǤ’Þ³¿W $‘ðÎ䲫H@¾šôé2ê}ëÊ/+éDž:P5ø ¸—©sñF"H\õÿ÷³(ø°w+ü·Y/ÏfÊqÇ+f¼û.;4Ä€ŒNh•Ø M$S³2®Çó!çàbh¨Ñ”¯ŽÿP¡Ý4ÄIàf˜+¹ƒ»q—­ Š/J}Xuð˜°Ó «3ÉÂbYIYIJE 䟾¤@Tnø¯ž8«gŽÞX&ÝÖ¢ÖHüý—홹j†Tõqszø€«’®,‰åÑza@ƒ½”Øfh,Ϧ‚ñÊï ¿&~0" ˜¯Íµº‰V ŸG3™5:,³ )Y¸cÕ èÌ™¦ w•çGg¼Á}nïíܧ«¨÷ËútãD‰Ôp!ó_Ü¥³8P\tDÏà TØÅ£Ž;øWnì•¥®ý³Vð¢ÂŒ#®br½õMíQÜlµëž@üÈK˜CZ¼¨Oã7BíÖÿÕ›à¡>ÿè‰À,%ÊÎçï.Û“`ßn¾ÆëL”ñFŸªd=õÙ—¾”/R©aÀí½cÖ*–¢*ÅæÔ;"ìÍb¯;ƒ#Þö,àçê0³V«…AJqol8"ÛÀÓ¿˜ÇJþ¼vêˆíÁµŠ_¨CKð0Š _Ÿ€áUR`ŸŒ=èÃÌ8g-ólIÌÜ£d¦!âÛÞ×îgÇV6Crçþ'äéâzO:‘UO„ǽ„þ¼Ê¥x~ŒEëâ+ìÖ™B9¤˜Äz<M„ -Ñ’Ò¸BJm­!Ô*¢[œ.Ö+Cø!~˜V*®¸±ÀLžd>¼g•0ÊAñ³½½&ìÜÓ«ŠGòkï®`‰‘ÍUÉúú—È•R »¤WOÍ  `bt3«D(éNåÀ {.SâØìÅX¡6†‰9 *"F2Hdâª~fز{˰lvÏ×»cÀJ"±ŠÛñ‘ÝPp¢„çÇßÝðP&Á+Ø'ߘÐýô-²[xå—ˆ—Æx"R9³ÁŠGö«œõð»,¦à‘®ˆÑg\%”ƒ‹ƒi͈‹éDJƒf-ªG3ì§MÀ„ Ô´p!Š c¢.7q}t»ð[‹nÞË8ŒEù Þ•PxIùæ‰;ö›-“/o'7¼!ê{½3+‡Ù£{ð?ɧÛ^}n$Ä„ûtÏy¸ ñ)TZœ"þå™Á^0« Ž)"Ê!´‹Ñ!%x#uwœx!˜KÌFÛSÄàZŒVöÆõ è°– Ä»ÉÞ óã,r¹!að—$‘áRMÌ-Ÿ~îgÿ ×SÈ«áô¯r¾Ë´ƒ«d¤ëQ—ªHíp)báÂÐ] † ñ4 šÐ¦^» Oà¼Þdj‘QÃÍc © ·W¥\„î/à[›SæÇ,x#`1cËfÈ$yú?}öO¥¯q½:ÎÕ)dûã¥PfŸfЍË‚eÇ´8îà_†Ø}°Ãüz`ÖÎã?ÏJÄkáR„ðòM4PkNÇÅ,mˆüE&ñéÅ Ô²#Ä”­)‹Š]¯a… 3Op„|µwï¡?öØ•Wþô§w¾uç‹¿øâÄowÞùÖOz¥ÀA”’ ÊÕçÌØˆïßÈkÓê†{eÙM‡û½’5ˆq wt`¦®m$ ĵ©B1/¡H Ô…•. iÌ|¤¬Ñ*Ð×<¯Ã ‹WVªô»#İ7SsK,1‘§Ë'üØ{wþhӖС|ìx±¨áˆìWD@ê …b1•Ë mßôâ?½ò±Ï€\p®N fx¿vÄ E³d'ó ²aÔ{¢p­³ !(〴.Ò7Pð0Õ^—55CmöL¼%›°ÍJHmò(n Þ¸AC‰ ˆÒgKt³gòfHáGO ü³OýôÎM[2:é•ÊÅBÕ›îüº”Ëc°;pAÂ&+‡› zß`öuÙÁ=Riðä hCÂ…jR‚RScë˜ÖNõjÔ E 6¦ã£zœI·‰Ÿ ±°Q‹…Rþi$ѫ㠂8˜íI‹Ä¢¨½?õÞSçîzñÁí™3’¾ ©üöMw¾÷˜€£ª |á¡€Å,€¢uBöÌ8¶b£(A)š1½yÓ~šôÒ﯑j ÇC`!Š‹öà¹K¬ù8bµ\(EÔÅTß”`»páÜ3 S2ž„@ºU¶”yú¡s´%¤a¹6”Ý›§ÒfËûUZ,¹Øö-îzëë ´2HöRTñcoãÆD‚…|„؃ƊÚMѲؤº #Â&hÎÁS0ññJAðÅ|?€ý±öSjÑxQÀiS,ò,Äf£†EFdžžË|ü3[”ÉV7Ö¶¶ŽŽŽ6Ôñ«ý©µµ¶±±?”åRR\~±s™Œ ³*-X0Cf4“áY ‚AóTs¾ZTŠYÆ´ßÆT…ÖhÇeQ×Úƒàú(ô-ä7š£ qŠ®ÔµõSu¼Å#“ˆèq$ì{Ôñ‹"’Êg;ê>Ú»¡þĶ¡²²²ªª¾ªª*ö‡²>þ'öß¡m'wÔoØ[ÞÞ²®¶:”OEJ ÆYòã`0A\×&éÔÏˆÝ Å^<Ÿ×µ8[­E.$*E#¿úOD‘“ UÔOZgS‘ ¸ DP<†­D»"{a ç±XðއÎ[”²d‘ÊW·Ö•w9ÁŸúÐÐÉ“'&¸N*a•8²á£ö†ÚêL®h‹¥°zÎ9ׂ iŽñj Ê÷ù‚`ƒ`"gœž9ºáiDMÊÄÐLd!ä7–ÀóÜÔ‡X ´¸ˆu• q¸÷$y艣ððœH1Ó8Z¾aÇ{ÀÛ&”Ã8¢©*:±¯§nt0›/‚WfX²ò¦ö¤ð€ÌÝ l†)î#²'ëað£KðJ{Úš* õ ‚XÃÜ÷Ìm@Œ§£GKl ñïé {˜{ÞÑxf‘T¶µnïöL·øR×I&—ªûŽìmmäRS‡sι= †Jœˆ•<  ¢“BðJHH )¨Y©FK±µ |Š—áñE¬öè6~ø„XTÓö  ¡~/‘@àê•KR@ÅLk݆“ãÊâ}ví`¿äµCþar±”}¿§®6£…ÂôdõÅ ¼®Ž È¤=9m/ò@ôˆ(O$’µP :% !êR u-Ë7x‡©÷1»…䢞G`F1 !Á4vI0°ð 3ÿìDrÕ ='¸±ñ‹aÇûÒq?Á=E•rë̵ I›&Ä4®X˜TNìm©Ž™—œsu°»I¢Û°Äžñ Ö4f õâáN4<3E¬±XÝp×cј6Ï¿ô /„˜ˆÖÌì¶£.bÕ…ÑÈÂvä¢%EgXK£±îCUC>Q(ŸÐ××yù¿âÖÅ¿vyg·¤h¤ðJ ¥jhwû ÉêK-ZbÖø€J­e´6EìO¹/¡´Y>·ÄË;Û²tB‘sEèG¼¶ R@R_aí{±X¨(ÜÅH@€ö÷¯\d”#W]·» Û)!‹mLLúÉÅÛÂát:ÚeWw´;‡ÛÚ”dØ?—wþ¬iÍÉñÄr²¬¬¾®1¥ERùÄ;„dŽ»Dt%`õÁÙ]´»ŒÂ.+õr-¨–jáZ ¶ÀbÓ ºV”…ò,J©ŸÈJ¥ÜÞÀÕ—®öÄá²-ÊÊ`,żƒ……’Cw²)šw–í¨ßÝÕÓÓÓÞþQOÏÞÝ»°H¬-œî޲¿îN‡Û¤è:ûdtVŠ1µ¡!¤Ô„9ø'Þᆠv¥)œðÇT––Ú!(jñ•"«Ãó–Ö1YØY#~%mÁΰ…þòÌ*A€.<ÊM‘—!A(ŽXëæmUÛj0aôuz²hKG“ѱªúžö†ÖÁl†%ä)S3Is¹X>”eù{C]yϾûúƺ›¢Lpq¡.R,>eÙVu¢Ü¨ÉÀy· ˆPÈÐH0K—‹ÈÿQíÁÜÁòÜâoÌŸøØ±c§¾ùæ/ßÿN=xð ûÊá»çÏÿÆâçžcbHyr)ÄX†Ù5”fjÆ…Âe²[®ú-’9Gü\Ùí ˆåµHtýM+»¡ˆ»òé•â "®ÅÖN!»5…ÆH\ü>®'d¼$xûE-Ž.pìxÿdÙÏÆ¤ÏH¦ÿ\Þš)ªÃžZ½äâÅóï>6õàÔ—ÙåáÚ^žaFÒøùß1ñLqìîù‹×¯ßZ9P”‰U~ôL8™nS2y9øúužá¸t$`-S°Â¹¡`Â}â°to†}JýBòC>CˆECð:<9¢²3€³^ƒ–JgñT"qÎjõ鋵{MÊÁìû²TcáäPy£ÔŒáØÑ9ç3÷O·Ý6  Mr½<•]\.lUÚÂ3ν}BQÆú†šœ¬ÚÐ*ψã<ºÇ[Ä×L…[13ˆu3ÞDLÎŽƒYKDõ:3¯…믘¾‘Õñ˰ÍîE³Îã9˜b°ÌÞ«“/˜ã…V=@ÌttÊ$c,­ïÈsi l]ÿ‹^»íµ+~|íÕW¿vý0£)•‚ý*3UýzyÆËø;¸\Ž1©T¦„ªD2µå÷¥y^ÙYvrò%=Õ2vŽŸ÷‹bõòù—£Q‹‚ÝêÔÊJ4±å£3\'69—ëúoyx X#hØÓµàOG+~"°2¦Ô#[n|Çû'¸Sò8ÒÊt±rýâ»§Nýå‚Y¿fÒà×µÏJ%O™]Ý}7süZ¼xþbþ_öî]øe Ú›¹P¤̵„EÀpy* — µç=%™$þüRŸ—p}»Ã}¤ÙÖVké4rÆ—¥×¹Ö!`Í©ÝMƒ»j‰Ÿ Å,$ ^¯Ôɵœ({ƳU'Ë:ãž8ÆÂõ1^_?ÿ˜|¦‡ï¹ý×LìŸgîaþa>÷̱«˜RQûÏ0/MO Tn}dýzæqf0ɼ Lرùë·çïÝ×&óý1–ùgRVßQw7°’‡[f;¼¯‰BÜRäf’ãÃqT]1´f”xó!pˆm¬ÄÛN”{[äÇkíÿ!(uQ÷Òø¿•R= ƒû¼ãùþûÛª´rð+9ê*3rƲÿã×Ïþ0˜øÓ;­¼øᧇç¶]tÝuÏ?Ë-·œ­.öÇ矿nÑÑÊÔñal}n1ëÔ©F(Çî~„e=úý˜HN¾®BJIæÜt-ØdKmj ;0x—ãZ˲í¥?j,ÚšµCïÄáoTʞݱX”}cºâ¿xB©G¦ÝsÌuô!qŒEÛ#Ç翉<Á/κý¡Kçd™%;žytÎó·\rÉ´iÿpÏ=;w®ùË_Ö°kçšüZ#þ¼ó÷O2í’³ß~þh%Wµ­ëçßm4åÍõŽÓÚdÞÍIÙŽŽãÒ¢]$ +Q{Á8|xÓRÄ—‰)&¥CŒJªWu"w xIeÏË,Ò!.ño[FA–YÒÀÿ>xÁ£Ê™·©ò|‡OÌd•ÅœG¦ÂpéØÝÏmÍǽxåÿ}çöºÿ=ûW_ý‡ñ®§Ù¿¯îä’™6íì·æ„P<™Üê8íQø~ñ±2`·NV}”‘§&µRvJ§Ïy€ ¤÷×z’Zey»¦5„ÚÛ«íÝ6”‚÷"€nÍÅ]ßþbÖ‘’àEÒ\9ùvm­NúÅÁmV­SyÌHcþ#‘ã•×ÝrÎí·_»…[×>»ñžOþát®W™\îùä’·W§œ¢ÉŒ—ïˆw§ñÆ;‡ ’©Uå­‹{€åR,Y 0ϵÙ0^ÈOœ>ó ùcÜ?û¡‰om7µ6v¸Æó[0dÞ¢½Tå‚õežµª*%&òˆ³X¨ÈÔƒóŸ¸î–i¯þeÍïߘù¬Œ¶®~væ¬ONO$\av®yuÚÙ‹RNjëüƒo..8™Nûmãñ>`·¶•Õå<åö µÅÁ^ÑLá6{·‡u"Žq0(K4 TÜRx,@ûh)X±ˆ7ý×î²óZÉÈÅRÇt»£¬s¬ôÕV•w¶Nåʱ¸2»îìOÖ¬öé“{6^«àÿvÏï_ÕnC];Ç5e¯î¼gÚ-«§rñz'2õ¿'¶[U=Ê·ä#Bêß«âZ°k´v-‚¾À~‚Lù. öøà {K8s¹4Lsâê%Ê\}T¥­U|l¼+Ùê¤î~óîõ¹áEgò—5O{õ“OÞ¸úÙÿð¶Dð¶«ÿ4wÚ%—\«·õÅÂ,îì?yúi.[2¯®¹ç’ëŠN1âÔ%K½)S“(–}¿QêHåCAßq‚މÿ(^PcµÿìÕfõµªöBÃyë÷6‹0Úø @yª_XþˆßY$åQ½»ÌËÇÆ—ÇX÷戳~}*÷ü´§wâsþûomd¡ïysÍV¦T]w¸à]<.ä2«=ËÙÓ¦ÝÃb/$•k¦=_ˆ6¤K¾+W`¶F Òµ R@Ü” >R^_iØ¢ÿEÔ¡¸ Hi@ƒ lå+|ØCrûTG¢ …¨½œ«jW­Ûd°»c"õ‡5ï çßž¶s'ÆÎ§§ýü¢XŽe¹Ð¢%?qéyß` úü»½x¾Î+Š[·˜lr•×½}É´§‘Pþr s!Uã½y¼ ¸ªÚ¹#)8ÅKƒ„¼MÎÂ`»]NÐ^&¸ÕZÀ×QS¨!¾5"&H z*È¿\VµÀ| Pè\(ëBƒŠ®Þ›PÌfu8‘Kþò*8ÚÌ\·zx8Ås½•ïÜ~û³¯_»"¸qÁ/éU³ä%Š&¢ÖûÜ#•,m^ýüÙ\(ê…Ö¼í8¥\ˆ·¶G²YVR†Ïã4þ¨-K\{í‰õŽPWxdQp¸6r±³[KJìO÷u÷)^Šå›k¡ž‰¹R'5ãTJ_β»·W³,bþ±©oÊÜîð²×+‰\ûë kòS§¾ylþâ­L(•Ï_rÏšW_Ʋ?¤'|ë*m•©V üZ‹À\¸ÁO¦yŒžØÝ"ª—‚¬VÝ x0êG«ÀU˜ÄÕ;•©‹é)ÒUÞ* åáä{ªTjÞ9¹<„Ír.agzç=g/:~|ëbPûP´t¿–9ɵ¯ývI^EE®(«oùä/g;%²ÛG2ôL£¸{¦##¨D|¶ÖäE³W߈ÚWÆæ+BÐ8)›É½¹s‚ÙS{+®½S×ÅN,èÉ£KÉ£lìtä1–Þ[p®[³óé³WW®·¤¡èx˜_¿Çláf“IÑɽ}ãÔ&'SÏË#Ù¶­ÖÓâúÝí)"}­õ :øÕ{T©ÏÀé•Gþ ¶hs¨W`$x¿6Ѿžú–y—_å»Êûãׂo,ð:T/û:TF&w¯à7Óüí‡vHp­ö#D1Û4œ0q]׿—‹â\´è‰>3vp¥mb0ë æÔ"€ÝÁºxN'qýQ$UñÓˆˆ³*3q¼<žúåÁYWÿéöwÞ™;wîC+¿1ñüoØ *ŸH®/DR/…Oãík÷$¹ä#D‘T@°^ÚŠZðxÞLV²,NS &‹À]¼ÞZëwÀrw—º>\'¾¡÷êG±~œ¾zÝ–^zéJvÉÕÛÏ_wÝÑÕ©aÞ ÚÊåcFW¦nuœÑ¦Ó{¿Ž¿(14ÖÚ”˜½Òp‹;Þ!’C`F`ÑŠ‚VÀR"à›µÎ…K0&‹ŸùÇG_@ccM­Îðâ©P,«Ø:0<<\¹úº·y‡ê“xúžom _{í –#¾¶ÖïEÃjÍÎß?ýÓ.¹ä–çñÌ02ð˜…PîˆDzÒ§{"ÀhɠБØÜ 2ÐðÏØ¬Èb¯¡ æ¾ AE=¨$Núž¢­ëâI0J`’énp¥}m¯rÇ™Éc,º9ÂK¾*àÝðâðñÕ×Ýrö´Oî…ÝWU¹ñögeJòìÕo¨’üÓÿðê«¢úûô´KX~_p†)åâáH¾¬mìŒ%²í„ÈGœÕ×ÌEt3ö²…аú‰ö"xeïeé6ç žÍBüîzI¡66Zk…eÁDÐ&:-Ugì?<›¸ûeá‹?20œ[ýöÙÓž¶Ë…L"ߺþY¯&ƒÝ·z•}?¯€U:NjëâƒLX7™@"CG²âÃ<ú‹^ ·!ùÇðP uýc® 6M Èä‹ð^#†¢ea„øzb»)MÜ.^§U~ªeg(¦"µÜfMÊ’ºbåógO»GuEìë“§ÿÛµ^Ú~ý·>)Õ ÙÉk/ÃÎÀ@ÄižÁ™0~¤lŸèë:FèXU B©o|ŠbÜ;UJ‚0©ž†X¾‰ú@VÔ?q Å[)åFpéÐj9ü£l쌯d¹ã#M©j,Ç  6»×ÜF;]B1¨ãÜ1¹ŒÙ½¦ÆÀ‚b{™Õ}D!•=”„àB1üŸÚ,¬“—ÄÓƒŽsÝ´5kJ÷5;ï¹ç“iÓ¦ñž¡¸V>ôÐܹïÜþã÷¿ñûWK‹oµã´$Ïð.4úáÿ«jPkÅU˜KÀà9ÎÝàø‡…Á«_Ñ*«ƒå#t;]mš6ô°°„ô^+;„Ê¡ŸèŒŒ%Û#ÎÛñÛž§9æjÉ¢ÊXîø0ÔÎU]2缕Ӧ=}ÏŸÛyI*RÜ—>ӻئ«(U2A\22ââ5-¢þ!Ž€áD Yj&‚{a/àð',ÔMÐx?ÆUwÇdâ4–}±ËÃgIy5xÕaqìÙo/ʰ¬¯XdÙø#Ï­_¼xñ7įŋŸ{þâ9®Ž±DʼnT.bQÀ'8$[s‹ é­Ä/p”“â߬ãuGB´hàð`¡5òOàÖhÈIÃm\W©«Àz\êú2RúktÞx¹ÀCEè@ʾ˜<˜Íêw —ìôz†L×UNUn]/EØõ¦hC±ßøß|sÕm¯ýé¹­|bN?_Â[ôö%ÓvjtÏÑÓNÓqËj¨¬êä÷9TfhCLÓm¡"‘ÁkQ¼qN@¶-˜‚TD#à—Zd~`/½qÚD³‹«MÝ‘kEI±P.dègá/(±d‹ã¼½FJã’çW/ò¤ûî—ß´*T`MÏ ¯={ÏÚ¯}gÚúç8Ö=µèÕ,|usk{ÏÌb‰éÆÎ}u­Ù|{™pì‘ÞžðoÓFØ{+tÇÖ#ƒÓç^µ×bpæˆà’¼kšý^Äm¼™Æ˜Ê}^*Ù¶¹ü¥±(\:s¤÷Õ÷ðîó•ù­‹ç§¯%rlÕ³¢Krí_{ãà/ï^¼~kA4 ?Y³“[¬Ìé;³x8ŒÆwô´ *'tÿ4}â£Í ×&2½;Ä|H]›G_%j(±WúâÕ²¼ÀÍŒ|XEGj¯PvÁÖAÀ>.w"ˆ „‘Ð÷¹Ýݶ/ç¤Bµu]CádôL¥ËDRÓ>a9ÝÀ#‹£V—dÅFÙ·úÓ¯7®ø%G/~D5p9κӊ±âmÝÉdüÏ=uµÙœ&Ü’ú~òd?gƒtÄ–_ðJã(1dnv°PkZCW{‰‹Œž® ·‹q'x‹œþr=JQe°ªÉ"ëïhßWÅN]ú „›"Gs).©/ŸÞ Û²ƒ^s÷×âÜÙ¦[ŒövO*‹t4¾¯‹)%‹TìЖ 7Š8µBá˺r^ì«[˜{J1ã õ¡Öfb‹”ß A¦Œ`Üi "׿˜Í´ó% ²ºÿ'C,—Êçäty*VÝQ¾»JŒÃžžÍú() Œô s|½|xÙžë_S•”7$‚cRa·0±ÅЇ™e­Ú×ÞÚ‹¨i÷CÛ/|üÀ.~åüQsèk`-n´̉=bRƦñ‹€Œ \¡€òÆ7ûì%‘pNG#&LêJý½W‹Vl·@ðõ"±wxpËö|NªJ!SÝP^ßÙÝÔ=¹TX²îT|yA|™Ï³-^¼~ñzy-°¹»—-x#øÚm¼âøë V(Öù©onÈbqa´íØÜИ/š³B.¿ýÂÏb’8p¿v]‰H•?Q&`Ñ­À9‚&FP¤D­NˆÍË`xÌ.Æäúkˆþey.zª¸¤„ï_¾Î)>¸ë¬ì˜øæ¦-‡<ÄTf°¥ç™¶É X<Í>ýâ©6´gªÄÁm­È¥Š˜Å¡ û…Oœ³ðögƒ‰ÿøZ€˜:?5žÅâÂ;ÒÞ¡Œç•Ý"e!…Á¯g圩óÛöÅäâ¤^ÌXfQïQЍ±ì‘~˜ÈM ¤™­)` Ìþ¼¨Â¦à¸3ß,YÛ•Gÿgõ©Èƒè³‰˥⹕PmûîNæTÆ|+zVŠ…LÏUH@I‘Êe²ýýƒòWu6ŸK Žañ[*;gå;ׯ`6ïØ8+^ÖÒŸ²åFŠ;Œ¶,<›•Û-°Ï ‰nUqaÊU¥—¼Jš…"§rm¯"ÞˆÚç(Eô}€¤Á[È‹è=obÖÂ&ær¤W39mwò%>—ÊY^¸=&ÜJ¤j-²´C‰3KÝíõ +†å“ŽcÙÚuuuÕõ]Î2uµÅ/ﻯ~oyKGu&'X~¹P=ïì©oÎ/–½óÜ“ C*FIYÈ[~<)W*2Ô%v¸>š€¤–ÄÄG. ²}=Ý+1£nº/¨ÖzÕ^P(#…‚Åe5æ5y£¤@Ní•5“ªd«±Xö'äRy|Óåës­ã Sµ:Îâ7y”Ä„áÎØLcCùî¡¶h”y p ”¿sZ³d4ÚÉSº§TæXùÕ‹qœ’p“¦'’ßôÍñÜoÞiÐØäuÜ;fÇ1ó¥„Ãdꄸ¥à &õñ`ØŠ–…~Bñ:¹•Þi•k[¼­ßÙ´k¢OÉ„Â|=gÈOÝoºmëüG*‡E:“Êv”s¯3yŠ)‹õíµ™‚#y˜KWÞ£å‘Ȧ]“CجíΠ7>ròûáׯ¡V©œR †n[yq.ñu]Å÷™=†x‹‘UÁ„s.¢¬Fh bIŽð?—þ~_x(Wüæd–«Êv–#ô•„w¤"Åã’£µýÉx2y‰eœe<] Y1ÜX¬O—„¬rL.³\èôëyž²v Ч÷É)ž–Bÿ#1£ÄFÏ›ê£îÐã K’Å›”<‘!ïP<½!;»ëÁ¢Ó_ÚdÅÃýÜP³£]Çc%¾a4ÃüZI ?¹ðÀéäÅBÌÐJ¿>p}ŽFùœES ‚‘* v‡>a0«ûšu–Z$ç¾µ¬2ÄR râgcMíãºüaY”ß8ø ÊŒ|ftC<ÙýEkÆL&Må ¥Mb¼/sš÷x W¬ïóÚ‡e› <_¿8èú #æÚ+×,Öc´GeêÄërl®w‹†¿Ä~q÷^/B,­ cÑgËé|سäœuMã ´—w~ÕÀ¯ÂñÒïwí:yÜy(²7­GGÊt.77`Á(fi bmo]1[ìU°åÑÄbs;f„Bmt‹²ì®B,¦ cÉÚÓ³Ï\ uãÁsâÉè—”ÆÄõËZ'¿kr/wÖƒ[òN{²Êˆ }/¯•G Ñ› )0o¢µŒ•Ê:.Q‘2²¼Z:¥ˆ…ÆÅ»/ü,T±Tòþ÷˜ÙÈã“”2Èßû/¹X"’ûæ¤ÂylCrLOŽT ¶‡ÔÜ„äJ'¾­SÔ¢¥Ôž®†S€lG°:µp7±–RQSõš)Jf Qæ($o_œÏxˆ”ë¬É„ràÁbª>ü_#t¹S|ðÀ¸AùY›˜0 rkF¬'/¡"”¨éà¼0`¢•‚h§°‹(4!>¾WIšž<Þ›ˆ¤#I8bö ;ôŒÌAäp³X7”Ù¾é¬]äÁ».ŒÄªâÿEÙWŒ\¸kal7¨Ý,Â[E.H€Þ“«“v ©ÛˆM´Œ|:àÓ>Ä{èÕ,]‘#º4@ñ®I©Srã¶S']ºìl+¯V­^)ÚôøxBÙµÅéÿ/RdzÂ,YCØž7Âhè¹/,ëmñ>ŸŠP´·ŒÌZ%Ô!!®Å(N5úºÖŠ;j4OW) ‘ÁÔÂè]VMTç¤:~顽-yµI%5žPvrZÿ‹\ÈX¼- Ã,Q‘¾p{>%ë—Å|cÝÞ¡hÒônâFEU.âš=¸ÔÅÌ~Ôg‡½ ‚`Á#@3XÕµaŠÔõ/ø”oß{{%H µjK'Ówµ fdã'’ŠùkªΊ9¢G¬mátZÐñ³ë kXºÖ‰™öÀƒ[¶ÇRjEf°nC»ÿ6kFWç"E™®ã5Ôtí·2v"¶3Ö¤™Zƒõ+%× YK+媻bŒy/·0Ѿ uµµ±%ÅWØ¡x°ùèŒ4$æ âcU;öõl.o¿ê›xMQ”Ï0Ìjp î: Šj‡Ä–>^Á Õ¶×÷•Ëh)ãé:ïTQŠFÈѺg— ñV¼é’Ú041(:öQµ:÷ÅþºêPýRå¶ãÜÀ„ue>Î$ÓûcÅM·W§”Lò£õÑô$:/yŽ´oîi©Í†2™Pãºò?G½·‰ÿÌ4ªDîxN¤ Ÿ| )@³Êõ¯´ãÖl¢5 J¬MëxW“ô/„Ūµ-VwtwK®àí‹hÌvt…íÚmœ+P|bq´g•ßî/_Ú|ª1ãѼ†;Ë[[ËûÒÆÀW矬X[QÑ|WÏ ’Iªv_ôLçtºÓ›ShmOl°'ÝmE¾ª—»$@̲‹ÛÌšoÖ‹ ÌB<󺀀Àæ]†È8i^# Û+í’*’Ïög³ÜaúËÃgöpºÛÊÅê.ÇÉ·´5WÔÔÜTR“›áªÖüö-ÛCùF=þsþãšS§N5×T<¼Tþ$ߊ±áŒ2ǶdW5¿ÛØo¿ý½^x᯿äó&…Æ Ò#·Þ!³õ^¼ËÎb¾.5 1>º©a|ˆkáÜí…n®YŽî[’§c,i±vx+ZÖÁu#ó¦xãwo¼ñÃé÷r’ýHõ¾äéçnáäÞjQõwÝ\QÓ|êÔÚ›ª= ‰wd6±DæñÆLÖËc˜@2ç‘2yà¥A%’Ö§ÿ®áø(Dö{ùçÞµü…_q{ÙÐFn]ty*âR¼i„¢¥ ž†‚hcYŸ.x«ÐÚ*Øz´6pS„ØV€÷G…Å*ƒŒ É}!ö¡_øÛçæúð…FŽkIŸæq'ïkV§úÉ)‹Ç¼v^c~ŸøùtOfÓQóØ”oUCÓß)ðo®Xû±I®®í4U3]ÅRp§ÿ…?G×+¿b÷ÞX•ÆÃ¹!a³lg§>N­é?)S•s)\›‡·óÙ…F 6‚#ïèH ø,Vr3 ï3/|n]לÏA ­ñÓ’H8*z8ý/±'«žqóƒyÁWOf—Õ—YÏŠµ=ÓŸÒ„};‰âé«®?-% 3y8‘{oüÜw½À>RVlƒÑ-e³.HP˜¿™Þ7$1¼.Ú7o¾rêx¦Í%†ôD•)Gjœµ¬cÅêAŒ•üˆy_-÷¨Ïÿvï0Sœøi¤ÑÑûj…óˆõH°ÙÍLßž›ÅàÓ»•+º—Éã·×”üTâ ÕN [ˆ7õH6‹u¯kíWMC¬¼[ÀLû·{åÉjO <ÊêÂá†ëæ¹Í¨¶ª{Òc°ŽÉã•Òwþùr&¬ê±6c³N¾Ÿ‘3nÖrS«ùYft/ÃõæCl=‹?\\_ÁûÇøß©‘UÇâ+½ƒ¹ï{?ï:ŸI¤e’yØpºExüï*jðçQò øÛ³ÊdÅã1Ÿ@˜+>Œ¯N¶~’4ÚÃŽüùãÞúr¦#ë¢Ðfµò×e¹!¤j7B!>nŒ@F °’R¡| l=0EJ³²ZÉ( Òt…6Ù&r_v~óùø×ù'µa³R§õ&`­Ô틵 I¯ËÈnø3µ^ Ük÷ äÔ©Šyr^3¶79ñXcv¢£Ä¬Vν bŒp =IL)†— º¾íÛä`Ø2K/¬"z¼SÍYÃB¦”ä5ÉžÔCéMÌ@Tß8Á§úü^gâzb÷}ýr¬¡¦ÆÚÛs-Qem9ë²úóyTº!WR §jjzŽ‹YùDºmg^bÂ[Å*UaM¹qr·¨g=„„Ç”ZŒý˜{qêC(¨Yyd:P.Žh)½ 1 ‚È‘€h…tèfzúHÎÉMŸèC}þ]ø´T£/…„¹ú{uÖÛcÂÅãy.‘Ç·ä;´tÓu±–šæRi®ø˜ûvg"‰9Â[ÿœE¿£É±ªUªf@Q3»™!ÄšA©½'•€~ˆZ<*˜»ÚE»°?62S¸5=Á\HtÐq¾=ñ‡úü•‚3>ÍEr_Lt»¦þð”Ù«UQ_k&³½?“o­j3)¹–‡O•¼¦Ü?]èÝññ%íao{ÍÄ·¾<礎¤unX5‘ɺYkg‘ô{Ö… üÝ¿`PP\Ôù¡EB]¸àEwO‰ŽÝî“ @šÞPpúoœD ìœÛ¶JyÔ² ù»ÿè—HEO¬Zµ$Ãñ͵åca`sbëšKjÈ”‡ÿãóå¢<›êO9›Z篓Ý:³·­27$ß‹‚xgÑWÖä!Ù¨µgÚ5ˆ$ÐNÑPké‚uu^d!ªyË]H÷à¤ZÏýú°3X:ôîòø•Lñÿñ”-’Š}±ìްš*_º4šnZÊ7t‡%4„ äTsIyð“ÿ7Áè“Û]ú,púæÜòÉn}yÞÉ=Öo}L:ßÖ“³D=‡–§¡í z)˜aÀÆ{1¨¦Ç¢oeßYÈ:Õ¼wï+:ÕßT ßÍ:¹gJ¥ié<ýp~ã½Ä¿Ù©Ø—íNóUÒñ²?ô´ó룮ñ#å{Ÿ‹&›ZJkÈ””–HHD¸åRïÍ”ûW“Þ:W‘†&ÐXéD(µÐ†ŒÂ'o‘¢€Ž!Âí¢mS8d#&±*3#w.ärA£ÿÂç§õ©Ú›J.²Â^›÷ÍSl í^ÞÁ'ü‹Þôg.SX‘ÚXËÃ>L™òoºR žß`É®y´nrïÇU$å„:ÇLõ$¢ºTðœ38V…–|º`Ð_÷CáFÁSßrM+Þ8qAʸmcmU1'tãiä•H)t\[œÏå;Õü‹ùÎÿÀ®›¬žöê\ÄÀôå8±P~óýv˜5¥Ù¼Ü57gÄ }ròÊiÜû:'Ò=‰3‘…ù•H®}°Ä^­¾ƒˆFPXW=ujQ^³sÎi‚ÿàEü¹ôoS.$Z™0±2•ß'äOE’|žÒ ÝT3åAEIäþÎ SG‹@<8P6ý Îî¹;×Ò][ñ~‚J%¦|\+¶ü4îý|ÇéHzNdÛ¸Yçm›À¸—˜ï¡\Š‚àÓ®½R-£ÖrQÑ QˆÅUqóFJæ .·ôf»FûˆŠÓ–[ZÁŸ¢qD ØZ[qWG(ãá ùPucGmuH órôŸS/t4 Æsžý«ØËï8ë¯oòq*”¾ò×{ï}¡Ä§ù0Ï‚víD$ȱÐ&ÍÔPõC=VÇЙ€f‡½"”Â$“ê-žÂ§GêT!+<+ÈO¿·1?Y‡‚¯ï8….딦ëyÚ;üR…}®=i®˜×žrRâHò‘ÜÝeÌG“Épçÿîjì*oLÉR{±nžÊ×§œú7óžÿÂ#„æš1•ëó`áûrЧOú—û•?füSØÐ¦«'ÂÊ.é%ð™cHÅo·®ERnÑÄ‚Å#®µ!ú@Œw=rO ‹²uòòèG%,Öôß,ûp¿íª¿Ú;iÇÚÆxâæÔéGi,ÿw…DÖVüQm…Šd[êã¡ÈçÓ“ét:š~²Em² ýQ(É”‡7ú7)Öš›ø ¶?ÈK×@ îüãÿõ›åþúIKT{õu«ïïUŸ ÝKMeÐŒ|¯L£µ­}nh?Ø=J-¦lNe$è}›`¸²Ýâ_sd8x#þPØŒŠ2þ”µþØèóï2?]3¥E5d{âþ¹CQ_ 'ûÚeÙ>Ò2¥¾k´á«èÅžh‰¨÷·ú7ŽÉɜ»½Ô°¬]ÔÈä¼aiÔb}õ¨ ˆ²2¯ÂpÁÆjÀ/ ûºâwáÓê2åÓã!'ó7;¾…aÐo¡gÄì#Ñ EÏó¬MÅ]’?2¸/:ÁLn[²ªAgðõ `®¾kBƒæSü•R6 8ÝeÔûÆjtÛVfuMµ“¯ò¼ú€ÁGt®s"—y u’ݱ™Hj “\P‚†æ~ÍvOv¢,J½*-|¿*}$egVËó(:-hýyÅÁ€E>Ë^jsÎçŒ?þYL¶46Oê 7ÕKÑÊú/0©ø ^’/õÒïY1µe‡ëD6tâ‚ï¥A3ŠãùRRj%%pתم‹v{"Š!5‘ô)5$¾UÎé¨vz_´ÜάnlÄŸÌÈk:³Nð‰4Õ ƒe¥ÙF"/pí:Ocr!n¦ü+>s%U¤™µì(/mrMµ%áóm'RÞaI×kóìÅ®·›Í^¡Ã}îfQ˜g,…«&aù„Ypâ…t6uؙշíì-µ¼´@X|ÆL~Rá«y¨Gp\Ô<ºšN ÔÞÖÔ#*bJ"ÿa—+~ÇÿÖ¢ñM÷hL/Ú)'®—²›¯M#^gIPoµ'ª§gÚP‚Êåm€G G‡U¨Â[=:­¿òÃý<È’ø†'ÇXf…;!Ëc¾dúc² @š„Goñ7—¤gAÈ4ÞwÚƒ=ÉzîÛ‡cÓKÕÃN5Oáö1aáÀ©¿àŒ{ßês9ÙËOBrgÑ1 0ãñQ(¡uÆ^ŸÉjáÚ ø@M`Ü^<Èrøè­œM—±ûüî„ bÜæù(ð3\t±›jJÔÿ…‡Årã¶3@=FŸ>)³üš,ÙU¸(ѹ×߃Hó«Ü3Cp,ÁY}û5+ÛÐò4k˜l4%MñçB’Ì—òtN«LD4C<ˆ\Y·]-½¦ßÿÁ¾c¢,àU¥‚”ì¾Niþü1÷בn;ƒ+]Âa4êèÞÅsûÆn‹zÈËC~믓¥¦ãrV¡ËãÏ*«ãŸkÀ› ¼\£..äƒsñŠ¥:u ’HñJÑ¥ò•d3dTYMíæy+Së¯ÿýJÇ1NG“I’…‚Ü\S²~“¨È&Ïp|°ûIQÔh(Ýcçpœ¶•å==øN‰Âå÷ìbõϼD¤\•©5EðÖ%<µèx½Ž!Áë†]‹÷I©A+*Õ÷HT¯ ²Þïc™n¸ýµÄçªÕÖŒsUf·qäÔÚS¢6Ñqæ 4QFêKÂPDrˆûñ¾gqK áhØq½ÜKDzÄtÌEOH60Ý› zåú™«Õ«j õºˆ˜KÖÍ)ˆ¨WYÉZ«ò› >×wÀœN|ŒG޹×K*H…ˆ‡ýöJðP'›š’ãÌ>19— èO)ÏTs,¬ZÕÞü‡ãø–ZßZO j’ŠC®µ£% ‡AA„"ìÌêUL`€Aˆ‰V%ŒpZŽ8‘£…'N\žÎZ…“êŸËËÕYh¢gº{F±¤‚TTññØl§¯Çß·¡¼®nݺººò}eé’NQQÎï¨ð÷e¶ž¯jÃÔ@ÊS”PíÈÀ‡¸“õÐr'ëó^fHñ   ñj X¤!n T#E.¨ûRÀb&¾}„ò¨7óþÿ÷¯ÊàÚû‡±>äÛº‡[ôoÆ¢ÿ×VJAÖÎã®9Wß탚>S3ýXu˾°ßÉ´urÕ‹|P1ŽâáÉp~ áG›^p2ÆŒO±;¶¶AÀ¨­ƒ¡4¤$Ù»kuᜂ|©Þ™œ7Yu§¶±Ðc¦§Jä|ýw¹?«Sßö´Õ5kÇ5Xv··-ÙÙS­ø{ó­µÕ^OîoïôqkE÷ò6oÿ©µ>¼oņã±"¦H¶{>ЗòëT^Tó08Uwía@›Ã8û8¶\zÙ @.4µ ó{‚yÌs¸yWYé#ÅHþɃ,}ÎX^¨;†ÑÍÌxäkÒ\±Ö]ÊM}£õ £uÞÂÍ…ú[¨YûÀ]OŽÆØ+–û&¥’\ù˯×TÜ\¿®?”áW晡Œo ”T*Rܰ͋{³j«ˆ§žŸÂËv(¶j& -û꧘Éa†`×µzöà5Z,Kw1óõ7dhýŸ*{²ôÖMþÐb¡L¾ãÉ›k*j Pšetz$çxzø0PݯÏ;µ¶fÊ©æææšŠŠ›ÚSN¤¶ÌÊÃÏpÛ’‘æšš»Ê«ó±L&VÀl¨¼CU}͸áÈߘuÞìÑ”V‰tN%»ÖÇb†0]ÙÐõRIÀýn«Þ»¦34ÏÀK‹çÜûûe|}-n–øTß1ˆÌ†&Í_-ª¸Õ±XuCWòæ‡+j<˜BE—x2Xmï þqJ^³FaÕT,ígJr$ê#3a/ñR…ªJÖecùL«°yEȜƩ–S;ÁÅ“³z_Ö”ÊÔ˜%ž 3ê[°›ko½òï[׌5 ™m¼å•ŠrF¥!Uœ• d|½ç'¬G9êônn–F—Vµ7fb±LõºÍ¿>¥†7ü¤‚`U[ºÕqbûjJà~+汸;ódÚjÌrÕ(±A5ü®#Ëg×m/‘ÂO²àB•u—çü·þ ˆãÕ¸™@j…@FêBÁaRîq¡˜­ÉÁóƒ^ULP¡h "ó†Tñ½ÝÒ®1¯LÚ+ÅHÌcnŠ–[O4íÛÛ2˜Í3¡dkÛÿxsMMM’+¢ˆG;'»ô~ãœ+˜¹RU3/ËtÄBÁñ.f¤ØTþ·æõ̓ÌVU×I§ã²Þº–|ýã¥P…W, ©ÓµQ]|”J0î‘ïR W„{ì‰ ø²¬½{f9ЇÁäщd-+vi8XCüÇLg!kP›øQ µ‰¡õ±#å ¡<{lýuKïç®ÎØ3 ÝT¡uâƒòÑu ››ÔÜBÅOŽûð^Ýûx Uw?ûÞ†l,–mÝÜ'\Td蛹Wü|œÀ7µÜÒ:­!"p8zÇõmX¸8ߦZÔÖÐQàE4½öúcÉ@ª#†Ùbûd´ÑéÓïfíOU˜®ÓBãBD«°Zšw§9‘S&“µÇD UdY¸ì5MÖV”çe&R¬Uß ^ƒÁ§ñ0wÙ¿·3Gl?â1?ð„½o7b¢É{Ȭk¬ÎZ×±±†ËÑý#p—·¿³Aá”è„»†ëäSˆÈÖzE"Ù °Å…tTŒ†äe7„ ÄÒŽq/]7aºSèJ{5=û±ÇÃÑð3íÕ™L†©˜I•ðœre¯j¦lvœ\þ_Ü㤤ۮyýˆµ‚˜Ù¦\>ĤQ[·oÌÄÅœWÞZLO7j/w¾3Q- kˆ@“ Á«ÙÀRI¼Í›¢=ÃÔ N,n_ÅA‡± Þ‹â¹z"hˆ˜ÅLpJŒjeú5ÄW8>Ì!ygzwÁךà3Ñeí¡ËáÓå©[èéÎ+–V;Ç·p´]ßÜ^tŽÿ]H¤‚Y9kUHøH.Ê7n~)Žˆ°ÚÊxÅ#v_¦ñqàÕÞ¿±äDiÜGìõÌ¥–ÉG`FÌÆØ4[z€ òVˆÊâ@ëT“Â(g>ü||¯n [¿ÍèGÜ–ù×­„›vç Ø¥óÙéQÆ©-çä—”šv]Xp²BPU¶•c¾¿rª—ZkÎ+Ìsœ0r"E}¦¦cˆR^^QV«ÒW·½¡/·)a)^vë‘ñj°5ÅãëPkd '.ÚßÊþíåÃSÞ‡“¢Dûðóq˽fð{{mè“uâ ú’< JA‹Å‘ñ›×ÊjmÌÉØeö{Dœ ¢öÅÜŠÅ:+ñÂ2jN+ÁÄDz¾“À'Dt‰å"xcù€Ž.=š&áCP’€wwA'¹–M, Ä †]—ZTA˜5@üEïõœŠ´ÚHzw1RÀ ¿é17ËO¦,ÓÙ›…!aÊãéîtZ[yŽF žãÑFÇ­¸‘F'–“x<å Š\£‚Å –xE<á>«-=´»ëȘpëbFDgCºh`°c mV”@ ,ºÜ†ò®!pé<„ßD‚‹–®B†e"±½€k’4Ã>#Å5J-ïÄÑH­Z¨3þs,bã_Ñqb¯œÙ0@|ðR¯SÍëZáxOúÑö#2 Š÷exÇ"‰J‘ˆ$j¨Øí6íú&"ÿ•ºÃ«‘H­ØÏÕs÷Ý Wµô‡B¡A9—lô÷Õ¹f´üö°£Ð¤ß¹Ñ7M¡ &S—sŠ\ÌæßE´– gT:ub–è@áz‚Ws$Ä‹vÅlˆ£ÿ8ã ÛÈäéj«Zãt˜Á²÷4;ÒÎ {îáÿÓš mÏdBuc\{ø²¼©ˆ!e«¹¹ÚÉìÂôØ‘ÈOø_UlpìcïË ÐO[Õ`f˃›¶„ä˜hrÐ/¶¡,Z½R-¢êÐ þ‰„H愆[‹ZÖ£½„–Ø)ß½ŠŠë§Í„ îYƒ^2Eጒ„¡èòþ”˜Ëu‡Þ‡f·ÿ=ßðÚ÷R¹Æï]=‹3˜¡&û8MÌ~×f68pÖ¦l~Çévoæ‡æ<êµ«š¥‹² äß+äŠã£ö–ÞLfÜu€9žþüº°Já#Ö¢Œ(ËP㻯Üû{Ï÷O }ÏqªÍ~—~¯–e­ ;Û¨'„¿Æô|£\;a(± DÎÒ‹ãl¿-56}9†*óB‰1(ÕrDº=/Öøfž³™tóa‘â¾4~X­5ª#^|ü€-‘ŠTü{Ä—r™;ÕÑ'CrÁ̳¶gØë Ø›K¸¦O:³ó[ÇY§©þNŠ~¯öⵑ`XO…<<ðÚ®š1ôtI2¢™a®¢þ‡È‰h£!¼¬Q;ùÒw@ÙÄK¤[¢ñ±Á¬b[z03È\ySƒ ÷Œ2 l¹_µ­¬µ\ ‚KŽkˆD‹âL´]qqœµkS¾.]ZCx{CgJ^ÌÚ•WáŽáœñ1õBÔ q!×"ÜRo˜QC;V.µúŽˆhˆ›·ÀE–ÉÚWÄçÒt!)t‚ãéjqNÃ;²[¼3¿=;Ô6ÖÔa²ÏBÝý§äÌMî,¬!‡"Å»„@ö:Vr1&_*ÔÝ¢ ¶Ì4t%"~ Ávn2Y>à¤^Òô²§~qÐëßù¯DïÓq ¿¸™¦R™‹ž r#µG`„¥èd$‘‚üJ-ð(~òõuLA AjH}FïKëÏ…¹bé‹#Tˆç©èpRØd•V™!ßi­½•å˰ȦLK”»$¿†ˆJ΄Ô9*Eé;é¡NvëQußþnvð[!,ZßM}|¦ I¨=éí•bHôî¿`á‹.Z¹ò¢‡^m DDñ“}Ÿ Y¶¸ð!-Ép}¨Z=âo†ÙiÈX‹ØÖ Éýu<êÅKa$0¸¹™I­±{¬”@ê$' ·o™Íé±hKDFÃÖ739_™”ð¤Aƒ­·u%ê„=¢@0H$z{{GFøé%x²ÃAr‰Ú{ çq&¹$ÁÅлæõs:gåy/9ºz ¥°ÅÊÕäC¸GœlŽõÃsÌèy¥ùƒMÆËúC;¿ iÕ •Ro…4Š/KC"‡vÁýÚ1ÑòàñW1â#f–&+Ý•‘FñÀã!ÎÎa¯ÏÒÉnqbîƒéÇ#ÅÝqMð‘ÈU.}‡Tv-\xÁõ3gÞqG/“LP^4ú¨ˆ®±¥žÉ"Ö°ýe„Ébÿ­|bÎ’£•r ³ƒ¦Ã£!‚”çr“ŒÞÿ†åÑÿYø4O ;X”5ª¢¬³²![5­ó9ufÞeöÇ‹º@EìbžX¦ðLw|Ëâ"ÕaÄ  1å|Ní\â¼HëˆX˜0ñ§`q{uZ“ŪIõ”ZÖP,¦*W]ôè’%KæÌ™sñ—žóÐûÁÓæp^MmióŠ‹„ lÝ[î_xéœÕh\PÝ;f‹9޲ÆDd”ŸHEød’EÌÄ+M<1 ¿”=ôà]ïÏsa&Þ{»»Ø¡P™:³+© åºö3L™›9 PÍÍyàZ˜9é¦ÛÒ»³™M/>¾©_²˜Ê(«Ë¨çÞëø+qEœò¤ao)Á`€®ÈÀê%—Î%ÁÍÒdH ¯æZ3ì}è‰Õo‹¼|ÕT.–Édû9÷6ÿ§¿¶®Åò!cá?ç"%S=Úê'pà6[ÙÓ=ÙÌöj–GwŸÜ-*P°çÝVÅžv­HDÖ>Àr½â!±îñ-Ì Çd×JÀ¸ìR¾@ªrØhz÷`†_£¢ôŸä%¨ân¿@°÷ {®þƼT¼Í ²Nôu8ãŠÂ;ÉÃŽSxtåÕA×€âQÃÜCà5ƺJ ç<ªíùVêÖºò  /}ý¦yóN­÷À¼yüßµ5÷‡ÅÔFÕ @GÊíò¸Ö+1NëiÕuEéOÓGF«C¡ÆvYOLoˆÈŒƒ¬ŠI!‘š›ÉIþPŽû´þ¦ ÝÂõ-ê50Ñ¡LwvÕ5ÔýAjP´ºÄ\›âÍbލñÆqó¨H%¶`•2UÅ\>[=ر®¡½½½eݺÑÖÚÆþP®ÑçúèEABQcÜ… <â~‡Z\ø¨,äkÛw×¼S5KPSóðÚµÍÍÍkÙÅ~kV³“µ@CÆÂey>$3ŽÊvëÇT”â<„iŠè~ˆÞ«.ÁÿQnäŸ$µ9Y‚6Œ[ørÿ_ˆFIÁ¡¹¸#yolc€æXÞfM33æNÞßêJ×/:¡±6Èî´{œ œÂçÙ—ó$ö»'ͫƢ[*&xRÍìVOqâgõÕ#¾Ýö.¦‰Eh`ª¦¢òTT4ŸšäªY*ÐX ñhoFÙ~ä|N¨ÛÛTjÍ/ý­‹ú¾¼´';ní‘ßÛø@ 8„&÷w®©Ÿ ¶ åc¡Ø`_ï–J×"‘q7(Æ“íÇYªõ[t²þö½<9'¤[X{D^X^1ÉÓZ{[Î,U)]m”[Ú )ŸçM,ažóãû'‡z‰„ÔAÑ¥›®Èo@=èÆóÅÃåì‘èxæ!ÒŸ¶¶Šômhi e3 µ( çø±îhöM.4ß—è7• ;åQC.Êv´×÷¥ÍŽH¡mlPLveøº÷•#Þ8ýÛ‚\­GïÙ‘ÛÚþ½bÒçu#j`?'šuV@9‚¶…ˆF¹3X3¾<šÕuª¹æ¦˜!:1Ë ãÑò{çâ¯þ:}ùò—Oá·Y.ŽâºÎÒC´âÉ@ôG8ßÝÂa¥!ŽÂAl’Y8µó¬_±VP5FJ­5˜ü-!S-ß]¥4E´ì#å°ÕV‰……þßÜû×oß».[àŸ"5—ŸBG½U"á—u‚ItdË›WÓPE7;†©ž B 8>éÌâ´–ûǕũ)SþyÞ¼ÿyóÏþ¿~þßóš…­žJ>ÓZ”s4±Ø€ÈfœHu×8KæÇÂOr#¼¹ÛSŽô3탡X,»®þÞ3¬N[ÕXa'ÿ^aÎLsEÅOÎàx{‰åëâåu÷W¼ÞÕÊ…Ì„RÏ—/‰J<ýë64œX—Üí} d ©ª LrU0Ä׿`Þk}é.bÝHÌeyèG%dqŠIâç_ù__ûÚ¿ªëkÿ¯Òüá>)8ú(µr›äeJ nw¿–+ËŸX“Ü7¶w]–ÙüÖ ¯³çÌŸY{…x´®È^¶0ºôaÖ0Ÿ>åc¹f$ßSŠ‘WôU"W°à½yiOk6ˇ[öÅ›x‡qª%Vuµæ½Ä¸û¬LµML‡T7¯m†Wilf™› 6$Hoúü¬]󚶉…> >‘7eÞÍ_Ñ’ðò¯ü^"jæ³Ó"Dw´fr<[Ê ÖIG'˜¡åU+A¬Ž±Še7/mæ §, b<Ù#(ª‹åßuÓMÉ—ZÄŠ*'RûÇR.Jpv¨ªf&“»ööç¹L8NÕ9–ˆ8’U=-­ƒƒµ í»ÇÌÆ7³6ZñĶþóÍ?gVƒýö?YÞüÏS¦œ*!Îè‘’{2tz‰ÜŽ ör½IÌ-ZiféOâúJÿ°a$í‹ûÖÎu>³oþ'–NN¼·C8‘H×Òt}C,Ö_÷÷”=’ã{ -z_­\¼)¦xÍS. û¨ôv¡ ξ =®Sñðëõ£Ù|>#ƒ¦ÉySÒMMét² ­˜³ƒ¬HÝWÐQýÚ×¾òó›™\°Px&]\€³J'µÂ¡$H Íüük¶f°7d¿ýëÏ…ù¬+oŸd<œÖ¸´‰>2u:ëê;B¹Lë¾›k̰aMÓñR(ºp´k0ç&ÌTuy[iLá$ûÀZ”Ô¼¾·6+`=Ý+n|z¸‡ÿç+%,˼¼ÎL ‚ñÔœ-o>ÄNß©@¿9(¨Öâ`/ÿ•Ÿÿü+übäÿü\œ¾Ñ¾qVÿÿì½t”å½/"GH‰À„ÌÒ’C ÍIg“4”H†\N³‰ o²7lr‚&*õ.¸iןZ*¹T©“«˜S…1îYÝJoV]µ±Uc[Û骇Ef­¦múœUWá”k×ßûü~¾ßç Aáüuß BH&ó¾ßçûûûý|¦¾áßÊ÷#Éþª§.âdT/©»¯Ý]ý|³ˆiÎsÌLâŠ\&QδØ*»e8£èg­dMéǬGñŸfˆ4# ¦5Cp0ôXµ,8àH'šêTçZ§JùÇó¿ñ—œ«ZÓY9Ü80¿„ù¬„ÕÅKWNâ(Æ{Ó‘Hãö{ Ýpû»b+:š…ÿ¥¤¡zß7÷nß¾÷ä–ꆜ(ëDLVÄV.»'tO@i/#(ëÓŠJV¤†Uþ‘/N,?¯@Qh¼õŠx”e¯)D6X'h†žóÚ|f!yö#þÔ><WHÒ¦#"´µT‚_VÕ ûžÏúj‹SÑähç·³„ò…ç]ÉÌæ†¬¯Ø}“Ø @u“_¾°˜§s¶¡=ÙÝ0 –‡u!ã§Ä2~ä×› ØÕνúê/~ü—ß=(-‰ÉñfýC96B×Ùˆˆ»BHÃË<¯ÓÞA ׎ßT‰´È©ùG3Ÿ©TÑÚg‰Vº´&’-Yã~Ñ+Š=[nUX ‰¨>…]é>)`k“èæïVN’…”Œ$[Ý–“oÚæé èD=”1þ‹ÕL2éomùõ÷•½¯™¦Ã^¯N'ꆬР¾¹0±*ú /e™ºÞMnÞÊ=x•†’?5^TwtÙÒcËWV,®Èœqœˆ¸ªqéPU£HLYiCº·g@¬C:%Œtlï•K¤{DÔ::³”$ÔI.š¾zäy™HkÁÁŠb›!-øŒÂþ6KŒÏ_X˜š­Í’ŒÆóNâÃq@pa°ß-—'ÿª7çz^ÿ4#f¯ªfBt°|Þ±™Gæo¢bô! ÏödD³ëäRΈÈÜ‹¢™ ÝY¿Š§³¢Ä\üîc¢H»b©–òè_Ͷ{ÑhN–ê†avêÒ²cKC€ܺb1 œ™·ìè²òÓ§ëŠã€îy¿«Á§Œ–˜Y`²ÔÈ¿ñjolÁZ#šß øÂ¢eËlòÅEQ$¢äáº1»³jÛ˜jìUªá½rý…›Çr˜íj^¡Ë`Vö˜5Lž»2À¬†ž )÷?¦e­*e_JQ…yïô‰›ï/§(é<ç(Šu!gzÄÔby8Ô×÷ðãÏÚ³iáâ#3—Ï]z´®Hö¤ªx W³Uƒ5ž¢ÅËÉ‚`™¯P…ÌùÖóÍÀò=!1Í!©¨ k³ž‘€jŒœ[/T#Z´íŽÌ+÷>ð艢`ƒõ’Ù=Ȫ"+ü?ˆH&ÝÒ=uˆ¿Úu2Ÿç ‹ÿ)¨ ÿ½5—‚xÞý>ðáôÑmwl‹ EilayºQëBvÈ(äwB&&&ú&êë¹õ…êéÂ#+ëx ô`Õ&=qêcô,n³Ìä"Ü!4´8M—” 9^À‹Æáz‚WÝÅï%¶±àÙëm¨]QÕ ÑÛþýß¾î¥ÁÓ?Ÿ=ûöW¼tV›¥Pbƒ^dZ3‡€\EQ¯q|ªJÒÛ&yÿÁ|‡ òoòzˆ <œ¹ùÙ·ßï-zïíE7=#&o2›G´¢ØÊ¢4ç3CÝ#žL0ó™£Jþ†çÐ ä¼Ó¨ý\•¢j/ŠW²éÛ4ªûL -^tŒw‰r¢q”£‰“g`½—e#›×GøÑÊÔ½r÷è#ï½~à ï=ŸþÆì7nŽ:xÖF„Ne P5Vø¿¬—4„í%SI­KI¬ Äÿ¦Y*¦ 01züîw’^Ý£ìôÜ~äõ^¿î½mƒ÷—Ǥ¢TqÈyË+)·Ù8®‹I"@˜_äó(KI {šaâ(+_¡3ujÝõúhMÕ›¥@2œ~n_+BF0]Í*9Y5Ìß¾—.¿ú…{oönbò¸áº;¼ë˜=ûB‘—ÊËZ`m3E¸O °ÛÿY³*Ý5θÇÉÎQùÅÿ! иFãÏ© |ˆÿþÛgÿüB¬è:öÎox{°üÃ.ÜüŠ`„eвoŠ9ëò>Báf§ÎÄÅyý5ˆÒÌ~O"`;Èú`Á)Ë ‰0Ç`:Ä,ìeé%BPªŠ(º@yu9<ŇA’Ûî¸cìæ™NL{†ßÖë‹Æî¾}ö‰_ÉÅ`(½/“g•Ç'Ÿü]‚짇”LR¬¬­®îY/÷è¼5}û?¡ø/6ŠŠOgÂì¹6?p³§Þ¹÷«ÛgÏ~àÞ¢m7=Ã=µWe ¨$¡ú±0qv¢Ô¸H…H¾ª ²G³2 §JЂ.Œ² hOD*‘@¢E{úˆÁgÅË*ª5Ò‰ì[áéhæ¦G^zïï•gÏž}¢¨èyΊ.œ`òÉxýYÝ:ßAçA*(Nk†¤.…÷¬—Ï9Ýß>ÒP-Ag®kË®JÉÆàÅö–•In6%[çÿË+ÉY{öî~Tø»›®“ºÍ ?YÿþÞuo/ÚÆ²4K…+º…ÑŠ0EÖúñÔ/ÎH— Âé–*B.=‹¡T7Q–µb2îe‰È€ND¦qŒqâD €‘C€)Z:ÎSã-ºŽK€âÙÞí-6ëa³Þ(÷‚ƒS*H«z¶®)ý¹ÅÒ/+{GÒ‡¤c‰Ê‚“% œ.O]m%ÝÕ,ªÛµ>U #o ëtLӇܗ’:åHïúÛùIŠ¿ÍOÒuEƒL_¸‹çòy dMžiNõ+F0ù\ ¥¡Áq¯Îr_³†™H QŸøCeÈê‰Þµ[ì²]ž <™@XÈ4ħ*MOJH< DϸI ùŽw{ë×m‹‹¹^üí†ënò^ášý˜WšÝ›öбþ¸F2AòÐ;›y,›`Jìïl?p²xK[IIIÛ–}ÜP°^Œ£%céŸ$êY+z,ù_« ÛÓ^Ñ a±¶ ‹õHºœ$æâ3ÌÅßðÈ iñE„¸Â’ó;›†ÿÀß\ ïJ|=s¼eB)Â@\¸Îº´d&\ÌŒÎ-:ÜÉÏ?Õ>¬b©? +©vdj¸î¼çm{‰%éÆß˜/z‰Ÿ´Gâ⤨óÒ²·s«;Ñ ÊCR£¤<&9嚎'"ý¥¥¥ý AB%ø2ÁŠ‹&8€¯Ã%¢jc§r¥4½Â¥3‹Åÿzâ ¿×¥£É_£º‰ˆu8%9¦oe,iOÀòî#‘–[PËë-â^ö2ïNÓùuÔr…J¦Õ¤~“ðêU`6‹Ó ¾­ÝøìÙžŽJ›µe[Bõ1~ ò‘¿Ù‘Ç4ÄV¿k ’öÐtsÔ‘Êߢ¦ ¤Núß/þDÛsu {¤½Aáìâƒ\#nxiÛØôÙ\³½¿NˆÇ°Yˆ®ÕØZròµ†DÓbQyáO<÷@!†"H9LqÑ.sJ^)^€×=Ãæüü?1«Íiú ~á3î+÷Ì|¯t",ŒŽq¼=»ÀuýWÆf~Txù\^„ùÓ´¢Î¢#«4ÒIg¡1ä­ÃHâï™2K´ómÊÕÚ›³ZꉠêÃ_é+Zwb¶qñìT)J:Á˜'^lY=ñÚGõ¤ë™É0èbïSʸ– jIv ‰y¸Ä²íp,Ói>^ó›dÔ«QÄ›§ˆ‘©Ä¶Öˆ@¢[[ÒÏœÈK_ü¡vãÓÇŠÞægíí"®p¸”}ØC ’pÚ£iÿ)'ÕgÙ-‰þ‘¶ -‰L¡ÓçOFúc s}r‚zf·äŠ›9’ÃàôGù±Ó±·X,г›x»:£-VÈráÀT›Jü%_E½ÜZÚåi@[L$ù35¤`zJûvfé|ó‚šoÈèÙ‡;Æ‹IL6¾CãŸþÏ_{éßnò„Í:q:*¬ñëwxws÷ȼH¶ >‘Vä>@жvš;â*)é^×½oï¹–ÎÊFqµžoßܳ¥ººzd UP–ƒ„„ÉC0¥zr96– qa§†ûÁ^{0£B®0LË£ÉC; ÅMr3p6ÙŸ¸7Ž}¿Æô§ZxåsºÀUg°Ž€ùñøCŽ<¦ò`~©Hø“´±˜25ôT&"lß/]¾ó>ŸÔ£ŒWÅÓ7󛽞)Á;ÙÏh·hÀYÜ ‡æSœ¤JE8ׯ"݆[zGNl»¥¡A²E·å•ƲՌ¥D$òJòPÎò §(:qBY`¸É1ÖÒÅ3}Q h–ãsYˆP„olAgBG¼hâ5ʧóž¨Ã5tH×­&a ¯|; Ÿ½¸1ßZ“¿žiZØÌË4râ¨åiS6kK"êÍ ðææ±€þÂ…7¦Ž*õ*òh9íÍÑœgÞúÉJ¥î‡kÉß5<h¬*îÖù~cª%[WEЈ”1¹9gc¥úÐhTä ì ©$D‡ŠmsJªÉ'2’m›§Õäó¶ÊA¤H6x“§n‡³ø*ú/ßÅÞOù¦Ÿ=Ê—r£cÜ‹<0}4÷Žzÿ÷¿äÇŪX«V¯îªÄÎŽ&Ó¨ Sµ[†cÙ²ö?þŸ"6KäÅ‚\!Þ¸PäÝñžP1ž“pI‹5@ëq1&¨rf„Þ :o-•ÖÔ¨ÒâÚÇ<iN\Ë1âÃÙ^âôqYè´Ì³£eùù¯²T}V¦§4Àg²¾hðîÄjÚv–Èüõ;M/ïIŒ 5Ýù¯ÓÓñGþMF¾åÊÀË‹ìα ÊB"A{Åsï5±õݺó›èzâ‰'v?¹$Q*7¡j/µfHÙ41Qê%Ûs7ùL]|ú"Ä’¹ÓKÛ¤Kg‘!×qc‹•w.-³Â«š™SΘùôfÕKi‡ƒçÛuŠoÑ€”U§Þ´&šók~çD–¹4Ph߬¢1>:%Y䆵µÜ{55±×™þàå—´Ì{åK7üð‹ÿ¶(>výíʯçÊ•k»EÝ[ÿÿþlòh¾Ø[£RÒ‘»KS €·$«@š+%³WërÖŠ9p¿xþoLOŽÝ¤*¢<'áå…¢×ùÛŒz‹´¸2ªëX ÈAtÛ¼*¸þøƒÇÕBÑ[q¯îñ hï-iž2Y„ —І-Äl_§È}5¿c± '²$²óB¨ÿ+ÖpŒëÅÚØûá»Z<šœÿ½¦¦7ÿêÅæoúÜ×¾ôùg¼¢ ¼Qus<=°žÝ”i¼'[Gwíc•±–^…+ªáQžxNàÕðÏuÆZsèee$+ej{Ã$# O†Þ-C¬^*b9Éæ˽›^úü—¾ö+v[+v ™^…#G‚(" !<h9®|úŲ§ÒÞÑzâbCs¨Ç€>|ÀùIë§½þffÕôsàÒ õ>öÿЄ=2ðÕѹ¨gqVïWÿ°éÍð²±coÞÕÔôòQ¦1oþè®;y¯Ü;ûÂf”Y¬Ü0¹D"OeÙâZ» ”Cù(‹Õ¡±6ÿH™KMñV«¬8æØRñs¹E!nF!¯ßÄs’GïýÙÊhÑâYM/0 §m27D,i³oˆn$È>od´ç+Âñ9mÙËÂV[¾H€ý®@š´úð¥”ZxhfaV+·@}›Ïœ»tÞÑò£óŽÍl JJU=Q_9ÀÈWï‹nxékÿü×±ØûLEúëØèâï4Ýõrx¹7ú×Yÿú³ÏÝ{7s#¹ JmÝ2€‰o( ˜Ÿµ÷ ¤>I÷ ^òÄs‘FÙ«>«œ†ÄXxq—êT÷N2±†9r zw\÷Å/~ñ%¦ Ç§)¼Œ…ýw5q­&lø5 ’ ¡Å}~jÙy• ¡Óžw.?_3 üÄcÁ§B:!˜_Í®´Y`RbÑ–Ih™—ùz(À{§—ƒ1êª[Y?¡‹.’RÝäKÜ­sÌ…±›^ÿâÛ_ª÷r×Ë˼e1U¹ë¡e^Ýž7_nzq‹}û÷•äÜðËHGrŸ«$e_g‘ªU²7Òeð–:¥p™@†k†Ü+Uë-%“̇rº*éߘÁbgéó_û‡;ñ·zWSý_G“ï¿Ìîá(sé‰TÀÒ0§oªX¾”Ï-›wlîÌŠ"ÏÛ¿ Ù4޽™bl§¾o‚øÔkßqêU±K—…¿µÕ¬sª†§ç¿ø_—†tËJÙ¬èÖyIˆÙá²l=vdÖ/¿ydtle}SSÓæÇÆæ±»¼ëÍùÌd—´M:4õ?q”¤l]$¡àj‹KµSïJ© ¬ê–T£Yfj.+|ªU5H"ÖM6sz éy¿z€ù‹{ï÷â+ÿyÓ? ϱƒŸ!foßljúÎ_Ç@Ù$OB‘ QËQÑ×7s^‘ ãð‹ãÓì,XúØÌ™G/Üó0“KŸïãÒ ˆEˆnâƒEó¼ôߌWÏ?'_Ý‹'S‰HÒLZˆo—ÄGÑ~}x¶¨EK©0µåÃ{ÿõ·–z‘=/3‰0Å[æ8žAgÎEžêu²[o)„"){*’Ø¥L]oU¬”ƒÉìîH «¹ÉêªT©6¶…e ë3ªÙY<ÙtDõŠsè÷Þû³5sŒÙ•ï½,”úèC45ÕÏKÎè®ï½ÉBdüz‚TdéŠoUjÈÄ‹óÄSÂWž!¨)ü­jdâEåóæVÌ ×ãÁjKp/ìÔD8´iæ±euƒr=0z‹™ªù~kåš‚íï<õõ·î»ï±Ç{k3oÕ=Þg{¾v€Q©H[¿êŠÜ4ŸÎ¼ø½Ÿ+b~èG?zùƒ‡æ%ÿÊ”åƒ7gŽzÑ59{ºkå3;-o‘”ý-aR»¯2Õõڌ纲„ ¤ÿžµÂ•_üs«*Ó{‘]Õ“³÷dÚºl{Oáù©±e‰ƒ³ÜK2·×ôrSÿرðšf­ƒèâb3Fdé¦.šu^¦}û 6ì*¨:ßÚ8к`-_E¢ª[:?L Ä"uT$üðÌeƒ@Ù÷Å_¿Uk¬]ûݵÍkË ÿÄûAõê»enhÊÒã·)å–[ñõ…Ù÷{c×î?oº+üA©—xÿ;RY¼hUNKRÛp@¶l½dË× HÄŒv•äur0™Té¡ÞKV o­][XxÏ®~Õ&aß>9çwïA–7•ïáféýÓÌo|éÅCÌ`ã9LìÈç¾öyvÑȇe¬h]Ó„-–Lð„̸­¹°Pck”5? œÖ>µ«½jM§€´ª”œêl:`é“’pÅiùEÑä@eËæŸ<¶J›/âm¹æ­ïr䉥:G˜£zרHT¤»‹ÆD½äÄ£w ~ñõ—¾ôµÏUļÓÌ[Þu×——M&‘KÕ—Ö¨‰…xçŸÕž[á;©ˆZ/)ÙPuþ|Aq÷% û¾½à·ëÍ|x¦õùÉ9¿»¹Û|»lj¯6­&Ÿ‰7>¿^GmÒ­Û P”y-û™÷ÄÍŒÞ|»lƒŽÞôžÈL"Þ`Qæ‡÷3Õ<ß;Éä[oɮƌî Fc•ì‰oo¨®n¨îíî.áW7ï‹T÷îëÙÜ2œÈÄ"iõÅ)°Wž3Þ]Áüùé÷ß¼ëå7÷,óF¯çõ·__”»ûöŸË²//óòN¡v² 2©PùÜhì7jÎ}J"ù6_C©™¨@Ö²x…J”£6|{íT_Š ä<{-„‹ML„1V‘¯\åkQ£bBk›'úÒoœ8í .U»÷îóN_à…£ïõ¼ÖÚIÌJmu÷¡N ê?:Iô·®©:w`Êç¿ù|ÏŠCÛ7·¯i-M$EP™Š$D6^Úž×pÙiù† 1ÞÞ¼ó_ö³Ï•³ãÂ3ô¾þH{Ão¨$ñiv£Éí#8Äb ¢RQ"efUϹOM" (:P×Ǽ¢ER,Àó¸—á¥Áz›e¾‘ÃNÑ(ËŠa4ð>AÊ™¦ç×L»8õ뻕žšYñp€,‹J… ç²øpÈO®@C¶ÖÔ<‘ÂÕcYÐÒüG*ôí~žÙõmÒ)z§…Uþð~ÏHä‘mì|N¿]ô«¼5ÀfU÷GOfu*%½ Ý#ÛÛÏ7¤’º°&¤O%J[[έØW½®7WÈVÒ“‰ •Ó’ƒ)oìþ™%eÏò©¸\?d‘‘;A^†g.”ÝÇÏ ˜qEµð¯˜³ÙM´äç_‰@x“°\¦OþZ¡lx3—Hþ0ŸÂ³{½RE¢íÅ0ôå r9èË\Æ+܈±,eµ^û™´Wtý‡·ßíEKÁH|ÉÉxjKÎü½­›ÅT—òzí*ho¯ªªjo/ؼá›#[ø§»k') ¶§2?)q'*â7xR»®cÒºò>³p Ü`y‰S;LDœ¼òu¦*ƒ|Ž×«º"¬ÖÏNuØ râ>‘u%yðÁü–elª·à˪ækç,÷i£•T¼lO‹†º’È ïÝ4è½ò«¨—úc7ZLh¼LöÀÇÝ{YJØÀ.–v—´]~×\g>ÙÏÔcÛ#R\i•~˜¬‰,cŠò¾Ú@°sŠrË+¸2p3ѓǑð&­eS²0œóxñÙ¶•1=g‰ìÐ}’ýLX½ôŒ¹Õû3,˜ÔOà™1Ž@€0±z1\ïÕºZ½áÞ@›0ê Þ$F~¸[+òâ×(ÞäÍcÞ3/Ý "¬Îb W&W7)VaÍ«W&{’ªE}DW1ñ";$¥§¯qü‚ýü™aßî, ò –¨æº6ZÌŽÉPe›—¹Y¾¸—\¤lÄK7¥özÉóq‡Dêj ¤Ê‹9ût|¾[Nü°·rÓ×"ùM8xÝ"öÐÎŒ#.!},| wË;ôW$²[â2ÝWÕ¹Ò&Úâ,‹H<6å¼°ù8ÈO“¦¸¦ÄŽ4ì–|k qo³°k¦æû1Š«†JH¤ëÑø.moÿÀ[Um—˜ç’ÑŠ•ˆŒkQóý ¼]VA1ðÕ»ørW£óO^r{/«½rBÉcPfèÂXb]Ä*qPœCeûMÍ•ä!ÏL”;À(©Zbc9óTÙâ´·x-~_…çåE¾s-§ŒÅ°¬h™Ššcì=Sn„¿Ii¢8ñ {7]÷‹¾âÛ1•à®ÜûfŸÝ‰ÄO¹Ùzw%2^ºƒ½·ûxCÊ#©œœp ­&”W)H|qï3‹i(.ï«L}ÚÚÂ)ïŸpÄ¿zC}«©W©,.þùòaVYÕhA¡„Ðb‘Kð`p®~S.ë•*^Õ”³¤LGdÒ5û®/òÆžY4èEÄ‘êa/æÅg½ø.Q`¬äo,¥z†©Çééâ‰äUÉc3òýÆì8%…åä9EûhÒÊðIwQí]ûVÁS—÷…Û=¯H¬¸ƒ…ù$f1 fóq-¡¥4Ä·$Ij–U  Œ`7²Fe#,¨bRÙ9ÏÏïóFXíÝ{3ÏÕ»ÚŠ^"€‘S½——¿bêm½ñ¨ Ëùù‰Æ~=âDXå/öiþaP:Q&Kζ˜¹¶ìÒì‹È]K,)Í?Ÿ®Ö³£“¼ÇêHÞS¶•[¬šßHbH¤ÿ˜……jYe¬-aVAzN‘+KýÆí׳À âÐW¬k͹ƒøÙ/¾ÿ4‡ íÑè˜(«±cránµó¸0:j2®_V,„Òps§žü©Ú',«Ìµ®-Sûê²Ñ^ØîÕÓŸ²y%}G=o½üêµ²?5­LW×Ú20Ÿ,ús¡Ä˜ûBïTÂ8rœl´D6RÂi·e Ë²snж!¾·Ú­”ĸ×Ä«¯ûïIo €ØU[ÝêyuÞî£Î»ã%™•0‡îµ˜ôÙšM›šÍMY;R0!`Øöù Õ¾æÚ·n‘`µkú(B9Ùvõ!¢œøkhKD$‡fA«˜3[ûÔ99oÖ|ß!38Qø–øm• uU*1´#]Zq¹Ñ: Ï•šd<ÅòC¹Dù:oN ^ûÂw`¸‚\“ëRɺ-»*3Ù"†¶K,àGï{ƒÿ¨rÖ;Ø­X‡>Þ#ù&¼1œªj ‘½öãü¬Nãó%6­¹yóŠ2ù‡ÒÏ-š›IÉ£¼ÞS Š‚OÏFYbÜ=ñ˜b¿ußÂNïïš—Ù,í•Ý’öJŽ (.ͪW;zq”L<,óu›JIÀê ¼Ì($ÂLæH^™þá…:Ns_HûÖ]ý,¤¶·¡GµÚSA4ëÞI^fœ^ÎÜÇÿ”U`5ŽXôQé@Š6Õ#OM;/¦Ó¼Ri=ÊþfÁI.–½å ÊJI2ÁÏöÚ·ââÿ‹fiÈn3.\"ÒL™ˆVÉç_xÞÛ%Ò¦hŸ6DŠÃ 1yp‘“Ã*ê5øÒhÙUU›±›2ïë0³Ut=3Öîö%oÄG#UÅ W7Ì*ih;PÉ‘~#çÖ{Þù†,Yioð¦¯È·xÃëÿÈ<ýÀ7w˜èDBùV„1þŽI³E.ý ®"¾[vÙ‰–Â)»Efß…÷"\ ÍÓ<醴,‰êÃâÞ6!sÞúBép$]Ù[q³Þ¶–S íÊgòx5£ç(1›`6%!–9¬‰zÉ 7–Ž&Wô—`K×G½ØùžË‚þ\4º{ªàcË–u,9ŒgY2ä/Sþ?ßû7UÕbñxb¯ °ÎeLWÊŒw–FJj¯¿†ëH>Ó¦½…_n=þÙ››…»„bóÅoÄt_nM‰Ò‰0^bÿßÛ.±Bâœ0ÁH¼.ǽŠæ‹r–%‹Uû/®m^ÿã¸uà ëDŸpÓ/«p‹$ùeŠ&á5øî¨®7o}Õ‹¦~zn€ïLÕmªG$pº¿¬ÓÃÄ©¤#µ%­¼£« yE½Îëi§Ç·îî]¥|è§}d2ìÜ©£¡í”€|äïgÍ>á¯øÁðÖGÁº{b^Tþî¢ÉÍc攡ôN7Õû_Üš×ÿ‹üwœŽÆôàgÙSéè>)VèÌ|;ÈÅãæ¿Ê‰´!‡ÎC„¾ñ0‹§ e ¡eOEµ)ðô«7oý ýä¬uÝüXMÔˆþ²ì+grKÇÇ‘Ž”´5ª:£èézýî9mØœ‘¨µÕ%çú™‰‰un¯ýt2©-ém(9¸ë|\H#“Ž&.uk](õ²Ak¬cn$-ËŒˆÄØÜñ3Ÿ®h¾µÑ'@­q†xéÈK7¾ÚµpJÌöÇÅÉæ3&üÌ—ÝóúÔˆ@U,`™U…('ž£ñêeo%å·Ýó†e ü·¨¶YÍ[kÕá¼=!›¥¦D…ï$»5ZR"—J•DxI1°®S2’0Ÿ¬­®-H‰yÅ5+ÚzÛ®PÕm=’«N¦rCCàÉ»ó"^2 '!“}Ìœzé* 72,ÛIJ)E)܆ĵ}Ç©¤¯PF…éCJÆTO¥Å/dGGc|¶M¶§´}Qtâ/‚¤â×Ì2ñUÑ^A¼ØïIlÒµL¢*Îúîñ|¥]v$T/+Šˆ²•j°ÒyòI˜É.þ(ŠÙ¹ßt)Ó.Ùï.¦íyí-çÔôáÀšíÅÕÕÝmSEwuCõ–?Hêù”ÑÔÎ%K’Я·‡I>ÌÜÖÆ eÑÛ¼«M·XýÈ“ qc 8qߺO½8šY§H$Ï©Ʋ[2Š@Н‡¯Í ¦ï&gBÌLlm^»¶T:iZPÈYI˜ÉÜÂÿtqóWÃ)fŽè€WA0SÈABi}Óiù4lž+ó‘îbée=œìùuÁuhTùãJ¤8âµ—N WmáO[B–áårÑÎå õ-=Ú×4ZYðÙ&%CAe5—È¡€ŽtŸŒñ@WÜ×yHfU`!4HonÌ 7­h<ñ¿ÔÔ,àïÛíœÚ€²à(õߎç?˜Ïe'G™{•™Òù}GÃôÔUjM}¿æøVŽÿp€=þÂçY\òßîËÏÏw3{R[,8ž/˜J¾?àE+B–vPL˦ o0/”Ž=Ó^ìHä`„\3‡UÛæâÄF™YA¼v{GG—t %õ¸Uk˹ Üw©¤zݺu­c.fKñÉC»Ôì"˜ÛÊ$‡¸4ص“ýe °«ö{ÉÍÕÎ8–ÙæÊ#ªJ¼„Âþ‡Ç@Ëé~ûSÃçxú||Á‚­ ØÓ깸àÝûÞ=õ*ÅàÓ)ÌbUzbáÇza¸ô©zô,­y—¹šÖ­Çÿ]*êUñ<§æû1/ùSÃ"ò”—™_ïCþC[GÐ:>"pA££P"#bÖœÓHF°Ü¥¶:ëTLgbK:–ìLšÁ«L<1P:\Ù¹F^ë++KûRq¡G5‹ £k‰¾’Ñ’G÷¾V/ 0³ ø´È-@?–NLP€ó†àŽ!O$÷ÆÌEhF¢ã»dQžå&ÌûžÓ­Ösù5<èµÕY¬#H—Ìë‹Ñȃ5<“Ùøñó¼žÌóö'&\…óŸÿƒ¨Àu Å{DêõC3Ç¢‰Œo©e‡“…io¤v]{Ú[ï.¶í‹xCòŒ§â¹¹±ñØ\&Óš!¯Ž”evDw{Òk<åiÚøÐƒ"$Gò˜÷bâ˜8ÇeOœJޱ—ü]æÀ)õ¤^äoˆz)þiþ|ÓÊç Âñ~)X+T>D½¢™FÔx,!J.Âa.‡|ìHæ¡ióB¶—Ž0%¬PˆÎØã@"¢?ÒûÇQ‡Î±„óµ® Òêq@³â©.Ù9”ÍL6Dšdž†ººÄWyìËŠ™\ÛpªÑ‹·ìƒï¤öÒ¾½‘Ñõy¦#5Ò*À¼‰z ‰B¼éL‰ð¦éÕãJAØÓþ€ãÄþ.åìS!(_ G C^ Ëc(_îEæFÀ$¢1QAä„FÞùü|YÚͼzœå 5ÇùFnEê­\•÷1#“È\/ #‘tŸÂÕÅ­^² ÛÚNu§7fžì’%];‡†bÉd&ŽŠT‡ý/=šL2Ú)M…ºFsa&wwD¼T•­Æ×nïÙqÊL4œÙq¦ÒÓòP(Š:‰ó%Õ`%dœ×´”UÆoD¢üYÖÿF£4]2¶Zóîƒ-iY¨H~ZÀmñ|ÎKîÊ÷ÜqdÅó†Êü 7€éó?~°æwí£Q>ý¨Ñå|ŠÌ( Úõ'ÔÒH4^%2r›Ã'Ôݰ+áõ¯ÈÚE/)NxIdÄï]ìÚÉDÀ¯b°‹Ÿªu9ö¶T%R–̬ø K=ÆM=1ïT©ô÷õ:ðSíµ¶¼.èUþŽS ý´”™ÿ¿›Ÿÿ ^º?÷. |<_ÌÑðyù¾†øS¯I$ÐRã¹W™Jý˜½ž(!zÉáªöó‚¡:éç·Ÿ_Oíì+[ˆ4þ©>-Û¯âqøõÚN {£-µ¹pñw¥£±zÉT®|3,'{d[þ?Öjõ0Äk*q:±ò >¾§P «ÞW³^ê|A{'¯i$×·¬)å GéÊ|£¦FŽIÒ¿åa@NAèªkB,e±¨ŸO²åÕöʤÅýÔ´L3ÛÙBv©¦¦ñr!-‘õ¶¯À$’{v •^å©Ü¤2)¶äÓ_£“óÝ^ªÕl÷Åpä! ‚E|U¯'~| t¡>ÖºobžXŸ[›æñ‰ò^cÄ%u3Ä"—œºBÌš˜˜;ªVßÅ`¹ø]áiDO/=-þP~$ì#æ*C9ÍÅì£ZŠÑ¯òÌP’`”ºíÌŠÞîÉX©7–úÔòà õ—¯†Ôc¼¸*®òPŸpeÌÓ#>æE§:'ᙫÌëêT(XtT=Ψ—Yzºîô²å{4Z ѧšb§. ä‹—Ö Í;­ LŽ‹-;]¾ìØ‘¦pSÅÜcs+&N%0ÑÙ’./jMa:¢<»×¿7ïLv%i›|ï¬û`‚eZŸN;¹®÷\®ÙU˼G’ÇȸJ£Ëön%Ÿ;EÜô–`‰ð¹ËÊÎ[9«©âزòòyË7½Y1O étEøáYM¡pŸý.mõ()PË¡ðÛn ½¿´ˆ½™¢e+›Â¡0 8 @7Ä2ZQX`ƒ`Qò‰N¼¹\íÐ&6Ãð·g|_í«¸ÕâÑO©#]õ¦–e…B}ÄÎÁQ |æý@UB) ‰Ú­(R®½òdT’3Å—¦&ÑýMvu\©Áâð''µˆµ·å9ÖŠ™«õ2õІ) iMoèÎ!ô¯!u'€S‰Yc˜†©^i’§vüKÚDöB}èÞ¨ž86?Í3ÃCž_§ÉÀh¶~ÏìÖ”DRÛËñÌ2;¯Ô` *ëu—q޵:S¼½_×bÆV†0b%¾·½Äw£~bW;8~=à]~GzˆI0œ£C¬€J‹ÍP+/¯¾+´g™†©ÊÛErfdJ"©­æ¢é¡+§Žl­¾2qŒŒ´¨íë8˨ã‹Ã>$»¡€å™bði"ò€[M¡Îhµ°›WÆ˰—ûª0ÛD$„)÷aÐí[$g·kXÒ+Z?qL—ÅW@³%D2ïÎ$"'º®Äplžœ ï\ã®8Îo(UÚÛýËdÔ«›Uïg¡CE\, £ˆðÙ§8yzc3uHø†-T Èô­Ý» íCÍ6r"4sP)Iª)ɵ¤¶ºŠ§«£SWî@Ü9UÐÕ ¸r\kõˆî¼ñìªá'C{B@aWó¶P ™<”]HÔý(8ï°BnöÔQ@'ùŒ43Ð9XÝÕy `0ý3<`É¢0ÿ¨6ÍÃ{‹Ï`‘츼{¯m(Àí;§T3=K t³ˆcGñfí=’O~ü /œÒ@â€d‘ܽIà…up›½ÊlŸbN jû¦§Z€{e;~¥‚ ê»ÓnMG—zŒŽêâ/§ëªÉÃ"ùýxñ–ËY®†4:»¼ÝêX2”ŽæfÊQ¼#(Žñâ­0²zãÇ«˜@öÇ9}ó TG ÄĶaGAéÛe€KqsQØÙ^Š{¾þÁ¨hXx(±Ðg”ú'ÕðZ™[®àÍd±¤é2•î*ÞqÆQ“¼ËY®îKç3R$—Ñ’Ž%)^L]Äs¬•ÊÇ™¼3-Þ&þÚÇ?þ˜«Èª%ìUŽ~oêÄ[GîÃ’>ËÀ‹øØ¦SX„õÁ^ƒ÷&.C< ö´Sà¨Ì!8O…$‡¸îÆ3×÷Y22ôËȪuEñø™€åš\MÚTHšNå¬öv,éâs•é*Wìµÿ ËM{ÎÅ1R0 õ—«¿VßøÂÙRQÛf…)–sˆãì-º+µ¹‰-1!b$j­¾|U?r^ëAŒ)¤ÈºéÊü”ÀÈÙÆa~ß÷˜‰ï_uöpÌØ­–3ypDÒsfdr™ô¶µ'ä“Kdžº\¡tiÄFEìà€¼³WÝ’—ÅTñP7ï\iÔxR|–­'™Z ë`’,4–E\^o„"‚{P‡&Dþ ¤lž$TKà#ENÃÈìÚ\4àîU«e ³Ë«ŸíJk‘DÚÇó܇Óó{.“Kµ¹1·ð¹i™·ñî9p(;wÅâé1Á=Yy°M 1Ku0›©bÚ‘·yØpŒu=«ÅññÇÏr¿Þ%Hûä®…-^Á< 2 ƒžÅ–Æw‰E@ [R¯Ên°ƒeŠë2>à’„œº¶}¦ß¨òø„ذY —²Ç”bGî…g7n|:aÚàý;"ámGÞ¾Ûr9”¶†KÃI0íOò+ÎôbLC=—TX.Œâ‘3Ù,•‡öåÑÄÓVÊhݘT@ÂY7NqÈãüÔÆÂžKâã”Ât“Bún ²F9iÈáâØ,J²pÅ lEýIŒ_gv3¬b÷¹ñã×’ZI„HƳÈä÷ãLQn«Íν^]ý¼¡ö²‚4%‡wµUkÅØÂ„1ž]5¤ï(5â`±.’‡0Z«^ã2·”¨žA _'±äÎΓ ÖEë¼Bñ{£çlæ²T ÅP›ÍîB:d0åjR—n]ÅZõ˘‚t­ Âotã³æ1DÚ¿Y<žåaõôpë¾o‹|°Á•?ž;_*ç°w&QÙþNuµ˜n¼m_qÞÈ™œÂ`!DO{¿™!Šw<ëˆC­U|Œ¬®©O0r *©à‹ú^ä5“«€‚h‘D¬HFKÛOçMò Ùæny||ÇÈH^“»öí+ÞÇÿÏþž7²c\:ŸžÉ$!sò¼ík,¶¯ÐŽìâP‘VJGZ¬¦kÓŽ2>äˆeóŽÀj LJŽ*i–6Ñ¢X‹P˜ZjãD`H*Dw BtÉ$y#V£%ik¹R­›Çs˜.Çáó«Gè@TüÔ™)\L{«Ôô¥¹ÅÁ#-&Ý1ë@4J©=ж'Nq]܇4n¾ FA/^oá4 sqžÃ %â0}×…„ó<í0÷è«·»qãÙþQ«&éóvLA&ŸöÚ‘—wˆ…g 8‹~v2q¨ô°‹­#aŠæª@(õT¹pLÊ }ß)$]¤5.EUk¡Y¢¸~rBÛ‹„y§Ô´>> âúq6Ûýnܸº# ‚ÖxÿùgŠóƯº0Æ™“ÚÞRš£Ùé!èN.i´^àÃD§_œð‰C‰ GÑ|Ô”òaã TŠqMÄÇa’ö 9,$ñD‰ÚªöÑŽE!  #Rôø~&ŸÍ~Ç7>»( d’‰tœÊ»šŠÂ„‘·¢ 3‡ƒòÉ®§/+ i‰d„¥‡Ôd˜'¶§å-â 3!,ÎÖtbˆíucêãÊ&DDé) $ ŠêR`šñšIvQ2ùxUc&wÑTcË®ž¼«¡);Ø«œ:g÷ •#zòÙ)‰CEZ<ü]ÎYGŸ>ÅÙ1¨P8ýnPÅÅ0ktˆáS'`îŽâ¶T–â$uzùŽ­³š#V·ùj›¨™¼ðìd7ÍÎþH&^&1¼fóŠ‘â¼‘ñÏ ‹3 Ö—¦œ¤>Õ±jãTÅÁŒÖ/¼ ’‰#Ku²;¤Â)‚Á$9Ø3B]B[:A´y†u–ÏÝâ.®s÷XÀ¡+óäeĘ®'wÆQ$šŽõwVX±£˜‡µW`¢˜(Šó¾¹¡`Í07SHéTÇÓO]Úhu¨-C¯™8ÖñŸÆÿRÝQDUt¬N>1îBƒÏØyÔP÷qÀŒ{ï6ïNR8*dÀšDÍdrÑ"aîé‰J&6P¹¦}×^qây¾1þ‡ìb`) OPFΜ:PТ6žDÇÙ+”†NFxVŸ¢¨IØqæÛp’¸TgÄ!¸ÒŽÆ¨)ê„j&¡¾ï G²•VìF.US¼y&“§S£AT÷L2R:|¾ª}󆽧zÆÙƒçÒ)–)"O wôœZ±as{ÕšÊÒX<ØtãÝø'W_¹4”Ñ’ÉÈщ‰ÀÂÁÙÞðóñWSÔÆÓÓD&3|ê*PK¼îã`Ÿ€5íD¹oòÚTø“ÕOv¥ëkr?¥"‘þÒÊÊÖNuµòÍÏH$‹«}Ýà•Œ~úÙŸJºìÛ¥ÁË@Pܤðõ’­ZŠÚ#Àݪ\Ú‚áº1u;à¯NQk8ÑnBX¤„‘U/¸E“)¯WïïJÄsPRD1_`4¨ÀLÅv¾öY„“‘è × ÈÐë€Ë‡ã¶Õ'ëÄÈÞƒ9!P — *Û^!€^ÝP˜ÈP jŠVÛ(ª6öÝÅ Vò—"Äú?ÂgŸ~­+‘LG?åE:ö¯þø³ ÃôªžLK<9b÷ õñ%СûpÚ׎`Y¯œ°áÞ¶³½Næêg°#l_•€5 D]& VÇ•¬,RYµ¿£(Ÿ‚\<ÓEL t=ù´Åg†IFv*£%;‡µÉo'`CÚ/hZ|Š6à.Eçž²b( Ôu1å*1stÇ~¬a‰šÉåCÞËJ…=ÕÕOï­±(Kf².¬³ˆv°¨HTo_Ûý‚ø¦«# m´ÎÞÈŒÖØàÂíÐe{|xXØ|'é¦vÖ S¯úºHñ*N…-ddÀL·ŠâÁpe°DÍä3(ˆ+ñŒÏîòIÑ._>wéÒ¥ÇæÎ»|åÊ™‹ç/lâ­—èk«Ï^éöªžãFëhß„9¸Äšn»Aáº.žÑ5…a8àn =ì‡ØÊõQmŒÂr:¥x¢ÂŠAAQã<+ƒu¥ý²!ÏÙ³«DZÖW(Äÿ«ïëts\-Ï^m< Œ¬¥ªÌAÁ­Ü(Âa‘d·a—©›˜<Ä8".‹§&…`4@øEÑ"ˆ*áveè…«`°²t(Ä“åWê㮥 ÌÈp½\}µ%Â;#|%šœ¢Èû€Ip“®ý™ÝÐ 4Ù%‘Óï°—¤9•@§\½Š3DQÌçåѱ†¬a]Mƒ…7ÆÕD¡¦TÑš;ñ0ß Šyµ"gPÒ‚³{9àçÓìý#J}wЀÚÅ:f[:ÄçâÄc¹úÆœe¢°ä­7ûÀ ¶*ðn…ͺú~KiE3G¹–xnœAô©ïã/¯µ®ea\`[´"Ô©¡àØ®RT£÷å\Ü52XÂbíÏÈ„´™õû™ ÜX.9{å³+3Z3¸Ñ↠ߣ¶ÃJàf†^R6éu–1ê± { ÎRÜo ~Ó‡Ëép˜b‘iÄE™®¾vƒ—•2‹ëQÖepœ©!°œ-Ê«ðæ7õ±Us ÊïÂqη¡^ ÓÕsYŽ1²³)>NÝ¥Oõ&ÄH$l†ihk™®ºfC´¸Ëùò=; –øúÉk¢ò§§$­$6óxÒ€€I^бû¹¾]!‘^![˜Ã݃ÈòæhÔ;Þ¡Å"6ä5¬¯‰ÕE%ŽùmHdÕhº°Üf õüt‘ÖFþk#/:o \«Ï®Z½Ÿ÷Y燜fÚ¦ÂPs¨Ÿáì‹¥æ² ŒÊ®92Qæ PJñæ´Œx3Ï­fùÛjt3âåí~z£qVE:’Ëà© bæÀÒO¬b?ŸÏÚOñïëYq­æ×ª§ÏŠýúz²£kIGB€\ß5A³îˆÃº9E¸ • }0ÆÅórÐÂEËíª$àcüSêãÚ +ÂûêÝ9šzí0ûØÿËýûŸV×Yq‡«åÝš£øñǯèúø…³«öÇeŒe+ 0w¥õ ãâ ìÞ¿÷«ŸóIx½Ö!¯®þ®Ò®®®ü#’bI¹Ùg¿2ìŠgÒâ2EKPä÷–Ö7Ê‚À“×Ýe¢©M”²¬#fˆÑ¾¼Ý)ÅB¢¹ð_!ÉB•¥2+ïMÞr,Å?üŠð««‘}”vtTvt¼6üš~tûÑõ4çþÝbŒ‹Ùp½ÐJ²S“DqN§3éh:G;EuP¼è§¿â›ú4£ ,¯Ýd¦Õ)„èÑ op< hH' Ãel¢¨³ìÆÔü§ô¾â›‹ŽÁÓ7I«‰] ÈV7« f DT¿þHôÁåÕ=&>u—ú|ŸB cÏpÕÉfÑpC˜ò;E+íêkaí ©Av\ 5Ö^æý¯xËCx\ ¸Ê­¦ýŒçŸv [.stf_ÈÎ: ,3˜¸K Ú.bM„†|äÃâ#èJ JVOPjÕGh9HÔ⥃ÈLI[œ‘¶øêÉãtS( A«%èæ_œ[7È<Àà`<͸2`æŠË+WŠ_C)æBøµsg—ÄÖØCÂ×~î¹ÃÏþ§çž›ñ\G×P tc– ÷áÙ¸uiÛ¨ÕgdFÜQ3¹HaÇ—‚‘ôÃlë ¢} ð¦¥qoLË"™ÚÙÕñ»~‰;“[²PtçÎÈΡâÄbìÑ$•?åLŠâc4mcÍó— üae„å1 þ'Â/\|dþÂ…ó°ÿq—ë8l/ölÙõO3fü»ž×îÝ»ùðzâ ó{±¿=·d§Š7zìÅ5&¨\eŽ3P]Ui'`Ž €8rbËZá΄bÐgìŽÂ í ¯Ô¸X|e¹cº—Ý»~Âüí þ1ƒ_ÏÉË<:æåóó«bfEÅÂúú`@uOU±`¢>ªçW(æë)C豪kû5Cþ`u¡¿ä¸øû>Ü•’‡Å;º0ì,òa„°ûo&á(œ÷Ôû!zpîòá"áMP@wÉ2ä&f¹Ób4pça!ŒWt=!žŽs©sÊw“ëH8¤®zB}¼µ&ɉqöü´ô‰ÂÚÎÝ3®æÅÞTÇ$¥*:6°Öp2”³Ž£,=9Ed’PñgÂZ> nµ"]äQe}¿\‚ïupm¸ª·¿{‰ÌõÆõÝeq×JP°!xç£K®®@ø›Ú}XÌì{IÉÛæTÌ}ƒtü2ET{²Aoá"44â ëãyD¼Œ‚Jý„†ÖqõHÞ}Õo}Ƈ3ÞÜÀ;úN«àlŠ6 QYK~bÆÕ[»;bR"!XÇA°Ù¼t«ÔTIpÇÐ Ý(µ T(–àòà¬%»¯ÁÏx⹸*·CH#âb°<ØØ$¢ËîZ¼/fa.òàâ0>Ȱ‡KÌ´›‡¸í!ú´zÇÎ8‰¡æxthx¸»ç4×Ôs»g\“ûž‘ŒJfj„郭K[O#PJœ…CÌ_yp%ÅøºMõhM¼#NiSÂØ§®‚¡ùp…שe(ŽÕ±OMLˆlpè‰ktÛ3vs, „¢f—S©±Ö}OJéÓ»g\3‰¤½!(LpÁÍÓÑè„ûøQUË">î¼Ù™X»ÒàŒb8ÎR¶k'»Sb]†„ZHÁdP|”?ÙÙy­ÂÞÛNÝ!غS§NP©‹ìo”-xš>'%Ä÷`{ïrä'6ã‰kwÓ,á¬>f&Æ/h`€ºÛ¯‚ì1Úuí"ÞœWô~½-ÌBŒ cëQ?°ù 웩ö2V*ˆ ššA6 š[ ×Èm‚S_X‚; 1\ÚÛq ßœ 9–†´±">ur4±€ábQŸøà`#\ƒ¨Cx¡—3­Š‚û’kx¥@Ò‹CÁz°… ÷À캷²2‡Ÿ¸–ïN´ø9§ŠëfëÃ:¦ÔA\Ö *Xäì ‰`ƒÝ&ûÓˆ_C¯io9ZB(uh: ­mÍÐÀ~ãTgÑøµ|sÌTÇD¿*°uë¬øQ‘D1ª®ÁËE!Àäºè´ßÑØd2ñº¶Y¢§ƒ¸'ÿ‚Tïºïñºkõ‚óâ nªú![ÑÁ™…y¦ •Ô=vV¤4Ë'ȺÅÔÐîk{ÇiÑÉ23à"<:8jœ–œ½½ýJ®*/~ÖÄPVßLâ ‘œ­O€,‡Ъ3u<=M1)ufíd2#¦›Ó»õí}Æ›´UEt‰ÚɱpÀ§á.2ØæÒ‡IF½É¡^ñ—­Þæ8|ø¹Ã3ž›üI—½ž°ñOÏÍCªËÇ~ ìk þªU 3$:@\G‘މ)Þ¤SÞ}Å·)ïRUàw“%ºq°ÈKß$vߘ˜(…€Õþ‹®AÏÌÖàJgÒÕŒ¥R¼'‘Ë~EćèÝ %D?+fº7ü›¸$‰ÄM €al1ü+ ×¢š¬ +§;[àR çËis9º8Þ‚K Éûéê¿–tu-é \;ÅCç_ÍnSô©ØÃJ§³Œ'xóB A¦Û^@)»ž¹ðìGîK®ùzæËÁ(DÎïåÿž‰òî%w¢©œª†gO*À:!pEË|Ò «Ò@+]Ë]š„8o6g¹½hp?6-fÆø¯±©<%³¯Æ—ĉ½¢ˆÄÁÙ GgF –¦Ï·W©«Eqè¶Vòk¸´±´±±¿€DÄL$&º–ªw‰>’ì_ þµìÛ*+[׬©ªj/صaÅ7æE$  Ä‹!Vo| ˆ– ]œayHÈ̘2ó2ÆŠï=¸wû®‚‚ª–óëÙ66–F"¡ÙƒÉØ`,™Œ;W§$û·¤R§„êoä×pk%ÿè<~½øhaO±ªJ€ºMH#MˆOˆËŠ×ém©UÆ®«Vç—^'~ãWo7¿Çá>yåíÙqÿî\¿ÿ.yt»×ñ—á¯ØÛ]RRRÝè ºMD)¡¦’,¢±; ÁP“–ÇQçÀÅj³10š¬p!‘6v[½ÕœT›¿AXÒÆ±÷~ûû?¾óû“ïœZá\=ï¼sò䙓'ßùsñßøÇ%~S%ÝòªîUŽ?½jùèn)õøÜÍU{cu(öWÃ5Šœ–Ôó²g*¯d-ºÚÚįöÑV¢.ø‡6þ%mꋲÅNöþŠïÓ+å>ž/Ek¹%ÞÁbÒa/!æÈ[À+#@‚¡Ì@´ ô±ßpiIm€B¼ys;LDüC?ós›âº<¡Kõ0¿áM}°µ‰ÁØ0‰NUĘ˜70Eª«O­ã|ªƒ ûÁ$”fAlqú¶¿fá™4_‰ÙnC)>¥.¿ªþÂ}ºWÙ}Ío˜“hί7ýA-ˆ4‡SZêŸ'fñ˜£ô¾?vðØÁº¥{ÒÅ!qj»P‘ ª)b²‚IMGó‹¸è}¨2ñpHç-%%Ùùê]œÝ;šY‚èiͧÁ¶¨ ÒöZoéíÕŠÙVë\@«³p[`«&íjo¯0¦òªÝ’wðäŠb¯*ä›i?°MHý >2\_×c@Ä®BáÌT" j­í¯ò›§þë¾ÛØÛí®–®Nß~›²¹W~ìjÉcÆNœ@¯"Œ‘òl¬OÔ6Û#ªøP•víÚ°÷ÐÞç{zzŠÿ˾}[ôÓå/.Ÿ«øß:}U›KZWþå·1‡~²§gï¡Clno—L) Î ’AFi\Ätðá1ß1¸I•”"Ä>’a€‚&*TQœ@–g±¨)2PÚXYÙ¹ž…‚íçìZ±âÔÁñæØ%V»s5§ãj¨t÷àÁƒ`AÀ†íœL²]ãÝ-‘—©‹†+,؆íç™R¢`` ¿‘Ǽ¥Ã•æj]¯/û¹añUìëû2KŠÇ3iÞÜÌ– 9¨£Î¡y\ìÿôgô~ l»… Ô ZEÈR¯Eèñ LU:ç·Îâvvq ðÆVq±pvÍyqÿ­9ߪ. οG¤ü!˜T¾±Š °ècß>A¥ÔðJ/çøû\“¢ ™ÜÔ@•º E@!*a!2 ;hAÁ.ÕR‡7ćè̯vâ£:ï2`0úæÑ©šäÖARé¾ßFÖ(^r¡C‡¦Õ&4ó~Ûwž\"@YÙÏ‚¨·,€ØûUðäQ<µh¿³á]ÙAú 7¿4Lºõ³A†@'?Ñ´,:Yö¯´%ªÿó².Ed9AüÓƒƒEu§ËΛwlîò™G†ŌقfŒ¬ãb5€SRiƒrê„Ì™D””7ÐD~hSÅÌ•s-·ìè麢ÁÁx&š£ò1¥gõ¢9Ÿài¾ÅfpîÌ µƒÚFúèböþV®\>wù±cKÍcײ£GÙ{åW‘¼'¹ÄÔ±¯-gßľyé±¥s—/_¹²‚CÝ,Ü3ëázƒ¶RÖ¤lÃÖIÌ}‡ó6%E@—xœÞøÔãò«É7T?ñðãM .^\QÁïîÒ¥ìý=z´œÝM¿+&,þ+ H\\ý‹½ýºröüÌãp?Ë9ÚÏÊŠ=}¼bBä 2Y*ÇUïÏ<7QÓGæ×ãM³øÇÂÜ×ûâ šø÷Mðnû:|’»oBL†?Ðú@q-qÑà ‰.[êUêÛóŽHuÕ$ ,šéÕ¥-ÎQÍÞ2Ÿ7G·žxüñ‡gmÚ³i“¾ÉÅk¾þ§=›f±_?þðáz1W Ÿe%ÁT¸¤0b$AHÉk#¯ pÕç¼úúÌéoTZ©[Ä>…‹P˜Ý‘ZÖ'³öQAÞVá,†w L»ûNU?ZçÆÞÖâII±;ëÓw \âÖûäïìKù7€s+;Ñ@aë ô}|'P¤n J]._ŠàA«†üÔy¡¹›D;̳’ÚNÁ›š`tÄYòñʤ¶aÄš³@9Y™q‚p1Ñ”žÃ@Ý΀³F!™"æA±ÊnR+á¶ p\ ÉàÁ&qðù(êÈ9ù¶—:Ì·nÑ$Ù”¸¸ .í­Ûç“oÜFYwUz8¶<îŒ!>1˜RÛÅ~TŠÁˆhsØw¬ªßA”vJß>!.¶›]°0ppA ‚±ÙIÀº ‚8Ûô«ï„Uº™I`i@ö¨y.-Šñ3¸dÖTâDeÅÄ%œDI³‹2!· ]‚ç8@•-Lw`;‚kIÔ£4°å¯LÜÊ'Ôâéø•³Á3äÌ¢»ÜöxZ b_8 s€ÛAK?0¥¤ n ¨1äS‡òÒ‡^pSéÝ![÷ À¢øÚFCbqDh vœIÅB…0…IµÏ‚Á`+áŠÞÈXQÝc!Ôð…ØCȺ¾Ëÿ¢M=¡²™ ŽónýÒðÀ}+Ÿâõ5Š {|´ºî»”îÎȘO­VLôŠæ“åNBt@7‰ï¦ù.²›µSð©R@"Dà™õ© tïvXòa•ZƒÑI»ÛŒä„8FÚadýØÿ>z(ïV"]6L 51f'ö­[…[n¾ì…°Ñ3ßvÍ® ¨5”9<ö>~tÃÀÐKŠ`†%iŒ81 Îi, /šsØÕý,NáóøøŒû4ë>»øãCy_ñþE0)€Ið. ylŸøS´uSœصgåËwœºCiCi€x€¸²i6N^°¢aÆ5ñš?Ú”¡N.íëéVÿ zgÄq”ú°Óhrp|Ηó¾úÐGî¡;±«ym¢²8¬aº ÜTgh(ˆÃùkç®ä ËçHð¦A¿£è0õ©ƒ¬êÄÒW:AñºïzŒ”oa‘ž[Kf0ä·úC(òØìÏs¾÷­/DhÁ7÷Ž^Ê¥84+ºÔ£Ø)w–P—[×Btº@GˆlNµÊ5Ü*¡4ˆ ™-«p[ž(ðpV™ ?u—ËlOÝÝK“‹acœWºdé@Méœ[ó>ùÂ\<ÄD0xذ†¸n¾Æ¸ ĸè+PÎçòÃÛÀ ÎÂy1ÓÝÄ}g| \V*¥x„Úá4áL|\ß§d(Ž/Ñ{ ~€«¢'êïšó•¼O¾2â2á$J­iDžbüMŠ6 Ë ¬$‚GFPÐ:¡®>ü>ü˜ \è¢Ü0„ß‚M1àì°ØgÁÉ.êÒ Q›À¿» ®nX§¡0ÒE+~¤æ8ˆ6¦ä‚s ÊÏLK.^0%Aü&ê6ã³0…£>2ujËøÀqk§Pçž{Šk±nMÈò6b88êP|¸g"BI¢7·»ïg«AÙ*ÂÆ¢Ì…|’÷É'_žƒ¶ïÜ ñ©±ƒKV΃MQY¤&ÔAŸPq¤¢¦>H \.=ÄÝvö·-+²’Aö:Ã~šÜ–Rß)égm»Æ&îDSì á­ä@qoÊ‘ÈW¸@¸Í²#Cf´NˆÃZè+ˆ5!‡¥-š­za¥€/ š)fÁ âi~ñ;~ ñ)un»¡¾£ÁÉYhT)ø‘Ø-kDP¡*€jc1J<ýÎW¹@¾e_“õÝ‘"\ž@¤q † ¨Ðk-…pçö!. S7¢Ô‘úÁ­U˜ôä RgÁI=P{`˜Ê‡k¤0¦…yMV5˽ƒÝ j•PçÜú È'·Î!>°vzR°0N0S4(óA‹,H,ˆÞ‡„#.±}3„âéPŠß‘«Á4À{‚aY@Ô 87çõaÝV qX[àÆ-#‹n¯Ã™íƒ1Ž9}U D¨ukô¥T ûÔnÔ“ñ1è,•$>~Ë.iÜ»5þšøèõ)^Ã4lBG‘"!èÁ9%:››'„B4j˜Å%¦Ô˜¹t)æE¬×§Ø2úxùCáŽZãw–zðú¨Ž»Õ()CýÐ Õ$ñ¸1 :…ÔÒbéP¾yŠÏ2q¹-Ú,¬8B¤«,6ÃŽ¦2ÈCQßY€ÏiýGßù–È·x #{”ä£ Æ aM¤r6‚ JĶìðž*qëJ ñ݈s6ºËàAÖb|c{“N‚ëp$‹éF\ƆùYê66ú°ù91ˆbtîA”@>ù*ùn9¥Zäv²÷¹€¬+ ¨¨âÀZJFvœE?@]RqjÑÔ}ç-Bœ¾4jÞàöuªµ”:_‡z065³´ƒ´(ß–Ú@ É<$¢±´„ÁÒáFËÝwƒ‘Ì (¶¢Ù Ð}âu4d‡ìQœê>]P­pp¡(†sÐdÃ2‰Ã6ïÚ_“ºý~T¿µÛ(Êp†EA>+šº #_lÎC_…a‘–‹2‹ÙˆƒUE#;‘AN}gü‘P˜À¯v€¹¨c)(¦ÒDå-„Q¨}ŠS„ÕP@ (¾Vç pŠÂ—ìÕ}´Œë; P?æ#*åaòÉæÐÀÀ“it†ÍN€–däÛ©Ý!¥Hgˆæ3(üÉÔ¶vñê¤7Cî’¸ éDœ¹Œ¬ë¢-9½ˆDÍ¥Y÷‘=³YFM5=ʱݗˆ‹Å†zôÎà*I€‰öH¨;EP͘"x (¢IÆÖLX‚HÈÇ)¤Á[­®-Çž I§NGs”¸q6…J3Tu]•»­<€@¾uëÜMòqÛÏÑR‡Vlj"Ðz‚e¸ ašÚƆø‘‚<) s•aM|g9†`êYPç€54 ¬uÔ@u´«uÒå;p&Ï;$¥AU4ú5‡y0?Bç@ˆ@?ð£ÐÐÍ2 ‡TÌÉÔöá–3ŽE)^c LA¬©ÈÌÜ §áÁòˆÔ4ŠÍ&Íâ ζ¼%ŸŒ Â1ĉ–'Õþ< O¾úÐÌÕm Bݲ%ÅOŒbœx<œD@Ò[;Æõ¡©C"ø Š0ꉇà­õUÌŒÁq!s —:Ç¥cs³ºÀWQç P$¿>šó…o}’C Ìl}4û š­ª6ÓX”Úr;¦wÍÐØuÀÿ‘,(^¥%4¸–NqpJ€ªØ_&Tø¸ a1ŠWL܆ÅÈ>NÀÉœ¶=ªhŠ&â(uÉg( †N樇 ˜K¢!w)šQ£p£<ÇPEØ”ðFø—ΙóЭßúdR0‘ÜJæ0™àZ'E):2öé0Ž‚"ÌÚcS© f˜  àYnÌóî@;ë]5dH ë,˜ì&[ÂÔŽ°Í-–¡µ l•JÔ¢zEÄwgKõ.õ2$÷ôsæÌù²+Žla†ë+_xˆùGüû¿.÷Á¿Zyðß‚Ÿ¯ü´þ¥¿Àü;xG¹R®¯†·’å=ÛEŸÌqËî“w`ïÞ†üàÏvÎÿË·~%ËÃÏ&.“¯~åÖ/|áËÿÿum®/ÜzëW¾šýÉÿ xG˜­äýòIEND®B`‚motor-3.4.0/doc/api-asyncio/000077500000000000000000000000001460060525600156435ustar00rootroot00000000000000motor-3.4.0/doc/api-asyncio/aiohttp.rst000066400000000000000000000003641460060525600200500ustar00rootroot00000000000000:mod:`motor.aiohttp` - Integrate Motor with the aiohttp web framework ===================================================================== .. currentmodule:: motor.aiohttp .. automodule:: motor.aiohttp :members: :no-inherited-members: motor-3.4.0/doc/api-asyncio/asyncio_gridfs.rst000066400000000000000000000007161460060525600214040ustar00rootroot00000000000000asyncio GridFS Classes ====================== .. currentmodule:: motor.motor_asyncio Store blobs of data in `GridFS `_. .. seealso:: :ref:`Differences between PyMongo's and Motor's GridFS APIs `. .. autoclass:: AsyncIOMotorGridFSBucket :members: .. autoclass:: AsyncIOMotorGridIn :members: .. autoclass:: AsyncIOMotorGridOut :members: .. autoclass:: AsyncIOMotorGridOutCursor :members: motor-3.4.0/doc/api-asyncio/asyncio_motor_change_stream.rst000066400000000000000000000003131460060525600241370ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorChangeStream` ====================================================== .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorChangeStream :members: motor-3.4.0/doc/api-asyncio/asyncio_motor_client.rst000066400000000000000000000007001460060525600226150ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorClient` -- Connection to MongoDB ========================================================================= .. autoclass:: motor.motor_asyncio.AsyncIOMotorClient :members: .. describe:: client[db_name] || client.db_name Get the `db_name` :class:`AsyncIOMotorDatabase` on :class:`AsyncIOMotorClient` `client`. Raises :class:`~pymongo.errors.InvalidName` if an invalid database name is used. motor-3.4.0/doc/api-asyncio/asyncio_motor_client_encryption.rst000066400000000000000000000003271460060525600250740ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption` ========================================================== .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorClientEncryption :members: motor-3.4.0/doc/api-asyncio/asyncio_motor_client_session.rst000066400000000000000000000003561460060525600243670ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorClientSession` -- Sequence of operations ================================================================================= .. autoclass:: motor.motor_asyncio.AsyncIOMotorClientSession :members: motor-3.4.0/doc/api-asyncio/asyncio_motor_collection.rst000066400000000000000000000010141460060525600234710ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorCollection` ==================================================== .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorCollection :members: .. describe:: c[name] || c.name Get the `name` sub-collection of :class:`AsyncIOMotorCollection` `c`. Raises :class:`~pymongo.errors.InvalidName` if an invalid collection name is used. .. attribute:: database The :class:`AsyncIOMotorDatabase` that this :class:`AsyncIOMotorCollection` is a part of. motor-3.4.0/doc/api-asyncio/asyncio_motor_database.rst000066400000000000000000000006771460060525600231200ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorDatabase` ================================================== .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorDatabase :members: .. describe:: db[collection_name] || db.collection_name Get the `collection_name` :class:`AsyncIOMotorCollection` of :class:`AsyncIOMotorDatabase` `db`. Raises :class:`~pymongo.errors.InvalidName` if an invalid collection name is used. motor-3.4.0/doc/api-asyncio/cursors.rst000066400000000000000000000006641460060525600201030ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorCursor` ================================================ .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorCursor :members: :inherited-members: :class:`~motor.motor_asyncio.AsyncIOMotorCommandCursor` ======================================================= .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorCommandCursor :members: :inherited-members: motor-3.4.0/doc/api-asyncio/index.rst000066400000000000000000000006461460060525600175120ustar00rootroot00000000000000Motor asyncio API ================= .. toctree:: asyncio_motor_client asyncio_motor_client_session asyncio_motor_database asyncio_motor_collection asyncio_motor_change_stream asyncio_motor_client_encryption cursors asyncio_gridfs aiohttp .. seealso:: :doc:`../tutorial-asyncio` This page describes using Motor with asyncio. For Tornado integration, see :doc:`../api-tornado/index`. motor-3.4.0/doc/api-tornado/000077500000000000000000000000001460060525600156445ustar00rootroot00000000000000motor-3.4.0/doc/api-tornado/cursors.rst000066400000000000000000000006121460060525600200750ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorCursor` ========================================= .. currentmodule:: motor.motor_tornado .. autoclass:: MotorCursor :members: :inherited-members: :class:`~motor.motor_tornado.MotorCommandCursor` ================================================ .. currentmodule:: motor.motor_tornado .. autoclass:: MotorCommandCursor :members: :inherited-members: motor-3.4.0/doc/api-tornado/gridfs.rst000066400000000000000000000007071460060525600176600ustar00rootroot00000000000000Motor GridFS Classes ==================== .. currentmodule:: motor.motor_tornado Store blobs of data in `GridFS `_. .. seealso:: :ref:`Differences between PyMongo's and Motor's GridFS APIs `. .. seealso:: :doc:`web` .. autoclass:: MotorGridFSBucket :members: .. autoclass:: MotorGridIn :members: .. autoclass:: MotorGridOut :members: .. autoclass:: MotorGridOutCursor :members: motor-3.4.0/doc/api-tornado/index.rst000066400000000000000000000005521460060525600175070ustar00rootroot00000000000000Motor Tornado API ================= .. toctree:: motor_client motor_client_session motor_database motor_collection motor_change_stream motor_client_encryption cursors gridfs web .. seealso:: :doc:`../tutorial-tornado` This page describes using Motor with Tornado. For asyncio integration, see :doc:`../api-asyncio/index`. motor-3.4.0/doc/api-tornado/motor_change_stream.rst000066400000000000000000000002661460060525600224220ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorChangeStream` =============================================== .. currentmodule:: motor.motor_tornado .. autoclass:: MotorChangeStream :members: motor-3.4.0/doc/api-tornado/motor_client.rst000066400000000000000000000006611460060525600210770ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorClient` -- Connection to MongoDB ================================================================== .. currentmodule:: motor.motor_tornado .. autoclass:: MotorClient :members: .. describe:: client[db_name] || client.db_name Get the `db_name` :class:`MotorDatabase` on :class:`MotorClient` `client`. Raises :class:`~pymongo.errors.InvalidName` if an invalid database name is used. motor-3.4.0/doc/api-tornado/motor_client_encryption.rst000066400000000000000000000003021460060525600233410ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorClientEncryption` =================================================== .. currentmodule:: motor.motor_tornado .. autoclass:: MotorClientEncryption :members: motor-3.4.0/doc/api-tornado/motor_client_session.rst000066400000000000000000000004011460060525600226320ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorClientSession` -- Sequence of operations ========================================================================== .. currentmodule:: motor.motor_tornado .. autoclass:: motor.motor_tornado.MotorClientSession :members: motor-3.4.0/doc/api-tornado/motor_collection.rst000066400000000000000000000007421460060525600217540ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorCollection` ============================================= .. currentmodule:: motor.motor_tornado .. autoclass:: MotorCollection :members: .. describe:: c[name] || c.name Get the `name` sub-collection of :class:`MotorCollection` `c`. Raises :class:`~pymongo.errors.InvalidName` if an invalid collection name is used. .. attribute:: database The :class:`MotorDatabase` that this :class:`MotorCollection` is a part of. motor-3.4.0/doc/api-tornado/motor_database.rst000066400000000000000000000006341460060525600213650ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorDatabase` =========================================== .. currentmodule:: motor.motor_tornado .. autoclass:: MotorDatabase :members: .. describe:: db[collection_name] || db.collection_name Get the `collection_name` :class:`MotorCollection` of :class:`MotorDatabase` `db`. Raises :class:`~pymongo.errors.InvalidName` if an invalid collection name is used. motor-3.4.0/doc/api-tornado/web.rst000066400000000000000000000003441460060525600171540ustar00rootroot00000000000000:mod:`motor.web` - Integrate Motor with the Tornado web framework ================================================================= .. currentmodule:: motor.web .. automodule:: motor.web :members: :no-inherited-members: motor-3.4.0/doc/changelog.rst000066400000000000000000001637041460060525600161230ustar00rootroot00000000000000Changelog ========= .. currentmodule:: motor.motor_tornado 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:`~gridfs.GridFSBucket` and :class:`~gridfs.GridFS`. See :ref:`removed-gridfs-checksum` for details. - PyMongoCrypt 1.2.0 or later is now required for client side field level encryption support. Notable improvements ~~~~~~~~~~~~~~~~~~~~ - Enhanced connection pooling to create connections more efficiently and avoid connection storms. - Added the ``maxConnecting`` URI and :class:`~motor.motor_tornado.MotorClient` keyword argument. - :class:`~motor.motor_tornado.MotorClient` now accepts a URI and keyword argument ``srvMaxHosts`` that limits the number of mongos-like hosts a client will connect to. More specifically, when a mongodb+srv:// connection string resolves to more than ``srvMaxHosts`` number of hosts, the client will randomly choose a ``srvMaxHosts`` sized subset of hosts. - Added :attr:`motor.motor_tornado.MotorClient.options` for read-only access to a client's configuration options. - Added support for the ``comment`` parameter to all helpers. For example see :meth:`~motor.motor_tornado.MotorCollection.insert_one`. - Added support for the ``let`` parameter to :meth:`~motor.motor_tornado.MotorCollection.update_one`, :meth:`~motor.motor_tornado.MotorCollection.update_many`, :meth:`~motor.motor_tornado.MotorCollection.delete_one`, :meth:`~motor.motor_tornado.MotorCollection.delete_many`, :meth:`~motor.motor_tornado.MotorCollection.replace_one`, :meth:`~motor.motor_tornado.MotorCollection.aggregate`, :meth:`~motor.motor_tornado.MotorCollection.find_one_and_delete`, :meth:`~motor.motor_tornado.MotorCollection.find_one_and_replace`, :meth:`~motor.motor_tornado.MotorCollection.find_one_and_update`, :meth:`~motor.motor_tornado.MotorCollection.find`, :meth:`~motor.motor_tornado.MotorCollection.find_one`, and :meth:`~motor.motor_tornado.MotorCollection.bulk_write`. ``let`` is a map of parameter names and values. Parameters can then be accessed as variables in an aggregate expression context. - :meth:`~motor.motor_tornado.MotorCollection.aggregate` now supports $merge and $out executing on secondaries on MongoDB >=5.0. aggregate() now always obeys the collection's :attr:`read_preference` on MongoDB >= 5.0. - :meth:`gridfs.grid_file.GridOut.seek` now returns the new position in the file, to conform to the behavior of :meth:`io.IOBase.seek`. Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 3.0 release notes in JIRA`_ for the list of resolved issues in this release. .. _Motor 3.0 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=29710 Motor 2.5.1 ----------- Motor 2.5.1 fixes a bug where :meth:`MotorCursor.to_list` could return more than ``length`` documents. Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 2.5.1 release notes in JIRA`_ for the complete list of resolved issues in this release. .. _Motor 2.5.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=31791 Motor 2.5 --------- Motor 2.5 adds support for MongoDB 5.0. It depends on PyMongo 3.12 or later. New features: - Added support for MongoDB 5.0. - Support for MongoDB Stable API, see :class:`~pymongo.server_api.ServerApi`. - Support for snapshot reads on secondaries via the new ``snapshot`` option to :meth:`~motor.motor_asyncio.AsyncIOMotorClient.start_session`. - Support for Azure and GCP KMS providers for client side field level encryption. See the examples in :doc:`examples/encryption`. - Support AWS authentication with temporary credentials when connecting to KMS in client side field level encryption. - Support for connecting to load balanced MongoDB clusters via the new ``loadBalanced`` URI option. - Support for creating timeseries collections via the ``timeseries`` and ``expireAfterSeconds`` arguments to :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.create_collection`. - Added :attr:`motor.motor_asyncio.AsyncIOMotorClient.topology_description`. - Added hash support to :class:`motor.motor_asyncio.AsyncIOMotorClient`, :class:`motor.motor_asyncio.AsyncIOMotorDatabase`, and :class:`motor.motor_asyncio.AsyncIOMotorCollection` classes. - Added session and read concern support to :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_raw_batches` and :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.aggregate_raw_batches`. Deprecations: - Deprecated support for Python 3.5. - Deprecated :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.profiling_info`, :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.profiling_level`, and :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.set_profiling_level`. Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 2.5 release notes in JIRA`_ for the complete list of resolved issues in this release. .. _Motor 2.5 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=30895 Motor 2.4 --------- Motor 2.4 adds support for client-side field-level encryption and Python 3.9. New Features: - Added the :class:`motor.motor_asyncio.AsyncIOMotorClientEncryption` class, with the same interface as the corresponding PyMongo class. See :doc:`examples/encryption` for examples. - Added support for Python 3.9 Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 2.4 release notes in JIRA`_ for the complete list of resolved issues in this release. .. _Motor 2.4 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=29749 Motor 2.3.1 ----------- Motor 2.3.1 fixes two bugs related to change streams. Bug-fixes: - The :meth:`motor.motor_asyncio.AsyncIOMotorCollection.watch`, :meth:`motor.motor_asyncio.AsyncIOMotorDatabase.watch`, and :meth:`motor.motor_asyncio.AsyncIOMotorClient.watch` methods now properly support passing :class:`~motor.motor_asyncio.AsyncIOMotorClientSession` via the ``session`` argument. - Avoid exhausting Motor's worker thread pool when many change streams are being iterated simultaneously. Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 2.3.1 release notes in JIRA`_ for the complete list of resolved issues in this release. .. _Motor 2.3.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=30136 Motor 2.3 --------- Motor 2.3 adds support for contextvars. New features: - Added supported for the contextvars module. Specifically, it is now possible to access context variables inside :class:`~pymongo.monitoring.CommandListener` callbacks. Bug-fixes: - Fixed a bug that prohibited users from subclassing the :class:`motor.motor_asyncio.AsyncIOMotorClient`, :class:`motor.motor_asyncio.AsyncIOMotorDatabase`, and :class:`motor.motor_asyncio.AsyncIOMotorCollection` classes. - Updated the documentation to indicate full support for Windows. Previously, the documentation stated that Windows support was experimental. Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 2.3 release notes in JIRA`_ for the complete list of resolved issues in this release. .. _Motor 2.3 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=29836 Motor 2.2 --------- Motor 2.2 adds support for MongoDB 4.4 features. It depends on PyMongo 3.11 or later. Motor continues to support MongoDB 3.0 and later. Motor 2.2 also drops support for Python 2.7 and Python 3.4. New features: - Added the ``AsyncIOMotorCursor`` method :meth:`~motor.motor_asyncio.AsyncIOMotorCursor.next` that advances the cursor one document at a time, similar to to the ``AsyncIOMotorChangeStream`` method :meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream.next`. - Added index-hinting support to the :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.replace_one`, :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.update_one`, :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.update_many`, :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.delete_one`, :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.delete_many`, :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_one_and_replace`, :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_one_and_update`, and :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find_one_and_delete` methods. - Added support for the ``allow_disk_use`` parameter to :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find`. - Modified the :meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream` class' async context manager such that the change stream cursor is now created during the call to ``async with``. Previously, the cursor was only created when the application iterated the :meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream` object which could result in the application missing some changes. - Motor now advertises the framework used by the application to the MongoDB server as ``asyncio`` or ``Tornado``. Previously, no framework information was reported if the application used ``asyncio``. Bug-fixes: - Fixed a bug that caused calls to the :meth:`~motor.motor_asyncio.AsyncIOMotorGridOut.open()` method to raise :exc:`AttributeError`. - Fixed a bug that sometimes caused :meth:`~asyncio.Future.set_result` to be called on a cancelled :meth:`~asyncio.Future` when iterating a :meth:`~motor.motor_asyncio.AsyncIOMotorCommandCursor`. Deprecations: - Deprecated ``AsyncIOMotorCursor`` method :meth:`~motor.motor_asyncio.AsyncIOMotorCursor.next_object` and property :attr:`~motor.motor_asyncio.AsyncIOMotorCursor.fetch_next`. Applications should use ``async for`` to iterate over cursors instead. - Deprecated the :meth:`~motor.motor_asyncio.AsyncIOMotorClient.fsync` method. Applications should run the `fsync command `_ directly with :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command` instead. Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 2.2 release notes in JIRA`_ for the complete list of resolved issues in this release. .. _Motor 2.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=24884 Motor 2.1 --------- Motor 2.1 adds support for MongoDB 4.2 features. It depends on PyMongo 3.10 or later. Motor continues to support MongoDB 3.0 and later. Motor 2.1 also adds support for Python 3.8. Motor now offers experimental support for Windows when it is using the asyncio event loop. This means it supports Windows exclusively with Python 3, either integrating with asyncio directly or with Tornado 5 or later: starting in version 5, Tornado uses the asyncio event loop on Python 3 by default. Additional changes: - Support for MongoDB 4.2 sharded transactions. Sharded transactions have the same API as replica set transactions. - New method :meth:`~motor.motor_asyncio.AsyncIOMotorClientSession.with_transaction` to support conveniently running a transaction in a session with automatic retries and at-most-once semantics. - Added the ``max_commit_time_ms`` parameter to :meth:`~motor.motor_asyncio.AsyncIOMotorClientSession.start_transaction`. - The ``retryWrites`` URI option now defaults to ``True``. Supported write operations that fail with a retryable error will automatically be retried one time, with at-most-once semantics. - Support for retryable reads and the ``retryReads`` URI option which is enabled by default. See the :class:`~pymongo.mongo_client.MongoClient` documentation for details. Now that supported operations are retried automatically and transparently, users should consider adjusting any custom retry logic to prevent an application from inadvertently retrying for too long. - Support zstandard for wire protocol compression. - Support for periodically polling DNS SRV records to update the mongos proxy list without having to change client configuration. - New method :meth:`motor.motor_asyncio.AsyncIOMotorDatabase.aggregate` to support running database level aggregations. - Change stream enhancements for MongoDB 4.2: - Resume tokens can now be accessed from a ``AsyncIOMotorChangeStream`` cursor using the :attr:`~motor.motor_asyncio.AsyncIOMotorChangeStream.resume_token` attribute. - New ``AsyncIOMotorChangeStream`` method :meth:`~motor.motor_asyncio.AsyncIOMotorChangeStream.try_next` and attribute :attr:`~motor.motor_asyncio.AsyncIOMotorChangeStream.alive`. - New parameter ``start_after`` for change stream :meth:`motor.motor_asyncio.AsyncIOMotorCollection.watch`, :meth:`motor.motor_asyncio.AsyncIOMotorDatabase.watch`, and :meth:`motor.motor_asyncio.AsyncIOMotorClient.watch` methods. - New parameters ``bucket_name``, ``chunk_size_bytes``, ``write_concern``, and ``read_preference`` for :class:`motor.motor_asyncio.AsyncIOMotorGridFSBucket`. Issues Resolved ~~~~~~~~~~~~~~~ See the `Motor 2.1 release notes in JIRA`_ for the complete list of resolved issues in this release. .. _Motor 2.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=11182&version=20187 Motor 2.0 --------- Motor 2.0 drops support for MongoDB 2.6 and adds supports MongoDB 4.0 features, including multi-document transactions, and change stream notifications on entire databases or entire MongoDB servers. It adds support for Python 3.7. This version of Motor requires PyMongo 3.7 or later. This is a major release that removes previously deprecated APIs. To support multi-document transactions, Motor had to make breaking changes to the session API and release a major version bump. Since this is a major release it also deletes many helper methods and APIs that had been deprecated over the time since Motor 1.0, most notably the old CRUD methods ``insert``, ``update``, ``remove``, and ``save``, and the original callback-based API. Read the :doc:`migrate-to-motor-2` carefully to upgrade your existing Motor application. Documentation is updated to warn about obsolete TLS versions, see :doc:`configuration`. Motor is now tested on Travis in addition to MongoDB's `Evergreen `_ system. Added support for `aiohttp`_ 3.0 and later, and dropped older aiohttp versions. The aiohttp integration now requires Python 3.5+. The ``MotorDatabase.add_user`` and ``MotorDatabase.remove_user`` methods are deleted. Manage user accounts with four database commands: createUser_, usersInfo_, updateUser_, and dropUser_. You can run any database command with the :meth:`MotorDatabase.command` method. .. _createUser: https://mongodb.com/docs/manual/reference/command/createUser/ .. _usersInfo: https://mongodb.com/docs/manual/reference/command/usersInfo/ .. _updateUser: https://mongodb.com/docs/manual/reference/command/updateUser/ .. _dropUser: https://mongodb.com/docs/manual/reference/command/createUser/ The deprecated GridFS classes ``MotorGridFS`` and ``AsyncIOMotorGridFS`` are deleted in favor of :class:`~motor.motor_tornado.MotorGridFSBucket` and :class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket`, which conform to driver specs for GridFS. Additional changes: - New methods for retrieving batches of raw BSON: - :meth:`MotorCollection.find_raw_batches` - :meth:`MotorCollection.aggregate_raw_batches` - Motor adds its name, version, and Tornado's version (if appropriate) to the client data logged by the MongoDB server when Motor connects, in addition to the data added by PyMongo. - Calling :meth:`~MotorCommandCursor.batch_size` on a cursor returned from :meth:`~MotorCollection.aggregate` no longer raises ``AttributeError``. Motor 1.3.1 ----------- Fix a Python 3.7 compatibility bug caused by importing "async", which is a keyword in Python 3.7. Drop support for Python 3.4.3 and older. Motor 1.3.0 ----------- Deprecate Motor's old callback-based async API in preparation for removing it in Motor 2.0. Raise ``DeprecationWarning`` whenever a callback is passed. See the :doc:`migrate-to-motor-2`. Motor 1.2.5 ----------- Fix a Python 3.7 compatibility bug caused by importing "async", which is a keyword in Python 3.7. Drop support for Python 3.4.3 and older. Motor 1.2.4 ----------- Fix a Python 3.7 compatibility bug in the :class:`MotorChangeStream` class returned by :meth:`MotorCollection.watch`. It is now possible to use change streams in ``async for`` loops in Python 3.7. Motor 1.2.3 ----------- Compatibility with latest Sphinx and document how to use the latest TLS protocols. Motor 1.2.2 ----------- Motor 1.2.0 requires PyMongo 3.6 or later. The dependency was properly documented, but not enforced in ``setup.py``. PyMongo 3.6 is now an install-time requirement; thanks to Shane Harvey for the fix. Motor 1.2.1 ----------- An asyncio application that created a Change Stream with :meth:`MotorCollection.watch` and shut down while the Change Stream was open would print several errors. I have rewritten :meth:`MotorChangeStream.next` and some Motor internals to allow clean shutdown with asyncio. Motor 1.2 --------- Motor 1.2 drops support for MongoDB 2.4 and adds support for MongoDB 3.6 features. It depends on PyMongo 3.6 or later. Motor continues to support MongoDB 2.6 and later. Dropped support for Python 2.6 and 3.3. Motor continues to support Python 2.7, and 3.4+. Dropped support for Tornado 3. A recent version of Tornado 4 is required. Dropped support for the `Python 3.5.0 and Python 3.5.1 "async for" protocol `_. Motor allows "async for" with cursors in Python 3.5.2 and later. See the :ref:`Compatibility Matrix ` for the relationships among Motor, Python, Tornado, and MongoDB versions. Added support for `aiohttp`_ 2.0 and later, and dropped older aiohttp versions. Highlights include: - New method :meth:`MotorCollection.watch` to acquire a Change Stream on a collection. - New Session API to support causal consistency, see :meth:`MotorClient.start_session`. - Support for array_filters in :meth:`~MotorCollection.update_one`, :meth:`~MotorCollection.update_many`, :meth:`~MotorCollection.find_one_and_update`, :meth:`~MotorCollection.bulk_write`. - :meth:`MotorClient.list_databases` and :meth:`MotorClient.list_database_names`. - Support for mongodb+srv:// URIs. See :class:`~pymongo.mongo_client.MongoClient` for details. - Support for retryable writes and the ``retryWrites`` URI option. See :class:`~pymongo.mongo_client.MongoClient` for details. The maximum number of workers in the thread pool can be overridden with an environment variable, see :doc:`configuration`. :class:`MotorCollection` accepts codec_options, read_preference, write_concern, and read_concern arguments. This is rarely needed; you typically create a :class:`MotorCollection` from a :class:`MotorDatabase`, not by calling its constructor directly. Deleted obsolete class ``motor.Op``. Motor 1.1 --------- Motor depends on PyMongo 3.4 or later. It wraps the latest PyMongo code which support the new server features introduced in MongoDB 3.4. (It is a coincidence that the latest MongoDB and PyMongo versions are the same number.) Highlights include: - Complete support for MongoDB 3.4: - Unicode aware string comparison using collations. See :ref:`PyMongo's examples for collation `. - :class:`MotorCursor` and :class:`MotorGridOutCursor` have a new attribute :meth:`~MotorCursor.collation`. - 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. - :meth:`~MotorCollection.parallel_scan` supports maxTimeMS. - :class:`~pymongo.write_concern.WriteConcern` is automatically applied by all helpers for commands that write to the database when connected to MongoDB 3.4+. This change affects the following helpers: - :meth:`MotorClient.drop_database` - :meth:`MotorDatabase.create_collection` - :meth:`MotorDatabase.drop_collection` - :meth:`MotorCollection.aggregate` (when using $out) - :meth:`MotorCollection.create_indexes` - :meth:`MotorCollection.create_index` - :meth:`MotorCollection.drop_indexes` - :meth:`MotorCollection.drop_indexes` - :meth:`MotorCollection.drop_index` - :meth:`MotorCollection.map_reduce` (when output is not "inline") - :meth:`MotorCollection.reindex` - :meth:`MotorCollection.rename` - Improved support for logging server discovery and monitoring events. See :mod:`PyMongo's monitoring documentation ` for examples. - Support for matching iPAddress subjectAltName values for TLS certificate 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:`~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. ``{}``). .. warning:: Starting in PyMongo 3.4, :attr:`bson.code.Code.scope` may return ``None``, as the default scope is ``None`` instead of ``{}``. .. note:: PyMongo 3.4+ attempts to create sockets non-inheritable when possible (i.e. it sets the close-on-exec flag on socket file descriptors). Support is limited to a subset of POSIX operating systems (not including Windows) and the flag usually cannot be set in a single atomic operation. CPython 3.4+ implements `PEP 446`_, creating all file descriptors non-inheritable by default. Users that require this behavior are encouraged to upgrade to CPython 3.4+. .. _PEP 446: https://www.python.org/dev/peps/pep-0446/ 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/stable/changelog.html#changes-in-version-3-0 :class:`MotorReplicaSetClient` is removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In Motor 1.0, :class:`MotorClient` is the only class. Connect to a replica set with a "replicaSet" URI option or parameter:: MotorClient("mongodb://hostname/?replicaSet=my-rs") MotorClient(host, port, replicaSet="my-rs") New features ~~~~~~~~~~~~ New classes :class:`~motor.motor_tornado.MotorGridFSBucket` and :class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket` conform to the `GridFS API Spec `_ for MongoDB drivers. These classes supersede the old ``MotorGridFS`` and ``AsyncIOMotorGridFS``. See `GridFS`_ changes below, especially note the **breaking change** in :class:`~motor.motor_web.GridFSHandler`. Serve GridFS files over HTTP using `aiohttp`_ and :class:`~motor.aiohttp.AIOHTTPGridFS`. .. _aiohttp: https://aiohttp.readthedocs.io/ :class:`MotorClient` changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Removed: - :meth:`MotorClient.open`; clients have opened themselves automatically on demand since version 0.2. - :attr:`MotorClient.seeds`, use :func:`pymongo.uri_parser.parse_uri` on your MongoDB URI. - :attr:`MotorClient.alive` Added: - :attr:`MotorClient.event_listeners` - :attr:`MotorClient.max_idle_time_ms` - :attr:`MotorClient.min_pool_size` Unix domain socket paths must be quoted with :func:`urllib.parse.quote_plus` (or ``urllib.quote_plus`` in Python 2) before they are included in a URI: .. code-block:: python path = "/tmp/mongodb-27017.sock" MotorClient("mongodb://%s" % urllib.parse.quote_plus(path)) :class:`~motor.motor_tornado.MotorCollection` changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added: - :meth:`MotorCollection.create_indexes` - :meth:`MotorCollection.list_indexes` New ``bypass_document_validation`` parameter for :meth:`~.MotorCollection.initialize_ordered_bulk_op` and :meth:`~.MotorCollection.initialize_unordered_bulk_op`. Changes to :meth:`~motor.motor_tornado.MotorCollection.find` and :meth:`~motor.motor_tornado.MotorCollection.find_one` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following find/find_one options have been renamed: These renames only affect your code if you passed these as keyword arguments, like ``find(fields=['fieldname'])``. If you passed only positional parameters these changes are not significant for your application. - spec -> filter - fields -> projection - partial -> allow_partial_results The following find/find_one options have been added: - cursor_type (see :class:`~pymongo.cursor.CursorType` for values) - oplog_replay - modifiers The following find/find_one options have been removed: - network_timeout (use :meth:`~motor.motor_tornado.MotorCursor.max_time_ms` instead) - read_preference (use :meth:`~motor.motor_tornado.MotorCollection.with_options` instead) - tag_sets (use one of the read preference classes from :mod:`~pymongo.read_preferences` and :meth:`~motor.motor_tornado.MotorCollection.with_options` instead) - secondary_acceptable_latency_ms (use the ``localThresholdMS`` URI option instead) - max_scan (use the new ``modifiers`` option instead) - snapshot (use the new ``modifiers`` option instead) - tailable (use the new ``cursor_type`` option instead) - 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:`~bson.codec_options.CodecOptions` instead) - compile_re (BSON regular expressions are always decoded to :class:`~bson.regex.Regex`) The following find/find_one options are deprecated: - manipulate The following renames need special handling. - timeout -> no_cursor_timeout - By default, MongoDB closes a cursor after 10 minutes of inactivity. In previous Motor versions, you disabled the timeout by passing ``timeout=False`` to :meth:`.MotorCollection.find` or :meth:`.MotorGridFS.find`. The ``timeout`` parameter has been renamed to ``no_cursor_timeout``, it defaults to ``False``, and you must now pass ``no_cursor_timeout=True`` to disable timeouts. :class:`~motor.motor_tornado.MotorCursor` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added: - :attr:`.MotorCursor.address` - :meth:`.MotorCursor.max_await_time_ms` Removed: - :attr:`.MotorCursor.conn_id`, use :attr:`~.MotorCursor.address` GridFS ~~~~~~ The old GridFS classes ``MotorGridFS`` and ``AsyncIOMotorGridFS`` are deprecated in favor of :class:`~motor.motor_tornado.MotorGridFSBucket` and :class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket`, which comply with MongoDB's cross-language driver spec for GridFS. The old classes are still supported, but will be removed in Motor 2.0. **BREAKING CHANGE**: The overridable method :class:`~motor.web.GridFSHandler.get_gridfs_file` of :class:`~motor.web.GridFSHandler` now takes a :class:`~motor.motor_tornado.MotorGridFSBucket`, not a :class:`~motor.motor_tornado.MotorGridFS`. It also takes an additional ``request`` parameter. :class:`~motor.motor_tornado.MotorGridOutCursor` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Added: - :attr:`.MotorGridOutCursor.address` - :meth:`.MotorGridOutCursor.max_await_time_ms` Removed: - :attr:`.MotorGridOutCursor.conn_id`, use :attr:`~.MotorGridOutCursor.address` :class:`~motor.motor_tornado.MotorGridIn` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ New method :meth:`.MotorGridIn.abort`. In a Python 3.5 native coroutine, the "async with" statement calls :meth:`~MotorGridIn.close` automatically:: async def upload(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) async with await fs.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. :class:`~motor.motor_tornado.MotorGridOut` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :class:`~motor.motor_tornado.MotorGridOut` is now an async iterable, so reading a chunk at a time is much simpler with a Python 3 native coroutine:: async def read_file(file_id): fs = motor.motor_tornado.MotorGridFS(db) gridout = await fs.get(file_id) async for chunk in gridout: sys.stdout.write(chunk) sys.stdout.flush() Documentation ~~~~~~~~~~~~~ The :doc:`/api-asyncio/index` is now fully documented, side by side with the :doc:`/api-tornado/index`. New :doc:`developer-guide` added. Motor 0.7 --------- For asynchronous I/O Motor now uses a thread pool, which is faster and simpler than the prior implementation with greenlets. It no longer requires the ``greenlet`` package, and now requires the ``futures`` backport package on Python 2. This version updates the PyMongo dependency from 2.8.0 to 2.9.x, and wraps PyMongo 2.9's new APIs. Most of Motor 1.0's API is now implemented, and APIs that will be removed in Motor 1.0 are now deprecated and raise warnings. :class:`MotorClient` changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :class:`~MotorClient.get_database` method is added for getting a :class:`MotorDatabase` instance with its options configured differently than the MotorClient's. New read-only attributes: - :attr:`~MotorClient.codec_options` - :attr:`~MotorClient.local_threshold_ms` - :attr:`~MotorClient.max_write_batch_size` :class:`MotorReplicaSetClient` changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :meth:`~MotorReplicaSetClient.get_database` method is added for getting a :class:`MotorDatabase` instance with its options configured differently than the MotorReplicaSetClient's. New read-only attributes: - :attr:`~MotorReplicaSetClient.codec_options` - :attr:`~MotorReplicaSetClient.local_threshold_ms` :class:`MotorDatabase` changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :meth:`~MotorDatabase.get_collection` method is added for getting a :class:`MotorCollection` instance with its options configured differently than the MotorDatabase's. The ``connection`` property is deprecated in favor of a new read-only attribute :attr:`~MotorDatabase.client`. New read-only attribute: - :attr:`~MotorDatabase.codec_options` :class:`MotorCollection` changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :meth:`~MotorCollection.with_options` method is added for getting a :class:`MotorCollection` instance with its options configured differently than this MotorCollection's. New read-only attribute: - :attr:`~MotorCollection.codec_options` The following methods wrap PyMongo's implementation of the standard `CRUD API Spec`_ for MongoDB Drivers: - :meth:`~MotorCollection.bulk_write` - :meth:`~MotorCollection.insert_one` - :meth:`~MotorCollection.insert_many` - :meth:`~MotorCollection.update_one` - :meth:`~MotorCollection.update_many` - :meth:`~MotorCollection.replace_one` - :meth:`~MotorCollection.delete_one` - :meth:`~MotorCollection.delete_many` - :meth:`~MotorCollection.find_one_and_delete` - :meth:`~MotorCollection.find_one_and_replace` - :meth:`~MotorCollection.find_one_and_update` These new methods do not apply SON Manipulators. .. _CRUD API Spec: https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst :doc:`GridFS ` changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ New :class:`MotorGridOutCursor` methods: - :meth:`~MotorGridOutCursor.add_option` - :meth:`~MotorGridOutCursor.remove_option` - :meth:`~MotorGridOutCursor.clone` Added :class:`MotorGridOut` documentation: - :attr:`~MotorGridOut.aliases` - :attr:`~MotorGridOut.chunk_size` - :meth:`~MotorGridOut.close` - :attr:`~MotorGridOut.content_type` - :attr:`~MotorGridOut.filename` - :attr:`~MotorGridOut.length` - :attr:`~MotorGridOut.md5` - :attr:`~MotorGridOut.metadata` - :attr:`~MotorGridOut.name` - :attr:`~MotorGridOut.upload_date` Bugfix ~~~~~~ `MOTOR-124 `_: an import deadlock in Python 2 and Tornado 3 led to an :exc:`~pymongo.errors.AutoReconnect` exception with some replica sets. Motor 0.6.2 ----------- Fix "from motor import \*" for Python 3. Motor 0.6.1 ----------- Fix source distribution, which hadn't included the "frameworks" submodules. 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These modules have been moved from: - ``motor_asyncio.py`` - ``motor_tornado.py`` To: - ``motor_asyncio/__init__.py`` - ``motor_tornado/__init__.py`` 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. Database and collection names with leading underscores ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A database or collection whose name starts with an underscore can no longer be accessed as a property:: # Now raises AttributeError. db = MotorClient()._mydatabase collection = db._mycollection subcollection = collection._subcollection Such databases and collections can still be accessed dict-style:: # Continues to work the same as previous Motor versions. db = MotorClient()['_mydatabase'] collection = db['_mycollection'] To ensure a "sub-collection" with a name that includes an underscore is 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'] These changes solve problems with iPython code completion and the Python 3 :class:`ABC` abstract base class. Motor 0.5 --------- asyncio ~~~~~~~ Motor can now integrate with asyncio, as an alternative to Tornado. My gratitude to Rémi Jolin, Andrew Svetlov, and Nikolay Novik for their huge contributions to Motor's asyncio integration. Python 3.5 ~~~~~~~~~~ Motor is now compatible with Python 3.5, which required some effort. Motor not only supports users' coroutines, it uses coroutines to implement some of its own features, like :meth:`~MotorClient.open` and :meth:`~MotorGridFS.put`. There is no single way to return a value from a Python 3.5 native coroutine or a Python 2 generator-based coroutine, so Motor internal coroutines that return values were rewritten. (See `commit message dc19418c`_ for an explanation.) .. _commit message dc19418c: https://github.com/mongodb/motor/commit/dc19418c ``async`` and ``await`` ~~~~~~~~~~~~~~~~~~~~~~~ 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``:: async def f(): async for doc in collection.find(): do_something_with(doc) .. _aggregate_changes_0_5: :meth:`~MotorCollection.aggregate` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :meth:`MotorCollection.aggregate` now returns a cursor by default, and the cursor is returned immediately without a ``yield``. The old syntax is no longer supported:: # Motor 0.4 and older, no longer supported. cursor = yield collection.aggregate(pipeline, cursor={}) while (yield cursor.fetch_next): doc = cursor.next_object() print(doc) In Motor 0.5, simply do:: # Motor 0.5: no "cursor={}", no "yield". cursor = collection.aggregate(pipeline) while (yield cursor.fetch_next): doc = cursor.next_object() print(doc) Or with Python 3.5 and later:: # Motor 0.5, Python 3.5. async for doc in collection.aggregate(pipeline): print(doc) MongoDB versions 2.4 and older do not support aggregation cursors. For compatibility with older MongoDBs, :meth:`~MotorCollection.aggregate` now takes an argument ``cursor=False``, and returns a Future that you can yield to get all the results in one document:: # Motor 0.5 with MongoDB 2.4 and older. reply = yield collection.aggregate(cursor=False) for doc in reply['results']: print(doc) Deprecations ~~~~~~~~~~~~ Motor 0.5 deprecates a large number of APIs that will be removed in version 1.0: ``MotorClient``: - `~MotorClient.host` - `~MotorClient.port` - `~MotorClient.document_class` - `~MotorClient.tz_aware` - `~MotorClient.secondary_acceptable_latency_ms` - `~MotorClient.tag_sets` - `~MotorClient.uuid_subtype` - `~MotorClient.disconnect` - `~MotorClient.alive` ``MotorReplicaSetClient``: - `~MotorReplicaSetClient.document_class` - `~MotorReplicaSetClient.tz_aware` - `~MotorReplicaSetClient.secondary_acceptable_latency_ms` - `~MotorReplicaSetClient.tag_sets` - `~MotorReplicaSetClient.uuid_subtype` - `~MotorReplicaSetClient.alive` ``MotorDatabase``: - `~MotorDatabase.secondary_acceptable_latency_ms` - `~MotorDatabase.tag_sets` - `~MotorDatabase.uuid_subtype` ``MotorCollection``: - `~MotorCollection.secondary_acceptable_latency_ms` - `~MotorCollection.tag_sets` - `~MotorCollection.uuid_subtype` Cursor slicing ~~~~~~~~~~~~~~ Cursors can no longer be indexed like ``cursor[n]`` or sliced like ``cursor[start:end]``, see `MOTOR-84 `_. If you wrote code like this:: cursor = collection.find()[i] yield cursor.fetch_next doc = cursor.next_object() Then instead, write:: cursor = collection.find().skip(i).limit(-1) yield cursor.fetch_next doc = cursor.next_object() The negative limit ensures the server closes the cursor after one result, saving Motor the work of closing it. See `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' doesn't match 'Y'". The message is now empty and Tornado logs a warning instead. Configuring uuid_subtype ~~~~~~~~~~~~~~~~~~~~~~~~ You can now get and set :attr:`~MotorClient.uuid_subtype` on :class:`MotorClient`, :class:`MotorReplicaSetClient`, and :class:`MotorDatabase` instances, not just on :class:`MotorCollection`. Motor 0.4.1 ----------- Fix `MOTOR-66 `_, deadlock when initiating :class:`MotorReplicaSetClient` connection from multiple operations at once. Motor 0.4 --------- Supports MongoDB 3.0. In particular, supports MongoDB 3.0's new SCRAM-SHA-1 authentication mechanism and updates the implementations of :meth:`MotorClient.database_names` and :meth:`MotorDatabase.collection_names`. Updates PyMongo dependency from 2.7.1 to 2.8, therefore inheriting `PyMongo 2.7.2's bug fixes `_ and `PyMongo 2.8's bug fixes `_ and `features `_. Fixes `a connection-pool timeout when waitQueueMultipleMS is set `_ and `two bugs in replica set monitoring `_. The ``copy_database`` method has been removed. It was overly complex and no one used it, see `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 ``copy_database`` will be removed in a future release too, use the mongo shell. .. seealso:: `The "copydb" command `_. Motor 0.3.3 ----------- Fix `MOTOR-45 `_, a stack-context leak in domain name resolution that could lead to an infinite loop and rapid memory leak. Document Motor's :doc:`requirements` in detail. Motor 0.3.2 ----------- Fix `MOTOR-44 `_, a socket leak in :class:`MotorClient.copy_database` and :class:`MotorReplicaSetClient.copy_database`. Motor 0.3.1 ----------- Fix `MOTOR-43 `_, a TypeError when using :class:`~motor.web.GridFSHandler` with a timezone-aware :class:`~motor.motor_tornado.MotorClient`. Fix GridFS examples that hadn't been updated for Motor 0.2's new syntax. Fix a unittest that hadn't been running. Motor 0.3 --------- No new features. * Updates PyMongo dependency from 2.7 to 2.7.1, therefore inheriting `PyMongo 2.7.1's bug fixes `_. * Motor continues to support Python 2.6, 2.7, 3.3, and 3.4, but now with single-source. 2to3 no longer runs during installation with Python 3. * ``nosetests`` is no longer required for regular Motor tests. * Fixes `a mistake in the docstring `_ for aggregate(). Motor 0.2.1 ----------- Fixes two bugs: * `MOTOR-32 `_: The documentation for :meth:`MotorCursor.close` claimed it immediately halted execution of :meth:`MotorCursor.each`, but it didn't. * `MOTOR-33 `_: An incompletely iterated cursor's ``__del__`` method sometimes got stuck and cost 100% CPU forever, even though the application was still responsive. Motor 0.2 --------- This version includes API changes that break backward compatibility with applications written for Motor 0.1. For most applications, the migration chores will be minor. In exchange, Motor 0.2 offers a cleaner style, and it wraps the new and improved PyMongo 2.7 instead of 2.5. Changes in Dependencies ~~~~~~~~~~~~~~~~~~~~~~~ Motor now requires PyMongo 2.7.0 exactly and Tornado 3 or later. It drops support for Python 2.5 since Tornado 3 has dropped it. Motor continues to work with Python 2.6 through 3.4. It still requires `Greenlet`_. API Changes ~~~~~~~~~~~ open_sync ''''''''' The ``open_sync`` method has been removed from :class:`MotorClient` and :class:`MotorReplicaSetClient`. Clients now connect to MongoDB automatically on first use. Simply delete the call to ``open_sync`` from your application. 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) try: loop.run_sync(client.open) except pymongo.errors.ConnectionFailure: print "Can't connect" Similarly, calling :meth:`MotorGridOut.open` is now optional. :class:`MotorGridIn` and :class:`MotorGridFS` no longer have an ``open`` method at all. .. _changelog-futures: Futures ''''''' Motor 0.2 takes advantage of Tornado's tidy new coroutine syntax:: # Old style: document = yield motor.Op(collection.find_one, {'_id': my_id}) # New style: document = yield collection.find_one({'_id': my_id}) To make this possible, Motor asynchronous methods (except :meth:`MotorCursor.each`) now return a :class:`~tornado.concurrent.Future`. Using Motor with callbacks is still possible: If a callback is passed, it will be executed with the ``(result, error)`` of the operation, same as in Motor 0.1:: def callback(document, error): if error: logging.error("Oh no!") else: print document collection.find_one({'_id': my_id}, callback=callback) If no callback is passed, a Future is returned that resolves to the method's result or error:: document = yield collection.find_one({'_id': my_id}) ``motor.Op`` works the same as before, but it's deprecated. ``WaitOp`` and ``WaitAllOps`` have been removed. Code that used them can now yield a ``Future`` or a list of them. Consider this function written for Tornado 2 and Motor 0.1:: @gen.engine def get_some_documents(): cursor = collection.find().sort('_id').limit(2) cursor.to_list(callback=(yield gen.Callback('key'))) do_something_while_we_wait() try: documents = yield motor.WaitOp('key') print documents except Exception, e: print e The function now becomes:: @gen.coroutine def f(): cursor = collection.find().sort('_id').limit(2) future = cursor.to_list(2) do_something_while_we_wait() try: documents = yield future print documents except Exception, e: print e Similarly, a function written like so in the old style:: @gen.engine def get_two_documents_in_parallel(collection): collection.find_one( {'_id': 1}, callback=(yield gen.Callback('one'))) collection.find_one( {'_id': 2}, callback=(yield gen.Callback('two'))) try: doc_one, doc_two = yield motor.WaitAllOps(['one', 'two']) print doc_one, doc_two except Exception, e: print e Now becomes:: @gen.coroutine def get_two_documents_in_parallel(collection): future_0 = collection.find_one({'_id': 1}) future_1 = collection.find_one({'_id': 2}) try: doc_one, doc_two = yield [future_0, future_1] print doc_one, doc_two except Exception, e: print e to_list ''''''' Any calls to :meth:`MotorCursor.to_list` that omitted the ``length`` argument must now include it:: result = yield collection.find().to_list(100) ``None`` is acceptable, meaning "unlimited." Use with caution. Connection Pooling '''''''''''''''''' :class:`MotorPool` has been rewritten. It supports the new options introduced in PyMongo 2.6, and drops all Motor-specific options. :class:`MotorClient` and :class:`MotorReplicaSetClient` have an option ``max_pool_size``. It used to mean "minimum idle sockets to keep open", but its meaning has changed to "maximum sockets open per host." Once this limit is reached, operations will pause waiting for a socket to become available. Therefore the default has been raised from 10 to 100. If you pass a value for ``max_pool_size`` make sure it's large enough for the expected load. (Sockets are only opened when needed, so there's no cost to having a ``max_pool_size`` larger than necessary. Err towards a larger value.) If you've been accepting the default, continue to do so. ``max_pool_size`` is now synonymous with Motor's special ``max_concurrent`` option, so ``max_concurrent`` has been removed. ``max_wait_time`` has been renamed ``waitQueueTimeoutMS`` for consistency with PyMongo. If you pass ``max_wait_time``, rename it and multiply by 1000. The :exc:`MotorPoolTimeout` exception is gone; catch PyMongo's :exc:`~pymongo.errors.ConnectionFailure` instead. DNS ''' Motor can take advantage of Tornado 3's `asynchronous resolver interface`_. By default, Motor still uses blocking DNS, but you can enable non-blocking lookup with a threaded resolver:: Resolver.configure('tornado.netutil.ThreadedResolver') Or install `pycares`_ and use the c-ares resolver:: Resolver.configure('tornado.platform.caresresolver.CaresResolver') MotorCursor.tail '''''''''''''''' The ``MotorCursor.tail`` method has been removed. It was complex, diverged from PyMongo's feature set, and encouraged overuse of MongoDB capped collections as message queues when a purpose-built message queue is more appropriate. An example of tailing a capped collection is provided instead: :doc:`examples/tailable-cursors`. MotorClient.is_locked ''''''''''''''''''''' ``is_locked`` has been removed since calling it from Motor would be bizarre. If you called ``MotorClient.is_locked`` like:: locked = yield motor.Op(client.is_locked) you should now do:: result = yield client.admin.current_op() locked = bool(result.get('fsyncLock', None)) The result is ``True`` only if an administrator has called `fsyncLock`_ on the mongod. It is unlikely that you have any use for this. GridFSHandler ''''''''''''' :meth:`~web.GridFSHandler.get_gridfs_file` now 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/ New Features ~~~~~~~~~~~~ The introduction of a :ref:`Futures-based API ` is the most pervasive new feature. In addition Motor 0.2 includes new features from PyMongo 2.6 and 2.7: * :meth:`MotorCollection.aggregate` can return a cursor. * Support for all current MongoDB authentication mechanisms (see PyMongo's `authentication examples`_). * A new :meth:`MotorCollection.parallel_scan` method. * An :doc:`API for bulk writes `. * Support for wire protocol changes in MongoDB 2.6. * The ability to specify a server-side timeout for operations 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 Bugfixes ~~~~~~~~ ``MotorReplicaSetClient.open`` threw an error if called without a callback. ``MotorCursor.to_list`` `ignored SON manipulators `_. (Thanks to Eren Güven for the report and the fix.) `The full list is in Jira `_. Motor 0.1.2 ----------- Fixes innocuous unittest failures when running against Tornado 3.1.1. Motor 0.1.1 ----------- Fixes issue `MOTOR-12`_ by pinning its PyMongo dependency to PyMongo version 2.5.0 exactly. Motor relies on some of PyMongo's internal details, so changes to PyMongo can break Motor, and a change in PyMongo 2.5.1 did. Eventually PyMongo will expose stable hooks for Motor to use, but for now I changed Motor's dependency from ``PyMongo>=2.4.2`` to ``PyMongo==2.5.0``. .. _MOTOR-12: https://jira.mongodb.org/browse/MOTOR-12 motor-3.4.0/doc/conf.py000066400000000000000000000150071460060525600147310ustar00rootroot00000000000000# # Motor documentation build configuration file # # This file is execfile()d with the current directory set to its containing dir. import os import sys sys.path[0:0] = [os.path.abspath("..")] from pymongo import version as pymongo_version # noqa: E402 import motor # noqa: E402 # -- 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", ] primary_domain = "py" # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The root toctree document. root_doc = "index" # General information about the project. project = "Motor" copyright = "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 # built documents. # # The short X.Y version. version = motor.version # The full version, including alpha/beta/rc tags. release = motor.version # List of documents that shouldn't be included in the build. unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # 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" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] linkcheck_ignore = [r"http://localhost:\d+"] # -- Options for extensions ---------------------------------------------------- autoclass_content = "init" doctest_path = [os.path.abspath("..")] # Don't test examples pulled from PyMongo's docstrings just because they start # with '>>>' doctest_test_doctest_blocks = "" doctest_global_setup = """ import pprint import sys from datetime import timedelta from tornado import gen from tornado.ioloop import IOLoop import pymongo from pymongo.mongo_client import MongoClient sync_client = MongoClient() hello = sync_client.admin.command('hello') server_info = sync_client.server_info() if 'setName' in hello: raise Exception( "Run doctests with standalone MongoDB 5.0 server, not a replica set") if hello.get('msg') == 'isdbgrid': raise Exception( "Run doctests with standalone MongoDB 5.0 server, not mongos") if server_info['versionArray'][:2] != [5, 0]: raise Exception( "Run doctests with standalone MongoDB 5.0 server, not %s" % ( server_info['version'], )) sync_client.drop_database("doctest_test") db = sync_client.doctest_test import motor from motor import MotorClient """ # -- Options for HTML output --------------------------------------------------- html_copy_source = False # Theme gratefully vendored from CPython source. html_theme = "pydoctheme" html_theme_path = ["."] html_theme_options = {"collapsiblesidebar": True} html_static_path = ["static"] html_sidebars = { "index": ["globaltoc.html", "searchbox.html"], } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # 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 # 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 # 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'] # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = "Motor" + release.replace(".", "_") # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # 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"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_use_modindex = True autodoc_default_options = { "inherited-members": True, "member-order": "groupwise", } pymongo_inventory = ("https://pymongo.readthedocs.io/en/%s/" % pymongo_version, None) intersphinx_mapping = { "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), } motor-3.4.0/doc/configuration.rst000066400000000000000000000050311460060525600170270ustar00rootroot00000000000000Configuration ============= TLS Protocol Version '''''''''''''''''''' Industry best practices, and some regulations, require the use of TLS 1.1 or newer. Though no application changes are required for Motor to make use of the newest protocols, some operating systems or versions may not provide an OpenSSL version new enough to support them. Users of macOS older than 10.13 (High Sierra) will need to install Python from `python.org`_, `homebrew`_, `macports`_, or another similar source. Users of Linux or other non-macOS Unix can check their OpenSSL version like this:: $ openssl version If the version number is less than 1.0.1 support for TLS 1.1 or newer is not available. Contact your operating system vendor for a solution or upgrade to a newer distribution. You can check your Python interpreter by installing the `requests`_ module and executing the following command:: python -c "import requests; print(requests.get('https://www.howsmyssl.com/a/check', verify=False).json()['tls_version'])" You should see "TLS 1.X" where X is >= 1. You can read more about TLS versions and their security implications in this `cheat sheet`_. .. _python.org: https://www.python.org/downloads/ .. _homebrew: https://brew.sh/ .. _macports: https://www.macports.org/ .. _requests: https://pypi.python.org/pypi/requests .. _cheat sheet: https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html#only-support-strong-protocols Thread Pool Size '''''''''''''''' Motor uses the Python standard library's :class:`~concurrent.futures.ThreadPoolExecutor` to defer network operations to threads. By default, the executor uses at most five threads per CPU core on your system; to override the default set the environment variable ``MOTOR_MAX_WORKERS``. Some additional threads are used for monitoring servers and background tasks, so the total count of threads in your process will be greater. Network Compression ''''''''''''''''''' Like PyMongo, Motor supports network compression where network traffic between the client and MongoDB server are compressed. Keyword arguments to the Motor clients match those in MongoClient, documented `here `_. 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 `_ and zstandard requires `zstandard `_:: $ python3 -m pip install "motor[snappy, zstd]" motor-3.4.0/doc/contributors.rst000066400000000000000000000006561460060525600167250ustar00rootroot00000000000000Contributors ============ The following is a list of people who have contributed to **Motor**. If you belong here and are missing please let us know (or send a pull request after adding yourself to the list): - A\. Jesse Jiryu Davis - Eren Güven - Jorge Puente Sarrín - Rémi Jolin - Andrew Svetlov - Nikolay Novik - Prashant Mital - Shane Harvey - Bulat Khasanov - William Zhou - Tushar Singh - Steven Silvester - Julius Park motor-3.4.0/doc/coroutine_annotation.py000066400000000000000000000016111460060525600202410ustar00rootroot00000000000000"""Gratefully adapted from aiohttp, provides coroutine support to autodoc.""" from sphinx import addnodes from sphinx.domains.python import PyFunction, PyMethod class PyCoroutineMixin: def handle_signature(self, sig, signode): ret = super().handle_signature(sig, signode) signode.insert(0, addnodes.desc_annotation("coroutine ", "coroutine ")) return ret class PyCoroutineFunction(PyCoroutineMixin, PyFunction): def run(self): self.name = "py:function" return PyFunction.run(self) class PyCoroutineMethod(PyCoroutineMixin, PyMethod): def run(self): 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} motor-3.4.0/doc/developer-guide.rst000066400000000000000000000106631460060525600172470ustar00rootroot00000000000000=============== Developer Guide =============== Some explanations for those who would like to contribute to Motor development. Compatibility ------------- Motor supports the asyncio module in the standard library of Python 3.5.3 and later. Motor also works with Tornado 5.0 and later along with all the Python versions it supports. Each new Motor feature release depends on the latest PyMongo minor version release or newer, up to the next PyMongo major version release. For example, if 3.10 is the latest available PyMongo version when Motor 2.1 is being released, Motor 2.1 will require 3.10<=PyMongo<4. Frameworks ---------- Motor abstracts the differences between Tornado and asyncio by wrapping each in a "framework" interface. A Motor framework is a module implementing these properties and functions: - ``CLASS_PREFIX`` - ``add_future`` - ``call_soon`` - ``chain_future`` - ``chain_return_value`` - ``check_event_loop`` - ``coroutine`` (**DEPRECATED**) - ``get_event_loop`` - ``get_future`` - ``is_event_loop`` - ``is_future`` - ``platform_info`` - ``pymongo_class_wrapper`` - ``run_on_executor`` - ``yieldable`` (**DEPRECATED**) See the ``frameworks/tornado`` and ``frameworks/asyncio`` modules. .. note:: Starting in Motor 2.2, the functions marked **DEPRECATED** in the list above are not used internally in Motor. Instead of being removed from the codebase, they have been left in a deprecated state to avoid breaking any libraries built on top of Motor. These deprecated functions may be removed in a future major release. A framework-specific class, like ``MotorClient`` for Tornado or ``AsyncIOMotorClient`` for asyncio, is created by the ``create_class_with_framework`` function, which combines a framework with a framework-agnostic class, in this case ``AgnosticClient``. Wrapping PyMongo ---------------- For each PyMongo class, Motor declares an equivalent framework-agnostic class. For example, the ``AgnosticClient`` class is a framework-agnostic equivalent to PyMongo's ``MongoClient``. This agnostic class declares each method and property of the PyMongo class that it intends to wrap. These methods and properties begin life as type ``MotorAttributeFactory``. When ``create_class_with_framework`` creates a framework-specific class from an agnostic class, it creates methods and properties for that class which wrap the equivalent PyMongo methods and properties. For example, the ``AgnosticClient`` class declares that ``drop_database`` is an ``AsyncCommand``, which is a subclass of ``MotorAttributeFactory``. At import time, ``create_class_with_framework`` calls the ``create_attribute`` method of each ``MotorAttributeFactory`` on the ``AgnosticClient``, which results in framework-specific implementations of each method and property. So at import time, ``create_class_with_framework`` generates framework-specific wrappers of ``drop_database`` for ``MotorClient`` and ``AsyncIOMotorClient``. These wrappers use framework-specific features to run the ``drop_database`` method asynchronously. Asynchronization ---------------- This is the heart of Motor's implementation. The ``create_attribute`` method for asynchronous methods like ``drop_database`` wraps the equivalent PyMongo method in a Motor method. This wrapper method uses either the Tornado or asyncio framework to: - get a reference to the framework's event loop - start the PyMongo method on a thread in the global ``ThreadPoolExecutor`` - create a ``Future`` that will be resolved by the event loop when the thread finishes - returns the ``Future`` to the caller This is what allows Tornado or asyncio awaitables to call Motor methods with ``await`` to await I/O without blocking the event loop. Synchro ------- A common kind of bug in Motor arises when PyMongo adds a feature, like a new method or new optional behavior, which we forget to wrap with Motor. Since PyMongo adds a test to its suite for each new feature, we could catch these omissions by applying PyMongo's latest tests to Motor. Then a missing method or feature would cause an obvious test failure. But PyMongo is 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 synchro37`` to check out PyMongo's test suite and run it with Synchro. motor-3.4.0/doc/differences.rst000066400000000000000000000103451460060525600164410ustar00rootroot00000000000000.. currentmodule:: motor.motor_tornado ===================================== Differences between Motor and PyMongo ===================================== .. important:: This page describes using Motor with Tornado. Beginning in version 0.5 Motor can also integrate with asyncio instead of Tornado. Major differences ================= Connecting to MongoDB --------------------- Motor provides a single client class, :class:`MotorClient`. Unlike PyMongo's :class:`~pymongo.mongo_client.MongoClient`, Motor's client class does not begin connecting in the background when it is instantiated. Instead it connects on demand, when you first attempt an operation. Coroutines ---------- Motor supports nearly every method PyMongo does, but Motor methods that do network I/O are *coroutines*. See :doc:`tutorial-tornado`. Threading and forking --------------------- Multithreading and forking are not supported; Motor is intended to be used in a single-threaded Tornado application. See Tornado's documentation on `running Tornado in production`_ to take advantage of multiple cores. .. _`running Tornado in production`: http://www.tornadoweb.org/en/stable/guide/running.html Minor differences ================= .. _gridfs-differences: GridFS ------ - File-like 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 file API and aren't suitable in the same circumstances as files. - Setting properties In PyMongo, you can set arbitrary attributes on a :class:`~gridfs.grid_file.GridIn` and they're stored as metadata on the server, even after the ``GridIn`` is closed:: fs = gridfs.GridFSBucket(db) grid_in = fs.open_upload_stream('test_file') grid_in.close() grid_in.my_field = 'my_value' # Sends update to server. Updating metadata on a :class:`MotorGridIn` is asynchronous, so the API is different:: async def f(): fs = motor.motor_tornado.MotorGridFSBucket(db) grid_in = fs.open_upload_stream('test_file') await grid_in.close() # Sends update to server. await grid_in.set('my_field', 'my_value') .. seealso:: :doc:`../api-tornado/gridfs`. is_locked --------- In PyMongo ``is_locked`` is a property of :class:`~pymongo.mongo_client.MongoClient`. Since determining whether the server has been fsyncLocked requires I/O, Motor has no such convenience method. The equivalent in Motor is:: result = await client.admin.current_op() locked = bool(result.get('fsyncLock', None)) system_js --------- PyMongo supports Javascript procedures stored in MongoDB with syntax like: .. code-block:: pycon >>> db.system_js.my_func = "function(x) { return x * x; }" >>> db.system_js.my_func(2) 4.0 Motor does not. Cursor slicing -------------- In PyMongo, the following raises an ``IndexError`` if the collection has fewer than 101 documents: .. code-block:: python # Can raise IndexError. doc = db.collection.find()[100] In Motor, however, no exception is raised. The query simply has no results: .. code-block:: python async def f(): cursor = db.collection.find()[100] # Iterates zero or one time. async for doc in cursor: print(doc) The difference arises because the PyMongo :class:`~pymongo.cursor.Cursor`'s slicing operator blocks until it has queried the MongoDB server, and determines if a document exists at the desired offset; Motor simply returns a new :class:`MotorCursor` with a skip and limit applied. Creating a collection --------------------- There are two ways to create a capped collection using PyMongo: .. code-block:: python # Typical: db.create_collection("collection1", capped=True, size=1000) # Unusual: collection = Collection(db, "collection2", capped=True, size=1000) Motor can't do I/O in a constructor, so the unusual style is prohibited and only the typical style is allowed: .. code-block:: python async def f(): await db.create_collection("collection1", capped=True, size=1000) motor-3.4.0/doc/docs-requirements.txt000066400000000000000000000001251460060525600176370ustar00rootroot00000000000000tornado aiohttp sphinx>=5.3,<8 sphinx_rtd_theme>=2,<3 readthedocs-sphinx-search~=0.3 motor-3.4.0/doc/examples/000077500000000000000000000000001460060525600152455ustar00rootroot00000000000000motor-3.4.0/doc/examples/aiohttp_example.py000066400000000000000000000026051460060525600210050ustar00rootroot00000000000000# These comments let tutorial-asyncio.rst include this code in sections. # -- setup-start -- from aiohttp import web from motor.motor_asyncio import AsyncIOMotorClient async def setup_db(): db = AsyncIOMotorClient().test await db.pages.drop() 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.")}) 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") # Retrieve the long-lived database handle. 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") # -- 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() # -- main-end -- motor-3.4.0/doc/examples/aiohttp_gridfs_example.py000066400000000000000000000030111460060525600223330ustar00rootroot00000000000000"""Serve pre-compressed static content from GridFS with aiohttp. Requires Python 3.5 or later and aiohttp 3.0 or later. Start a MongoDB server on its default port, run this script, and visit: http://localhost:8080/fs/my_file """ # -- include-start -- import asyncio import gzip import tempfile import aiohttp.web from motor.aiohttp import AIOHTTPGridFS from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorGridFSBucket 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: for _ in range(10): 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} ) 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" 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) aiohttp.web.run_app(app) motor-3.4.0/doc/examples/aiohttp_gridfs_example.rst000066400000000000000000000010501460060525600225140ustar00rootroot00000000000000AIOHTTPGridFS Example ===================== Serve pre-compressed static content from GridFS over HTTP. Uses the `aiohttp`_ web framework and :class:`~motor.aiohttp.AIOHTTPGridFS`. .. _aiohttp: https://aiohttp.readthedocs.io/ Instructions ------------ Start a MongoDB server on its default port and run this script. Then visit: http://localhost:8080/fs/my_file Serve compressed static content from GridFS ------------------------------------------- .. literalinclude:: aiohttp_gridfs_example.py :language: python3 :start-after: include-start motor-3.4.0/doc/examples/authentication.rst000066400000000000000000000016031460060525600210160ustar00rootroot00000000000000.. currentmodule:: motor.motor_tornado Authentication With Motor ========================= This page describes using Motor with Tornado. Beginning in version 0.5 Motor can also integrate with asyncio instead of Tornado. To use authentication, you must start ``mongod`` with ``--auth`` or, for replica sets or sharded clusters, ``--keyFile``. Create an admin user and optionally normal users or read-only users. .. seealso:: `MongoDB Authentication `_ To create an authenticated connection use a `MongoDB connection URI`_:: uri = "mongodb://user:pass@localhost:27017/database_name" client = motor.motor_tornado.MotorClient(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/ motor-3.4.0/doc/examples/auto_csfle_example.py000066400000000000000000000074211460060525600214620ustar00rootroot00000000000000import asyncio import os from bson import json_util from bson.codec_options import CodecOptions from pymongo.encryption import Algorithm from pymongo.encryption_options import AutoEncryptionOpts from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption async def create_json_schema_file(kms_providers, key_vault_namespace, key_vault_client): client_encryption = AsyncIOMotorClientEncryption( kms_providers, key_vault_namespace, key_vault_client, # The CodecOptions class used for encrypting and decrypting. # This should be the same CodecOptions instance you have configured # on MotorClient, Database, or Collection. We will not be calling # encrypt() or decrypt() in this example so we can use any # CodecOptions. CodecOptions(), ) # Create a new data key and json schema for the encryptedField. # https://dochub.mongodb.org/core/client-side-field-level-encryption-automatic-encryption-rules data_key_id = await client_encryption.create_data_key( "local", key_alt_names=["pymongo_encryption_example_1"] ) schema = { "properties": { "encryptedField": { "encrypt": { "keyId": [data_key_id], "bsonType": "string", "algorithm": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, } } }, "bsonType": "object", } # Use CANONICAL_JSON_OPTIONS so that other drivers and tools will be # able to parse the MongoDB extended JSON file. json_schema_string = json_util.dumps(schema, json_options=json_util.CANONICAL_JSON_OPTIONS) with open("jsonSchema.json", "w") as file: file.write(json_schema_string) async def main(): # The MongoDB namespace (db.collection) used to store the # encrypted documents in this example. encrypted_namespace = "test.coll" # This must be the same master key that was used to create # the encryption key. local_master_key = os.urandom(96) kms_providers = {"local": {"key": local_master_key}} # The MongoDB namespace (db.collection) used to store # the encryption data keys. key_vault_namespace = "encryption.__pymongoTestKeyVault" key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1) # The MotorClient used to access the key vault (key_vault_namespace). key_vault_client = AsyncIOMotorClient() key_vault = key_vault_client[key_vault_db_name][key_vault_coll_name] # Ensure that two data keys cannot share the same keyAltName. await key_vault.drop() await key_vault.create_index( "keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}} ) await create_json_schema_file(kms_providers, key_vault_namespace, key_vault_client) # Load the JSON Schema and construct the local schema_map option. with open("jsonSchema.json") as file: json_schema_string = file.read() json_schema = json_util.loads(json_schema_string) schema_map = {encrypted_namespace: json_schema} auto_encryption_opts = AutoEncryptionOpts( kms_providers, key_vault_namespace, schema_map=schema_map ) client = AsyncIOMotorClient(auto_encryption_opts=auto_encryption_opts) db_name, coll_name = encrypted_namespace.split(".", 1) coll = client[db_name][coll_name] # Clear old data await coll.drop() await coll.insert_one({"encryptedField": "123456789"}) decrypted_doc = await coll.find_one() print(f"Decrypted document: {decrypted_doc}") unencrypted_coll = AsyncIOMotorClient()[db_name][coll_name] encrypted_doc = await unencrypted_coll.find_one() print(f"Encrypted document: {encrypted_doc}") if __name__ == "__main__": asyncio.run(main()) motor-3.4.0/doc/examples/bulk.rst000066400000000000000000000146421460060525600167430ustar00rootroot00000000000000.. currentmodule:: motor.motor_tornado .. _bulk-write-tutorial: Bulk Write Operations ===================== .. testsetup:: client = MotorClient() db = client.test_database IOLoop.current().run_sync(db.test.drop) This tutorial explains how to take advantage of Motor's bulk write operation features. Executing write operations in batches reduces the number of network round trips, increasing write throughput. This example describes using Motor with Tornado. Beginning in version 0.5 Motor can also integrate with asyncio instead of Tornado. Bulk Insert ----------- A batch of documents can be inserted by passing a list or generator to the :meth:`~MotorCollection.insert_many` method. Motor will automatically split the batch into smaller sub-batches based on the maximum message size accepted by MongoDB, supporting very large bulk insert operations. .. doctest:: >>> async def f(): ... await db.test.insert_many(({"i": i} for i in range(10000))) ... count = await db.test.count_documents({}) ... print("Final count: %d" % count) ... >>> >>> IOLoop.current().run_sync(f) Final count: 10000 Mixed Bulk Write Operations --------------------------- Motor also supports executing mixed bulk write operations. A batch of insert, update, and remove operations can be executed together using the bulk write operations API. .. _ordered_bulk: Ordered Bulk Write Operations ............................. Ordered bulk write operations are batched and sent to the server in the order provided for serial execution. The return value is an instance of :class:`~pymongo.results.BulkWriteResult` describing the type and count of operations performed. .. doctest:: :options: +NORMALIZE_WHITESPACE >>> from pprint import pprint >>> from pymongo import InsertOne, DeleteMany, ReplaceOne, UpdateOne >>> async def f(): ... result = await db.test.bulk_write( ... [ ... DeleteMany({}), # Remove all documents from the previous example. ... InsertOne({"_id": 1}), ... InsertOne({"_id": 2}), ... InsertOne({"_id": 3}), ... UpdateOne({"_id": 1}, {"$set": {"foo": "bar"}}), ... UpdateOne({"_id": 4}, {"$inc": {"j": 1}}, upsert=True), ... ReplaceOne({"j": 1}, {"j": 2}), ... ] ... ) ... pprint(result.bulk_api_result) ... >>> IOLoop.current().run_sync(f) {'nInserted': 3, 'nMatched': 2, 'nModified': 2, 'nRemoved': 10000, 'nUpserted': 1, 'upserted': [{'_id': 4, 'index': 5}], 'writeConcernErrors': [], 'writeErrors': []} The first write failure that occurs (e.g. duplicate key error) aborts the remaining operations, and Motor raises :class:`~pymongo.errors.BulkWriteError`. The :attr:`details` attribute of the exception instance provides the execution results up until the failure occurred and details about the failure - including the operation that caused the failure. .. doctest:: :options: +NORMALIZE_WHITESPACE >>> from pymongo import InsertOne, DeleteOne, ReplaceOne >>> from pymongo.errors import BulkWriteError >>> async def f(): ... requests = [ ... ReplaceOne({"j": 2}, {"i": 5}), ... InsertOne({"_id": 4}), # Violates the unique key constraint on _id. ... DeleteOne({"i": 5}), ... ] ... try: ... await db.test.bulk_write(requests) ... except BulkWriteError as bwe: ... pprint(bwe.details) ... >>> IOLoop.current().run_sync(f) {'nInserted': 0, 'nMatched': 1, 'nModified': 1, 'nRemoved': 0, 'nUpserted': 0, 'upserted': [], 'writeConcernErrors': [], 'writeErrors': [{'code': 11000, 'errmsg': '... duplicate key error ...', 'index': 1, 'keyPattern': {'_id': 1}, 'keyValue': {'_id': 4}, 'op': {'_id': 4}}]} .. _unordered_bulk: Unordered Bulk Write Operations ............................... Unordered bulk write operations are batched and sent to the server in **arbitrary order** where they may be executed in parallel. Any errors that occur are reported after all operations are attempted. In the next example the first and third operations fail due to the unique constraint on _id. Since we are doing unordered execution the second and fourth operations succeed. .. doctest:: :options: +NORMALIZE_WHITESPACE >>> async def f(): ... requests = [ ... InsertOne({"_id": 1}), ... DeleteOne({"_id": 2}), ... InsertOne({"_id": 3}), ... ReplaceOne({"_id": 4}, {"i": 1}), ... ] ... try: ... await db.test.bulk_write(requests, ordered=False) ... except BulkWriteError as bwe: ... pprint(bwe.details) ... >>> IOLoop.current().run_sync(f) {'nInserted': 0, 'nMatched': 1, 'nModified': 1, 'nRemoved': 1, 'nUpserted': 0, 'upserted': [], 'writeConcernErrors': [], 'writeErrors': [{'code': 11000, 'errmsg': '... duplicate key error ...', 'index': 0, 'keyPattern': {'_id': 1}, 'keyValue': {'_id': 1}, 'op': {'_id': 1}}, {'code': 11000, 'errmsg': '... duplicate key error ...', 'index': 2, 'keyPattern': {'_id': 1}, 'keyValue': {'_id': 3}, 'op': {'_id': 3}}]} Write Concern ............. Bulk operations are executed with the :attr:`~pymongo.collection.Collection.write_concern` of the collection they are executed against. Write concern errors (e.g. wtimeout) will be reported after all operations are attempted, regardless of execution order. .. doctest:: :options: +SKIP .. Standalone MongoDB raises "can't use w>1" with this example, so skip it. >>> from pymongo import WriteConcern >>> async def f(): ... coll = db.get_collection("test", write_concern=WriteConcern(w=4, wtimeout=1)) ... try: ... await coll.bulk_write([InsertOne({"a": i}) for i in range(4)]) ... except BulkWriteError as bwe: ... pprint(bwe.details) ... >>> IOLoop.current().run_sync(f) {'nInserted': 4, 'nMatched': 0, 'nModified': 0, 'nRemoved': 0, 'nUpserted': 0, 'upserted': [], 'writeConcernErrors': [{'code': 64, 'errInfo': {'wtimeout': True}, 'errmsg': 'waiting for replication timed out'}], 'writeErrors': []} motor-3.4.0/doc/examples/encryption.rst000066400000000000000000000121151460060525600201710ustar00rootroot00000000000000.. _Client-Side Field Level Encryption: Client-Side Field Level Encryption ================================== 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 `_ and `TLS/SSL (Transport Encryption) `_. With field level encryption, applications can encrypt fields in documents *prior* to transmitting data over the wire to the server. Client-side field level encryption supports workloads where applications must guarantee that unauthorized parties, including server administrators, cannot read the encrypted data. .. mongodoc:: client-side-field-level-encryption Dependencies ------------ To get started using client-side field level encryption in your project, you will need to install the `pymongocrypt `_ library as well as the driver itself. Install both the driver and a compatible version of pymongocrypt like this:: $ python -m pip install 'motor[encryption]' Note that installing on Linux requires pip 19 or later for manylinux2010 wheel support. For more information about installing pymongocrypt see `the installation instructions on the project's PyPI page `_. mongocryptd ----------- The ``mongocryptd`` binary is required for automatic client-side encryption and is included as a component in the `MongoDB Enterprise Server package `_. For more information on this binary, see the `PyMongo documentation on mongocryptd `_. Automatic Client-Side Field Level Encryption -------------------------------------------- Automatic client-side field level encryption is enabled by creating a :class:`~motor.motor_asyncio.AsyncIOMotorClient` with the ``auto_encryption_opts`` option set to an instance of :class:`~pymongo.encryption_options.AutoEncryptionOpts`. The following examples show how to setup automatic client-side field level encryption using :class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption` to create a new encryption data key. .. note:: Automatic client-side field level encryption requires MongoDB 4.2+ enterprise or a MongoDB 4.2+ Atlas cluster. The community version of the server supports automatic decryption as well as :ref:`explicit-client-side-encryption`. Providing Local Automatic Encryption Rules ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example shows how to specify automatic encryption rules via the ``schema_map`` option. The automatic encryption rules are expressed using a `strict subset of the JSON Schema syntax `_. Supplying a ``schema_map`` provides more security than relying on JSON Schemas obtained from the server. It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending unencrypted data that should be encrypted. JSON Schemas supplied in the ``schema_map`` only apply to configuring automatic client-side field level encryption. Other validation rules in the JSON schema will not be enforced by the driver and will result in an error. .. literalinclude:: auto_csfle_example.py :language: python3 Server-Side Field Level Encryption Enforcement ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The MongoDB 4.2+ server supports using schema validation to enforce encryption of specific fields in a collection. This schema validation will prevent an application from inserting unencrypted values for any fields marked with the ``"encrypt"`` JSON schema keyword. The following example shows how to setup automatic client-side field level encryption using :class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption` to create a new encryption data key and create a collection with the `Automatic Encryption JSON Schema Syntax `_. .. literalinclude:: server_fle_enforcement_example.py :language: python3 .. _explicit-client-side-encryption: Explicit Encryption ~~~~~~~~~~~~~~~~~~~ Explicit encryption is a MongoDB community feature and does not use the ``mongocryptd`` process. Explicit encryption is provided by the :class:`~motor.motor_asyncio.AsyncIOMotorClientEncryption` class, for example: .. literalinclude:: explicit_encryption_example.py :language: python3 Explicit Encryption with Automatic Decryption ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Although automatic encryption requires MongoDB 4.2 enterprise or a MongoDB 4.2 Atlas cluster, automatic *decryption* is supported for all users. To configure automatic *decryption* without automatic *encryption* set ``bypass_auto_encryption=True`` in :class:`~pymongo.encryption_options.AutoEncryptionOpts`: .. literalinclude:: explicit_encryption_automatic_decryption_example.py :language: python3 motor-3.4.0/doc/examples/explicit_encryption_automatic_decryption_example.py000066400000000000000000000053421460060525600277370ustar00rootroot00000000000000import asyncio import os from pymongo.encryption import Algorithm from pymongo.encryption_options import AutoEncryptionOpts from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption async def main(): # This must be the same master key that was used to create # the encryption key. local_master_key = os.urandom(96) kms_providers = {"local": {"key": local_master_key}} # The MongoDB namespace (db.collection) used to store # the encryption data keys. key_vault_namespace = "encryption.__pymongoTestKeyVault" key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1) # bypass_auto_encryption=True disable automatic encryption but keeps # the automatic _decryption_ behavior. bypass_auto_encryption will # also disable spawning mongocryptd. auto_encryption_opts = AutoEncryptionOpts( kms_providers, key_vault_namespace, bypass_auto_encryption=True ) client = AsyncIOMotorClient(auto_encryption_opts=auto_encryption_opts) coll = client.test.coll # Clear old data await coll.drop() # Set up the key vault (key_vault_namespace) for this example. key_vault = client[key_vault_db_name][key_vault_coll_name] # Ensure that two data keys cannot share the same keyAltName. await key_vault.drop() await key_vault.create_index( "keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}} ) client_encryption = AsyncIOMotorClientEncryption( kms_providers, key_vault_namespace, # The MotorClient to use for reading/writing to the key vault. # This can be the same MotorClient used by the main application. client, # The CodecOptions class used for encrypting and decrypting. # This should be the same CodecOptions instance you have configured # on MotorClient, Database, or Collection. coll.codec_options, ) # Create a new data key for the encryptedField. _ = await client_encryption.create_data_key( "local", key_alt_names=["pymongo_encryption_example_4"] ) # Explicitly encrypt a field: encrypted_field = await client_encryption.encrypt( "123456789", Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_alt_name="pymongo_encryption_example_4", ) await coll.insert_one({"encryptedField": encrypted_field}) # Automatically decrypts any encrypted fields. doc = await coll.find_one() print(f"Decrypted document: {doc}") unencrypted_coll = AsyncIOMotorClient().test.coll print(f"Encrypted document: {await unencrypted_coll.find_one()}") # Cleanup resources. await client_encryption.close() if __name__ == "__main__": asyncio.run(main()) motor-3.4.0/doc/examples/explicit_encryption_example.py000066400000000000000000000045231460060525600234310ustar00rootroot00000000000000import asyncio import os from pymongo.encryption import Algorithm from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption async def main(): # This must be the same master key that was used to create # the encryption key. local_master_key = os.urandom(96) kms_providers = {"local": {"key": local_master_key}} # The MongoDB namespace (db.collection) used to store # the encryption data keys. key_vault_namespace = "encryption.__pymongoTestKeyVault" key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1) # The MotorClient used to read/write application data. client = AsyncIOMotorClient() coll = client.test.coll # Clear old data await coll.drop() # Set up the key vault (key_vault_namespace) for this example. key_vault = client[key_vault_db_name][key_vault_coll_name] # Ensure that two data keys cannot share the same keyAltName. await key_vault.drop() await key_vault.create_index( "keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}} ) client_encryption = AsyncIOMotorClientEncryption( kms_providers, key_vault_namespace, # The Motorlient to use for reading/writing to the key vault. # This can be the same MotorClient used by the main application. client, # The CodecOptions class used for encrypting and decrypting. # This should be the same CodecOptions instance you have configured # on MotorClient, Database, or Collection. coll.codec_options, ) # Create a new data key for the encryptedField. data_key_id = await client_encryption.create_data_key( "local", key_alt_names=["pymongo_encryption_example_3"] ) # Explicitly encrypt a field: encrypted_field = await client_encryption.encrypt( "123456789", Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_id=data_key_id ) await coll.insert_one({"encryptedField": encrypted_field}) doc = await coll.find_one() print(f"Encrypted document: {doc}") # Explicitly decrypt the field: doc["encryptedField"] = await client_encryption.decrypt(doc["encryptedField"]) print(f"Decrypted document: {doc}") # Cleanup resources. await client_encryption.close() if __name__ == "__main__": asyncio.run(main()) motor-3.4.0/doc/examples/index.rst000066400000000000000000000004151460060525600171060ustar00rootroot00000000000000Motor Examples ============== .. seealso:: :doc:`../tutorial-tornado` .. toctree:: bulk monitoring tailable-cursors tornado_change_stream_example authentication aiohttp_gridfs_example encryption See also :ref:`example-web-application-aiohttp`. motor-3.4.0/doc/examples/monitoring.rst000066400000000000000000000110651460060525600201670ustar00rootroot00000000000000.. currentmodule:: motor.motor_tornado Application Performance Monitoring (APM) ======================================== 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. Motor wraps PyMongo, and it shares PyMongo's API for monitoring. To receive notifications about events, you subclass one of PyMongo's four listener classes, :class:`~pymongo.monitoring.CommandListener`, :class:`~pymongo.monitoring.ServerListener`, :class:`~pymongo.monitoring.TopologyListener`, or :class:`~pymongo.monitoring.ServerHeartbeatListener`. Command Monitoring ------------------ Subclass :class:`~pymongo.monitoring.CommandListener` to be notified whenever a command starts, succeeds, or fails. .. literalinclude:: monitoring_example.py :language: py3 :start-after: command logger start :end-before: command logger end Register an instance of ``MyCommandLogger``: .. literalinclude:: monitoring_example.py :language: py3 :start-after: command logger register start :end-before: command logger register end You can register any number of listeners, of any of the four listener types. Although you use only APIs from PyMongo's :mod:`~pymongo.monitoring` module to configure monitoring, if you create a :class:`MotorClient` its commands are monitored, the same as a PyMongo :class:`~pymongo.mongo_client.MongoClient`. .. literalinclude:: monitoring_example.py :language: py3 :start-after: motorclient start :end-before: motorclient end This logs something like: .. code-block:: text Command insert with request id 50073 started on server ('localhost', 27017) Command insert with request id 50073 on server ('localhost', 27017) succeeded in 362 microseconds See PyMongo's :mod:`~pymongo.monitoring` module for details about the event data your callbacks receive. Server and Topology Monitoring ------------------------------ Subclass :class:`~pymongo.monitoring.ServerListener` to be notified whenever Motor detects a change in the state of a MongoDB server it is connected to. .. literalinclude:: monitoring_example.py :language: py3 :start-after: server logger start :end-before: server logger end Subclass :class:`~pymongo.monitoring.TopologyListener` to be notified whenever Motor detects a change in the state of your server topology. Examples of such topology changes are a replica set failover, or if you are connected to several mongos servers and one becomes unavailable. .. literalinclude:: monitoring_example.py :language: py3 :start-after: topology logger start :end-before: topology logger end Motor monitors MongoDB servers with periodic checks called "heartbeats". Subclass :class:`~pymongo.monitoring.ServerHeartbeatListener` to be notified whenever Motor begins a server check, and whenever a check succeeds or fails. .. literalinclude:: monitoring_example.py :language: py3 :start-after: heartbeat logger start :end-before: heartbeat logger end Thread Safety ------------- Watch out: Your listeners' callbacks are executed on various background threads, *not* the main thread. To interact with Tornado or Motor from a listener callback, you must defer to the main thread using :meth:`IOLoop.add_callback `, which is the only thread-safe :class:`~tornado.ioloop.IOLoop` method. Similarly, if you use asyncio instead of Tornado, defer your action to the main thread with :meth:`~asyncio.AbstractEventLoop.call_soon_threadsafe`. There is probably no need to be concerned about this detail, however: logging is the only reasonable thing to do from a listener, and `the Python logging module is thread-safe `_. Further Information ------------------- See also: * PyMongo's :mod:`~pymongo.monitoring` module * The `Command Monitoring`_ Spec * The `Topology Monitoring`_ Spec * The `monitoring_example.py`_ example file in the Motor repository .. _Command Monitoring: https://github.com/mongodb/specifications/blob/master/source/command-logging-and-monitoring/command-logging-and-monitoring.rst .. _Topology Monitoring: https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring-logging-and-monitoring.rst .. _monitoring_example.py: https://github.com/mongodb/motor/blob/master/doc/examples/monitoring_example.py motor-3.4.0/doc/examples/monitoring_example.py000066400000000000000000000104721460060525600215230ustar00rootroot00000000000000# Copyright 2016 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. """Application Performance Monitoring (APM) example""" # command logger start import logging import sys from pymongo import monitoring 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}" ) 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" ) 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" ) # command logger end # command logger register start monitoring.register(CommandLogger()) # command logger register end # 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!"}) # For this example, wait 10 seconds for more monitoring events to fire. await gen.sleep(10) 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}") 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}" ) def closed(self, event): logging.warning(f"Server {event.server_address} removed from topology {event.topology_id}") 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") def description_changed(self, event): logging.info(f"Topology description updated for topology id {event.topology_id}") 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}" ) def closed(self, event): logging.info(f"Topology with id {event.topology_id} closed") 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}") def succeeded(self, event): logging.info( f"Heartbeat to server {event.connection_id} " "succeeded with reply " f"{event.reply.document}" ) def failed(self, event): logging.warning( f"Heartbeat to server {event.connection_id} failed with error {event.reply}" ) monitoring.register(HeartbeatLogger()) # heartbeat logger end motor-3.4.0/doc/examples/server_fle_enforcement_example.py000066400000000000000000000074501460060525600240610ustar00rootroot00000000000000import asyncio import os from bson.binary import STANDARD from bson.codec_options import CodecOptions from pymongo.encryption import Algorithm from pymongo.encryption_options import AutoEncryptionOpts from pymongo.errors import OperationFailure from pymongo.write_concern import WriteConcern from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorClientEncryption async def main(): # The MongoDB namespace (db.collection) used to store the # encrypted documents in this example. encrypted_namespace = "test.coll" # This must be the same master key that was used to create # the encryption key. local_master_key = os.urandom(96) kms_providers = {"local": {"key": local_master_key}} # The MongoDB namespace (db.collection) used to store # the encryption data keys. key_vault_namespace = "encryption.__pymongoTestKeyVault" key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1) # The MotorClient used to access the key vault (key_vault_namespace). key_vault_client = AsyncIOMotorClient() key_vault = key_vault_client[key_vault_db_name][key_vault_coll_name] # Ensure that two data keys cannot share the same keyAltName. await key_vault.drop() await key_vault.create_index( "keyAltNames", unique=True, partialFilterExpression={"keyAltNames": {"$exists": True}} ) client_encryption = AsyncIOMotorClientEncryption( kms_providers, key_vault_namespace, key_vault_client, # The CodecOptions class used for encrypting and decrypting. # This should be the same CodecOptions instance you have configured # on MotorClient, Database, or Collection. We will not be calling # encrypt() or decrypt() in this example so we can use any # CodecOptions. CodecOptions(), ) # Create a new data key and json schema for the encryptedField. data_key_id = await client_encryption.create_data_key( "local", key_alt_names=["pymongo_encryption_example_2"] ) json_schema = { "properties": { "encryptedField": { "encrypt": { "keyId": [data_key_id], "bsonType": "string", "algorithm": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, } } }, "bsonType": "object", } auto_encryption_opts = AutoEncryptionOpts(kms_providers, key_vault_namespace) client = AsyncIOMotorClient(auto_encryption_opts=auto_encryption_opts) db_name, coll_name = encrypted_namespace.split(".", 1) db = client[db_name] # Clear old data await db.drop_collection(coll_name) # Create the collection with the encryption JSON Schema. await db.create_collection( coll_name, # uuid_representation=STANDARD is required to ensure that any # UUIDs in the $jsonSchema document are encoded to BSON Binary # with the standard UUID subtype 4. This is only needed when # running the "create" collection command with an encryption # JSON Schema. codec_options=CodecOptions(uuid_representation=STANDARD), write_concern=WriteConcern(w="majority"), validator={"$jsonSchema": json_schema}, ) coll = client[db_name][coll_name] await coll.insert_one({"encryptedField": "123456789"}) decrypted_doc = await coll.find_one() print(f"Decrypted document: {decrypted_doc}") unencrypted_coll = AsyncIOMotorClient()[db_name][coll_name] encrypted_doc = await unencrypted_coll.find_one() print(f"Encrypted document: {encrypted_doc}") try: await unencrypted_coll.insert_one({"encryptedField": "123456789"}) except OperationFailure as exc: print(f"Unencrypted insert failed: {exc.details}") if __name__ == "__main__": asyncio.run(main()) motor-3.4.0/doc/examples/tailable-cursors.rst000066400000000000000000000037071460060525600212610ustar00rootroot00000000000000.. currentmodule:: motor.motor_tornado Motor Tailable Cursor Example ============================= By default, MongoDB will automatically close a cursor when the client has exhausted all results in the cursor. However, for capped collections you may use a tailable cursor that remains open after the client exhausts the results in the initial cursor. The following is a basic example of using a tailable cursor to tail the oplog of a replica set member: .. code-block:: python from asyncio import sleep from pymongo.cursor import CursorType async def tail_oplog_example(): oplog = client.local.oplog.rs first = await oplog.find().sort("$natural", pymongo.ASCENDING).limit(-1).next() print(first) ts = first["ts"] while True: # For a regular capped collection CursorType.TAILABLE_AWAIT is the # only option required to create a tailable cursor. When querying the # oplog, the oplog_replay option enables an optimization to quickly # find the 'ts' value we're looking for. The oplog_replay option # can only be used when querying the oplog. Starting in MongoDB 4.4 # this option is ignored by the server as queries against the oplog # are optimized automatically by the MongoDB query engine. cursor = oplog.find( {"ts": {"$gt": ts}}, cursor_type=CursorType.TAILABLE_AWAIT, oplog_replay=True, ) while cursor.alive: async for doc in cursor: ts = doc["ts"] print(doc) # We end up here if the find() returned no documents or if the # tailable cursor timed out (no new documents were added to the # collection for more than 1 second). await sleep(1) .. seealso:: `Tailable cursors `_ motor-3.4.0/doc/examples/tornado_change_stream_example.py000066400000000000000000000065651460060525600236740ustar00rootroot00000000000000import logging import os import sys from base64 import urlsafe_b64encode from pprint import pformat import tornado.escape import tornado.ioloop import tornado.options import tornado.web import tornado.websocket from bson import json_util # Installed with PyMongo. from tornado.options import define, options from motor.motor_tornado import MotorClient define("port", default=8888, help="run on the given port", type=int) define("debug", default=False, help="reload on source changes") define("mongo", default="mongodb://localhost", help="MongoDB URI") define("ns", default="test.test", help="database and collection name") class Application(tornado.web.Application): def __init__(self): handlers = [(r"/", MainHandler), (r"/socket", ChangesHandler)] templates = os.path.join(os.path.dirname(__file__), "tornado_change_stream_templates") super().__init__( handlers, template_path=templates, template_whitespace="all", debug=options.debug ) class MainHandler(tornado.web.RequestHandler): def get(self): self.render("index.html", changes=ChangesHandler.cache) class ChangesHandler(tornado.websocket.WebSocketHandler): waiters = set() cache = [] cache_size = 5 def open(self): ChangesHandler.waiters.add(self) def on_close(self): ChangesHandler.waiters.remove(self) @classmethod def update_cache(cls, change): cls.cache.append(change) if len(cls.cache) > cls.cache_size: cls.cache = cls.cache[-cls.cache_size :] @classmethod def send_change(cls, change): change_json = json_util.dumps(change) for waiter in cls.waiters: try: waiter.write_message(change_json) except Exception as exc: logging.exception(exc) @classmethod def on_change(cls, change): logging.info("got change of type '%s'", change.get("operationType")) # Each change notification has a binary _id. Use it to make an HTML # element id, then remove it. data = change["_id"]["_data"] if not isinstance(data, bytes): data = data.encode("utf-8") html_id = urlsafe_b64encode(data).decode().rstrip("=") change.pop("_id") change_pre = tornado.escape.xhtml_escape(pformat(change)) change["html"] = f'
    {change_pre}
    ' change["html_id"] = html_id ChangesHandler.send_change(change) ChangesHandler.update_cache(change) change_stream = None async def watch(collection): global change_stream async with collection.watch() as change_stream: async for change in change_stream: ChangesHandler.on_change(change) def main(): tornado.options.parse_command_line() if "." not in options.ns: sys.stderr.write(f'Invalid ns "{options.ns}", must contain a "."') sys.exit(1) db_name, collection_name = options.ns.split(".", 1) client = MotorClient(options.mongo) collection = client[db_name][collection_name] app = Application() app.listen(options.port) loop = tornado.ioloop.IOLoop.current() # Start watching collection for changes. try: loop.run_sync(lambda: watch(collection)) except KeyboardInterrupt: if change_stream: loop.run_sync(change_stream.close) if __name__ == "__main__": main() motor-3.4.0/doc/examples/tornado_change_stream_example.rst000066400000000000000000000027551460060525600240510ustar00rootroot00000000000000.. _tornado_change_stream_example: Tornado Change Stream Example ============================= .. currentmodule:: motor.motor_tornado Watch a collection for changes with :meth:`MotorCollection.watch` and display each change notification on a web page using web sockets. Instructions ------------ Start a MongoDB server on its default port and run this script. Then visit: http://localhost:8888 Open a ``mongo`` shell in the terminal and perform some operations on the "test" collection in the "test" database: .. code-block:: text > use test switched to db test > db.test.insertOne({}) > db.test.updateOne({}, {$set: {x: 1}}) > db.test.deleteOne({}) The application receives each change notification and displays it as JSON on the web page: .. code-block:: text Changes {'documentKey': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')}, 'fullDocument': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')}, 'ns': {'coll': 'test', 'db': 'test'}, 'operationType': 'insert'} {'documentKey': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')}, 'ns': {'coll': 'test', 'db': 'test'}, 'operationType': 'update', 'updateDescription': {'removedFields': [], 'updatedFields': {'x': 1.0}}} {'documentKey': {'_id': ObjectId('5a2a6967ea2dcf7b1c721cfb')}, 'ns': {'coll': 'test', 'db': 'test'}, 'operationType': 'delete'} Display change notifications over a web socket ---------------------------------------------- .. literalinclude:: tornado_change_stream_example.py :language: python3 motor-3.4.0/doc/examples/tornado_change_stream_templates/000077500000000000000000000000001460060525600236515ustar00rootroot00000000000000motor-3.4.0/doc/examples/tornado_change_stream_templates/index.html000066400000000000000000000023241460060525600256470ustar00rootroot00000000000000

    Changes

    {% for change in changes %} {% raw change['html'] %} {% end %}
    motor-3.4.0/doc/features.rst000066400000000000000000000025731460060525600160060ustar00rootroot00000000000000============== Motor Features ============== .. currentmodule:: motor.motor_tornado Non-Blocking ============ Motor is an asynchronous driver for MongoDB. It can be used from Tornado_ or asyncio_ applications. Motor never blocks the event loop while connecting to MongoDB or performing I/O. .. _Tornado: http://tornadoweb.org/ .. _asyncio: https://docs.python.org/3/library/asyncio.html 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`` =============================== The :mod:`tornado.gen` module lets you use coroutines to simplify asynchronous code. Motor methods return Futures that are convenient to use with coroutines. Configurable IOLoops ==================== Motor supports Tornado applications with multiple :class:`IOLoops `. Pass the ``io_loop`` argument to :class:`MotorClient` to configure the loop for a client instance. Streams Static Files from GridFS ================================ Motor can stream data from `GridFS `_ to a Tornado :class:`~tornado.web.RequestHandler` using :meth:`~MotorGridOut.stream_to_handler` or the :class:`~motor.web.GridFSHandler` class. It can also serve GridFS data with aiohttp using the :class:`~motor.aiohttp.AIOHTTPGridFS` class. motor-3.4.0/doc/index.rst000066400000000000000000000057511460060525600153000ustar00rootroot00000000000000Motor: Asynchronous Python driver for MongoDB ============================================= .. image:: _static/motor.png :align: center About ----- Motor presents a coroutine-based API for non-blocking access to MongoDB from Tornado_ or asyncio_. The `source is on GitHub `_ 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* Install with:: $ python -m pip install motor .. _Tornado: http://tornadoweb.org/ .. _asyncio: https://docs.python.org/3/library/asyncio.html Getting Help ------------ If you're having trouble or have questions about Motor, ask your question on our `MongoDB Community Forum `_. You may also want to consider a `commercial support subscription `_. Once you get an answer, it'd be great if you could work it back into this documentation and contribute! Issues ------ All issues should be reported (and can be tracked / voted for / commented on) at the main `MongoDB JIRA bug tracker `_, in the "Motor" project. Feature Requests / Feedback --------------------------- Use our `feedback engine `_ to send us feature requests and general feedback about PyMongo. Contributing ------------ **Motor** has a large :doc:`community ` and contributions are always encouraged. Contributions can be as simple as minor tweaks to this documentation. To contribute, fork the project on `GitHub `_ and send a pull request. Changes ------- See the :doc:`changelog` for a full list of changes to Motor. Contents -------- .. toctree:: :maxdepth: 1 differences features installation requirements configuration tutorial-tornado tutorial-asyncio examples/index changelog migrate-to-motor-2 migrate-to-motor-3 developer-guide contributors Classes ------- .. toctree:: api-tornado/index api-asyncio/index .. getting the caption italicized with a hyperlink in it requires some RST hackage *Logo by* |musho|_ .. _musho: http://whimsyload.com .. |musho| replace:: *Musho Rodney Alan Greenblat* motor-3.4.0/doc/installation.rst000066400000000000000000000040061460060525600166620ustar00rootroot00000000000000Installation ============ Install Motor from PyPI_ with pip_:: $ python3 -m pip install motor Pip automatically installs Motor's prerequisite packages. See :doc:`requirements`. To install Motor from sources, you can clone its git repository and do:: $ python3 -m pip install . Dependencies ------------ Motor works in all the environments officially supported by Tornado or by asyncio. It requires: * Unix (including macOS) or Windows. * PyMongo_ >=4.1,<5 * Python 3.7+ 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 `_ for details about compatibility. .. _PyPI: http://pypi.python.org/pypi/motor .. _pip: http://pip-installer.org .. _PyMongo: http://pypi.python.org/pypi/pymongo/ motor-3.4.0/doc/make.bat000066400000000000000000000057751460060525600150520ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation set SPHINXBUILD=sphinx-build set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Motor.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Motor.ghc goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end motor-3.4.0/doc/migrate-to-motor-2.rst000066400000000000000000000165301460060525600175330ustar00rootroot00000000000000Motor 2.0 Migration Guide ========================= .. currentmodule:: motor.motor_tornado Motor 2.0 brings a number of changes to Motor 1.0's API. The major version is required in order to update the session API to support multi-document transactions, introduced in MongoDB 4.0; this feature is so valuable that it motivated me to make the breaking change and bump the version number to 2.0. Since this is the first major version number in almost two years, it removes a large number of APIs that have been deprecated in the time since Motor 1.0. Follow this guide to migrate an existing application that had used Motor 1.x. Check compatibility ------------------- Read the :doc:`requirements` page and ensure your MongoDB server and Python interpreter are compatible, and your Tornado version if you are using Tornado. If you use aiohttp, upgrade to at least 3.0. Upgrade to Motor 1.3 -------------------- The first step in migrating to Motor 2.0 is to upgrade to at least Motor 1.3. If your project has a requirements.txt file, add the line:: motor >= 1.3, < 2.0 Enable Deprecation Warnings --------------------------- Starting with Motor 1.3, :exc:`DeprecationWarning` is raised by most methods removed in Motor 2.0. Make sure you enable runtime warnings to see where deprecated functions and methods are being used in your application:: python -Wd Warnings can also be changed to errors:: python -Wd -Werror Migrate from deprecated APIs ---------------------------- The following features are deprecated by PyMongo and scheduled for removal; they are now deleted from Motor: - ``MotorClient.kill_cursors`` and ``close_cursor``. Allow :class:`MotorCursor` to handle its own cleanup. - ``MotorClient.get_default_database``. Call :meth:`MotorClient.get_database` with a database name of ``None`` for the same effect. - ``MotorDatabase.add_son_manipulator``. Transform documents to and from their MongoDB representations in your application code instead. - The server command ``getLastError`` and related commands are deprecated, their helper functions are deleted from Motor: - ``MotorDatabase.last_status`` - ``MotorDatabase.error`` - ``MotorDatabase.previous_error`` - ``MotorDatabase.reset_error_history`` Use acknowledged writes and rely on Motor to raise exceptions. - The server command ``parallelCollectionScan`` is deprecated and ``MotorCollection.parallel_scan`` is removed. Use a regular :meth:`MotorCollection.find` cursor. - ``MotorClient.database_names``. Use :meth:`~MotorClient.list_database_names`. - ``MotorDatabase.eval``. The server command is deprecated but still available with ``MotorDatabase.command("eval", ...)``. - ``MotorDatabase.group``. The server command is deprecated but still available with ``MotorDatabase.command("group", ...)``. - ``MotorDatabase.authenticate`` and ``MotorDatabase.logout``. Add credentials to the URI or ``MotorClient`` options instead of calling ``authenticate``. To authenticate as multiple users on the same database, instead of using ``authenticate`` and ``logout`` use a separate client for each user. - ``MotorCollection.initialize_unordered_bulk_op``, ``initialize_unordered_bulk_op``, and ``MotorBulkOperationBuilder``. Use :meth:`MotorCollection.bulk_write``, see :ref:`Bulk Writes Tutorial `. - ``MotorCollection.count``. Use :meth:`~MotorCollection.count_documents` or :meth:`~MotorCollection.estimated_document_count`. - ``MotorCollection.ensure_index``. Use :meth:`MotorCollection.create_indexes`. - Deprecated write methods have been deleted from :class:`MotorCollection`. - ``save`` and ``insert``: Use :meth:`~MotorCollection.insert_one` or :meth:`~MotorCollection.insert_many`. - ``update``: Use :meth:`~MotorCollection.update_one`, :meth:`~MotorCollection.update_many`, or :meth:`~MotorCollection.replace_one`. - ``remove``: Use :meth:`~MotorCollection.delete_one` or :meth:`~MotorCollection.delete_many`. - ``find_and_modify``: Use :meth:`~MotorCollection.find_one_and_update`, :meth:`~MotorCollection.find_one_and_replace`, or :meth:`~MotorCollection.find_one_and_delete`. - ``MotorCursor.count`` and ``MotorGridOutCursor.count``. Use :meth:`MotorCollection.count_documents` or :meth:`MotorCollection.estimated_document_count`. Migrate from the original callback API -------------------------------------- Motor was first released before Tornado had introduced Futures, generator-based coroutines, and the ``yield`` syntax, and long before the async features developed during Python 3's career. Therefore Motor's original asynchronous API used callbacks: .. code-block:: python3 def callback(result, error): if error: print(error) else: print(result) collection.find_one({}, callback=callback) Callbacks have been largely superseded by a Futures API intended for use with coroutines, see :doc:`tutorial-tornado`. You can still use callbacks with Motor when appropriate but you must add the callback to a Future instead of passing it as a parameter: .. code-block:: python3 def callback(future): try: result = future.result() print(result) except Exception as exc: print(exc) future = collection.find_one({}) future.add_done_callback(callback) The :meth:`~asyncio.Future.add_done_callback` call can be placed on the same line: .. code-block:: python3 collection.find_one({}).add_done_callback(callback) In almost all cases the modern coroutine API is more readable and provides better exception handling: .. code-block:: python3 async def do_find(): try: result = await collection.find_one({}) print(result) except Exception as exc: print(exc) Upgrade to Motor 2.0 -------------------- Once your application runs without deprecation warnings with Motor 1.3, upgrade to Motor 2.0. Update any calls in your code to :meth:`MotorClient.start_session` or :meth:`~pymongo.client_session.ClientSession.end_session` to handle the following change. :meth:`MotorClient.start_session` is a coroutine ------------------------------------------------ In the past, you could use a client session like: .. code-block:: python3 session = client.start_session() doc = await client.db.collection.find_one({}, session=session) session.end_session() Or: .. code-block:: python3 with client.start_session() as session: doc = client.db.collection.find_one({}, session=session) To support multi-document transactions, in Motor 2.0 :meth:`MotorClient.start_session` is a coroutine, not a regular method. It must be used like ``await client.start_session()`` or ``async with await client.start_session()``. The coroutine now returns a new class :class:`~motor.motor_tornado.MotorClientSession`, not PyMongo's :class:`~pymongo.client_session.ClientSession`. The ``end_session`` method on the returned :class:`~motor.motor_tornado.MotorClientSession` is also now a coroutine instead of a regular method. Use it like: .. code-block:: python3 session = await client.start_session() doc = await client.db.collection.find_one({}, session=session) await session.end_session() Or: .. code-block:: python3 async with client.start_session() as session: doc = await client.db.collection.find_one({}, session=session) motor-3.4.0/doc/migrate-to-motor-3.rst000066400000000000000000000437731460060525600175450ustar00rootroot00000000000000Motor 3.0 Migration Guide ========================= .. 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 Warnings can also be changed to errors:: python -Wd -Werror 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 `_. 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:`~bson.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:`~bson.codec_options.TypeCodec` and :class:`~bson.codec_options.TypeRegistry` APIs may be a suitable alternative. For more information, see the :external:pymongo:doc:`custom type example `. 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:`~bson.codec_options.CodecOptions`, :class:`~bson.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. See :ref:`handling-uuid-data-example` for details. Upgrade to Motor 3.0 -------------------- Once your application runs without deprecation warnings with Motor 2.5, upgrade to Motor 3.0. motor-3.4.0/doc/mongo_extensions.py000066400000000000000000000056061460060525600174060ustar00rootroot00000000000000# Copyright 2009-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. """MongoDB specific extensions to Sphinx.""" from docutils import nodes from docutils.parsers import rst from sphinx import addnodes class mongodoc(nodes.Admonition, nodes.Element): pass class mongoref(nodes.reference): pass def visit_mongodoc_node(self, node): self.visit_admonition(node, "seealso") def depart_mongodoc_node(self, node): self.depart_admonition(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)) def depart_mongoref_node(self, node): self.body.append("") if not isinstance(node.parent, nodes.TextElement): self.body.append("\n") class MongodocDirective(rst.Directive): has_content = True required_arguments = 0 optional_arguments = 0 final_argument_whitespace = False option_spec = {} def run(self): node = mongodoc() title = "The MongoDB documentation on" node += nodes.title(title, title) self.state.nested_parse(self.content, self.content_offset, node) return [node] def process_mongodoc_nodes(app, doctree, fromdocname): for node in doctree.traverse(mongodoc): anchor = None for name in node.parent.parent.traverse(addnodes.desc_signature): anchor = name["ids"][0] break if not anchor: for name in node.parent.traverse(nodes.section): anchor = name["ids"][0] break for para in node.traverse(nodes.paragraph): tag = str(list(para.traverse())[1]) link = mongoref("", "") link["refuri"] = "http://dochub.mongodb.org/core/%s" % tag link["name"] = anchor link.append(nodes.emphasis(tag, tag)) new_para = nodes.paragraph() new_para += link node.replace(para, new_para) 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_directive("mongodoc", MongodocDirective) app.connect("doctree-resolved", process_mongodoc_nodes) motor-3.4.0/doc/motor_extensions.py000066400000000000000000000152061460060525600174240ustar00rootroot00000000000000# Copyright 2012-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. """Motor specific extensions to Sphinx.""" 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.util.inspect import safe_getattr import motor import motor.core # This is a place to store info while parsing, to be used before generating. motor_info = {} def has_node_of_type(root, klass): if isinstance(root, klass): return True for child in root.children: # noqa: SIM110 if has_node_of_type(child, klass): return True return False def find_by_path(root, classes): if not classes: return [root] _class = classes[0] rv = [] for child in root.children: if isinstance(child, _class): rv.extend(find_by_path(child, classes[1:])) return rv docstring_warnings = [] def maybe_warn_about_code_block(name, content_node): if has_node_of_type(content_node, (literal_block, doctest_block)): docstring_warnings.append(name) def has_coro_annotation(signature_node): try: return "coroutine" in signature_node[0][0] except IndexError: return False def process_motor_nodes(app, doctree): # Search doctree for Motor's methods and attributes whose docstrings were # copied from PyMongo, and fix them up for Motor: # 1. Add a 'coroutine' annotation to the beginning of the declaration. # 2. Remove all version annotations like "New in version 2.0" since # PyMongo's version numbers are meaningless in Motor's docs. # 3. Remove "seealso" directives that reference PyMongo's docs. # # We do this here, rather than by registering a callback to Sphinx's # '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"): signature_node = find_by_path(objnode, [desc_signature])[0] name = ".".join([signature_node["module"], signature_node["fullname"]]) 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"]: maybe_warn_about_code_block(name, desc_content_node) if obj_motor_info["is_async_method"]: # noqa: SIM102 # 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"] ) signature_node.insert(0, coro_annotation) 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]) for version_node in version_nodes: version_node.parent.remove(version_node) # Remove all "seealso" directives that contain :doc: # references from PyMongo's docs seealso_nodes = find_by_path(desc_content_node, [seealso]) for seealso_node in seealso_nodes: if 'reftype="doc"' in str(seealso_node): seealso_node.parent.remove(seealso_node) def get_motor_attr(motor_class, name, *defargs): """If any Motor attributes can't be accessed, grab the equivalent PyMongo attribute. While we're at it, store some info about each attribute in the global motor_info dict. """ attr = safe_getattr(motor_class, name, *defargs) # Store some info for process_motor_nodes() full_name = f"{motor_class.__module__}.{motor_class.__name__}.{name}" full_name_legacy = f"motor.{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) if is_async_method or is_cursor_method: pymongo_method = getattr(motor_class.__delegate_class__, attr.pymongo_method_name) else: pymongo_method = None # attr.doc is set by statement like 'error = AsyncRead(doc="OBSOLETE")'. 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, } return attr pymongo_ref_pat = re.compile(r":doc:`(.*?)`", re.MULTILINE) def _sub_pymongo_ref(match): ref = match.group(1) return ":doc:`%s`" % ref.lstrip("/") def process_motor_docstring(app, what, name, obj, options, lines): if name in motor_info and motor_info[name].get("is_pymongo_docstring"): joined = "\n".join(lines) subbed = pymongo_ref_pat.sub(_sub_pymongo_ref, joined) lines[:] = subbed.split("\n") def build_finished(app, exception): if not exception and docstring_warnings: print("PyMongo docstrings with code blocks that need update:") for name in sorted(docstring_warnings): print(name) 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} motor-3.4.0/doc/pydoctheme/000077500000000000000000000000001460060525600155705ustar00rootroot00000000000000motor-3.4.0/doc/pydoctheme/static/000077500000000000000000000000001460060525600170575ustar00rootroot00000000000000motor-3.4.0/doc/pydoctheme/static/pydoctheme.css000066400000000000000000000056441460060525600217430ustar00rootroot00000000000000@import url("classic.css"); body { background-color: white; margin-left: 1em; margin-right: 1em; } div.related { margin-bottom: 1.2em; padding: 0.5em 0; border-top: 1px solid #ccc; margin-top: 0.5em; } div.related a:hover { color: #0095C4; } div.related:first-child { border-top: 0; border-bottom: 1px solid #ccc; } div.sphinxsidebar { background-color: #eeeeee; border-radius: 5px; line-height: 130%; font-size: smaller; } div.sphinxsidebar h3, div.sphinxsidebar h4 { margin-top: 1.5em; } div.sphinxsidebarwrapper > h3:first-child { margin-top: 0.2em; } div.sphinxsidebarwrapper > ul > li > ul > li { margin-bottom: 0.4em; } div.sphinxsidebar a:hover { color: #0095C4; } div.sphinxsidebar input { font-family: 'Lucida Grande',Arial,sans-serif; border: 1px solid #999999; font-size: smaller; border-radius: 3px; } div.sphinxsidebar input[type=text] { max-width: 150px; } /* override an apparently misguided "hyphens: auto" style in Sphinx basic.css */ div.body p, div.body dd, div.body li, div.body blockquote { -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; } div.body { padding: 0 0 0 1.2em; } div.body p { line-height: 140%; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { margin: 0; border: 0; padding: 0.3em 0; } div.body hr { border: 0; background-color: #ccc; height: 1px; } div.body pre { border-radius: 3px; border: 1px solid #ac9; } div.body div.admonition, div.body div.impl-detail { border-radius: 3px; } div.body div.impl-detail > p { margin: 0; } div.body div.seealso { border: 1px solid #dddd66; } div.body a { color: #0072aa; } div.body a:visited { color: #6363bb; } div.body a:hover { color: #00B0E4; } tt, code, pre { font-family: monospace, sans-serif; font-size: 96.5%; } div.body tt, div.body code { border-radius: 3px; } div.body tt.descname, div.body code.descname { font-size: 120%; } div.body tt.xref, div.body a tt, div.body code.xref, div.body a code { font-weight: normal; } .deprecated { border-radius: 3px; } table.docutils { border: 1px solid #ddd; min-width: 20%; border-radius: 3px; margin-top: 10px; margin-bottom: 10px; } table.docutils td, table.docutils th { border: 1px solid #ddd !important; border-radius: 3px; } table p, table li { text-align: left !important; } table.docutils th { background-color: #eee; padding: 0.3em 0.5em; } table.docutils td { background-color: white; padding: 0.3em 0.5em; } table.footnote, table.footnote td { border: 0 !important; } div.footer { line-height: 150%; margin-top: -2em; text-align: right; width: auto; margin-right: 10px; } div.footer a:hover { color: #0095C4; } .refcount { color: #060; } .stableabi { color: #229; } motor-3.4.0/doc/pydoctheme/theme.conf000066400000000000000000000010231460060525600175350ustar00rootroot00000000000000[theme] inherit = classic stylesheet = pydoctheme.css pygments_style = sphinx [options] bodyfont = 'Lucida Grande', Arial, sans-serif headfont = 'Lucida Grande', Arial, sans-serif footerbgcolor = white footertextcolor = #555555 relbarbgcolor = white relbartextcolor = #666666 relbarlinkcolor = #444444 sidebarbgcolor = white sidebartextcolor = #444444 sidebarlinkcolor = #444444 bgcolor = white textcolor = #222222 linkcolor = #0090c0 visitedlinkcolor = #00608f headtextcolor = #1a1a1a headbgcolor = white headlinkcolor = #aaaaaa motor-3.4.0/doc/requirements.rst000066400000000000000000000233711460060525600167120ustar00rootroot00000000000000Requirements ============ The current version of Motor requires: * CPython 3.7 and later. * PyMongo_ 4.5 and later. Motor can integrate with either Tornado or asyncio. 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/ .. _compatibility-matrix: Compatibility Matrix -------------------- 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+ | +-------------------+-----------------+ Motor and MongoDB ````````````````` +---------------------------------------------------------------------------------------------------+ | MongoDB Version | +=====================+=====+=====+=====+=====+=====+=====+=====+=====+=====+=====+=====+=====+=====+ | | 2.2 | 2.4 | 2.6 | 3.0 | 3.2 | 3.4 | 3.6 | 4.0 | 4.2 | 4.4 | 5.0 | 6.0 | 7.0 | +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | Motor Version | 1.0 | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.1 | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.2 |**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.3 |**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.0 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.1 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.2 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.3 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.4 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.5 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.0 |**N**|**N**|**N**|**N**|**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.1 |**N**|**N**|**N**|**N**|**N**|**N**| Y | Y | Y | Y | Y | Y |**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.2 |**N**|**N**|**N**|**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | Y | +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.3 |**N**|**N**|**N**|**N**|**N**|**N**| 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. 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://mongodb.com/docs/drivers/pymongo#mongodb-compatibility Motor and Tornado ````````````````` Where "N" appears in this matrix, the versions of Motor and Tornado are known to be incompatible, or have not been tested together. +---------------------------------------------+ | Tornado Version | +=====================+=====+=====+=====+=====+ | | 3.x | 4.x | 5.x | 6.x | +---------------+-----+-----+-----+-----+-----+ | Motor Version | 1.0 | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+ | | 1.1 | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+ | | 1.2 |**N**| Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+ | | 1.3 |**N**| Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+ | | 2.0 |**N**| Y | Y |**N**| +---------------+-----+-----+-----+-----+-----+ | | 2.1 |**N**| Y | Y | Y | +---------------+-----+-----+-----+-----+-----+ | | 2.2 |**N**|**N**| Y | Y | +---------------+-----+-----+-----+-----+-----+ | | 2.3 |**N**|**N**| Y | Y | +---------------+-----+-----+-----+-----+-----+ | | 3.0 |**N**|**N**|**N**| Y | +---------------+-----+-----+-----+-----+-----+ | | 3.1 |**N**|**N**|**N**| Y | +---------------+-----+-----+-----+-----+-----+ | | 3.2 |**N**|**N**|**N**| Y | +---------------+-----+-----+-----+-----+-----+ | | 3.3 |**N**|**N**|**N**| Y | +---------------+-----+-----+-----+-----+-----+ Motor and Python ```````````````` 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 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 deprecated support for Python 3.5. Motor 3.0 dropped support for Pythons older than 3.7. Motor 3.1.1 added support for Python 3.11. Motor 3.3 added support for Python 3.12. +---------------------------------------------------------------+ | Python Version | +=====================+=====+=====+=====+=====+=====+=====+=====+ | | 3.6 | 3.7 | 3.8 | 3.9 | 3.10| 3.11| 3.12| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | Motor Version | 1.0 | Y |**N**|**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.1 | Y |**N**|**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.2 | Y | Y |**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.3 | Y | Y |**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.0 | Y | Y |**N**|**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.1 | Y | Y | Y |**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.2 | Y | Y | Y |**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.3 | Y | Y | Y |**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.4 | Y | Y | Y | Y |**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.5 | Y | Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.0 |**N**| Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.1 |**N**| Y | Y | Y | Y |**Y**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.2 |**N**| Y | Y | Y | Y | Y |**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ | | 3.3 |**N**| Y | Y | Y | Y | Y |**Y**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+ Not Supported ------------- Motor does not support Jython or IronPython. motor-3.4.0/doc/static/000077500000000000000000000000001460060525600147165ustar00rootroot00000000000000motor-3.4.0/doc/static/sidebar.js000066400000000000000000000142141460060525600166670ustar00rootroot00000000000000/* * sidebar.js * ~~~~~~~~~~ * * This script makes the Sphinx sidebar collapsible and implements intelligent * scrolling. * * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds in * .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton used to * collapse and expand the sidebar. * * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden and the * width of the sidebar and the margin-left of the document are decreased. * When the sidebar is expanded the opposite happens. This script saves a * per-browser/per-session cookie used to remember the position of the sidebar * among the pages. Once the browser is closed the cookie is deleted and the * position reset to the default (expanded). * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ $(function() { // global elements used by the functions. // the 'sidebarbutton' element is defined as global after its // creation, in the add_sidebar_button function var jwindow = $(window); var jdocument = $(document); var bodywrapper = $('.bodywrapper'); var sidebar = $('.sphinxsidebar'); var sidebarwrapper = $('.sphinxsidebarwrapper'); // original margin-left of the bodywrapper and width of the sidebar // with the sidebar expanded var bw_margin_expanded = bodywrapper.css('margin-left'); var ssb_width_expanded = sidebar.width(); // margin-left of the bodywrapper and width of the sidebar // with the sidebar collapsed var bw_margin_collapsed = '.8em'; var ssb_width_collapsed = '.8em'; // colors used by the current theme var dark_color = '#AAAAAA'; var light_color = '#CCCCCC'; function get_viewport_height() { if (window.innerHeight) return window.innerHeight; else return jwindow.height(); } function sidebar_is_collapsed() { return sidebarwrapper.is(':not(:visible)'); } function toggle_sidebar() { if (sidebar_is_collapsed()) expand_sidebar(); else collapse_sidebar(); // adjust the scrolling of the sidebar scroll_sidebar(); } function collapse_sidebar() { sidebarwrapper.hide(); sidebar.css('width', ssb_width_collapsed); bodywrapper.css('margin-left', bw_margin_collapsed); sidebarbutton.css({ 'margin-left': '0', 'height': bodywrapper.height(), 'border-radius': '5px' }); sidebarbutton.find('span').text('»'); sidebarbutton.attr('title', _('Expand sidebar')); document.cookie = 'sidebar=collapsed'; } function expand_sidebar() { bodywrapper.css('margin-left', bw_margin_expanded); sidebar.css('width', ssb_width_expanded); sidebarwrapper.show(); sidebarbutton.css({ 'margin-left': ssb_width_expanded-12, 'height': bodywrapper.height(), 'border-radius': '0 5px 5px 0' }); sidebarbutton.find('span').text('«'); sidebarbutton.attr('title', _('Collapse sidebar')); //sidebarwrapper.css({'padding-top': // Math.max(window.pageYOffset - sidebarwrapper.offset().top, 10)}); document.cookie = 'sidebar=expanded'; } function add_sidebar_button() { sidebarwrapper.css({ 'float': 'left', 'margin-right': '0', 'width': ssb_width_expanded - 28 }); // create the button sidebar.append( '
    «
    ' ); var sidebarbutton = $('#sidebarbutton'); // find the height of the viewport to center the '<<' in the page var viewport_height = get_viewport_height(); var sidebar_offset = sidebar.offset().top; var sidebar_height = Math.max(bodywrapper.height(), sidebar.height()); sidebarbutton.find('span').css({ 'display': 'block', 'position': 'fixed', 'top': Math.min(viewport_height/2, sidebar_height/2 + sidebar_offset) - 10 }); sidebarbutton.click(toggle_sidebar); sidebarbutton.attr('title', _('Collapse sidebar')); sidebarbutton.css({ 'border-radius': '0 5px 5px 0', 'color': '#444444', 'background-color': '#CCCCCC', 'font-size': '1.2em', 'cursor': 'pointer', 'height': sidebar_height, 'padding-top': '1px', 'padding-left': '1px', 'margin-left': ssb_width_expanded - 12 }); sidebarbutton.hover( function () { $(this).css('background-color', dark_color); }, function () { $(this).css('background-color', light_color); } ); } function set_position_from_cookie() { if (!document.cookie) return; var items = document.cookie.split(';'); for(var k=0; k wintop && curbot > winbot) { sidebarwrapper.css('top', $u.max([wintop - offset - 10, 0])); } else if (curtop < wintop && curbot < winbot) { sidebarwrapper.css('top', $u.min([winbot - sidebar_height - offset - 20, jdocument.height() - sidebar_height - 200])); } } } jwindow.scroll(scroll_sidebar); }); motor-3.4.0/doc/tutorial-asyncio.rst000066400000000000000000000416071460060525600174770ustar00rootroot00000000000000.. currentmodule:: motor.motor_asyncio Tutorial: Using Motor With :mod:`asyncio` ========================================= .. 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. .. testsetup:: before-inserting-2000-docs import pymongo import motor.motor_asyncio import asyncio client = motor.motor_asyncio.AsyncIOMotorClient() db = client.test_database .. testsetup:: after-inserting-2000-docs 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)] ) .. testcleanup:: * import pymongo pymongo.MongoClient().test_database.test_collection.delete_many({}) A guide to using MongoDB and asyncio with Motor. .. contents:: Tutorial Prerequisites ---------------------- You can learn about MongoDB with the `MongoDB Tutorial`_ before you learn Motor. Using Python 3.5 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 `_ MongoDB, you can start it like so: .. code-block:: bash $ mongod .. _pip: http://www.pip-installer.org/en/latest/installing.html .. _MongoDB Tutorial: https://mongodb.com/docs/manual/tutorial/getting-started/ Object Hierarchy ---------------- Motor, like PyMongo, represents data with a 4-level object hierarchy: * :class:`~motor.motor_asyncio.AsyncIOMotorClient` represents a mongod process, or a cluster of them. You explicitly create one of these client objects, connect it to a running mongod or mongods, and use it for the lifetime of your application. * :class:`~motor.motor_asyncio.AsyncIOMotorDatabase`: Each mongod has a set of databases (distinct sets of data files on disk). You can get a reference to a database from a client. * :class:`~motor.motor_asyncio.AsyncIOMotorCollection`: A database has a set of collections, which contain documents; you get a reference to a collection from a database. * :class:`~motor.motor_asyncio.AsyncIOMotorCursor`: Executing :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find` on an :class:`~motor.motor_asyncio.AsyncIOMotorCollection` gets an :class:`~motor.motor_asyncio.AsyncIOMotorCursor`, which represents the set of documents matching a query. Creating a Client ----------------- You typically create a single instance of :class:`~motor.motor_asyncio.AsyncIOMotorClient` at the time your application starts up. .. doctest:: before-inserting-2000-docs >>> import motor.motor_asyncio >>> client = motor.motor_asyncio.AsyncIOMotorClient() This connects to a ``mongod`` listening on the default host and port. You can specify the host and port like: .. doctest:: before-inserting-2000-docs >>> 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") 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/ Getting a Database ------------------ A single instance of MongoDB can support multiple independent `databases `_. 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"] Creating a reference to a database does no I/O and does not require an ``await`` expression. Getting a Collection -------------------- A `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: .. doctest:: before-inserting-2000-docs >>> 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. Inserting a Document -------------------- As in PyMongo, Motor represents MongoDB documents with Python dictionaries. To store a document in MongoDB, call :meth:`~AsyncIOMotorCollection.insert_one` in an ``await`` expression: .. doctest:: before-inserting-2000-docs >>> async def do_insert(): ... document = {"key": "value"} ... result = await db.test_collection.insert_one(document) ... print("result %s" % repr(result.inserted_id)) ... >>> >>> import asyncio >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_insert()) result ObjectId('...') .. mongodoc:: insert .. doctest:: before-inserting-2000-docs :hide: >>> # Clean up from previous insert >>> pymongo.MongoClient().test_database.test_collection.delete_many({}) DeleteResult({'n': 1, 'ok': 1.0}, acknowledged=True) 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),)) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_insert()) inserted 2000 docs 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 less than 1: .. doctest:: after-inserting-2000-docs >>> async def do_find_one(): ... document = await db.test_collection.find_one({"i": {"$lt": 1}}) ... pprint.pprint(document) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find_one()) {'_id': ObjectId('...'), 'i': 0} The result is a dictionary matching the one that we inserted previously. .. note:: The returned document contains an ``"_id"``, which was automatically added on insert. .. mongodoc:: find Querying for More Than One Document ----------------------------------- Use :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find` to query for a set of documents. :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.find` does no I/O and does not require an ``await`` expression. It merely creates an :class:`~motor.motor_asyncio.AsyncIOMotorCursor` instance. The query is actually executed on the server when you call :meth:`~motor.motor_asyncio.AsyncIOMotorCursor.to_list` or execute an ``async for`` loop. To find all documents with "i" less than 5: .. doctest:: after-inserting-2000-docs >>> async def do_find(): ... cursor = db.test_collection.find({"i": {"$lt": 5}}).sort("i") ... for document in await cursor.to_list(length=100): ... pprint.pprint(document) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 3} {'_id': ObjectId('...'), 'i': 4} A ``length`` argument is required when you call ``to_list`` to prevent Motor from buffering an unlimited number of documents. ``async for`` ~~~~~~~~~~~~~ You can handle one document at a time in an ``async for`` loop: .. doctest:: after-inserting-2000-docs >>> async def do_find(): ... c = db.test_collection ... async for document in c.find({"i": {"$lt": 2}}): ... pprint.pprint(document) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} 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}}) ... # Modify the query before iterating ... cursor.sort("i", -1).skip(1).limit(2) ... async for document in cursor: ... pprint.pprint(document) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 1} The cursor does not actually retrieve each document from the server individually; it gets documents efficiently in `large batches`_. .. _`large batches`: https://mongodb.com/docs/manual/tutorial/iterate-a-cursor/#cursor-batches Counting Documents ------------------ Use :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.count_documents` to determine the number of documents in a collection, or the number of documents that match a query: .. doctest:: after-inserting-2000-docs >>> async def do_count(): ... n = await db.test_collection.count_documents({}) ... print("%s documents in collection" % n) ... n = await db.test_collection.count_documents({"i": {"$gt": 1000}}) ... print("%s documents where i > 1000" % n) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_count()) 2000 documents in collection 999 documents where i > 1000 Updating Documents ------------------ :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.replace_one` changes a document. It requires two parameters: a *query* that specifies which document to replace, and a replacement document. The query follows the same syntax as for :meth:`find` or :meth:`find_one`. To replace a document: .. doctest:: after-inserting-2000-docs >>> 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)) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_replace()) found document: {'_id': ObjectId('...'), 'i': 50} replaced 1 document document is now {'_id': ObjectId('...'), 'key': 'value'} You can see that :meth:`replace_one` replaced everything in the old document except its ``_id`` with the new document. Use :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.update_one` with MongoDB's modifier operators to update part of a document and leave the rest intact. We'll find the document whose "i" is 51 and use the ``$set`` operator to set "key" to "value": .. doctest:: after-inserting-2000-docs >>> 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)) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_update()) updated 1 document document is now {'_id': ObjectId('...'), 'i': 51, 'key': 'value'} "key" is set to "value" and "i" is still 51. :meth:`update_one` only affects the first document it finds, you can update all of them with :meth:`update_many`:: await coll.update_many({'i': {'$gt': 100}}, {'$set': {'key': 'value'}}) .. mongodoc:: update 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. .. doctest:: after-inserting-2000-docs >>> 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({}))) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_delete_many()) 1999 documents before calling delete_many() 1000 documents after .. mongodoc:: remove Commands -------- All operations on MongoDB are implemented internally as commands. Run them using the :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command` method on :class:`~motor.motor_asyncio.AsyncIOMotorDatabase`:: .. doctest:: after-inserting-2000-docs >>> from bson import SON >>> async def use_distinct_command(): ... response = await db.command(SON([("distinct", "test_collection"), ("key", "i")])) ... >>> loop = client.get_io_loop() >>> loop.run_until_complete(use_distinct_command()) Since the order of command parameters matters, don't use a Python dict to pass the command's parameters. Instead, make a habit of using :class:`bson.SON`, from the ``bson`` module included with PyMongo. Many commands have special helper methods, such as :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.create_collection` or :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.aggregate`, but these are just conveniences atop the basic :meth:`command` method. .. mongodoc:: commands .. _example-web-application-aiohttp: A Web Application With `aiohttp`_ --------------------------------- Let us create a web application using `aiohttp`_, a popular HTTP package for asyncio. Install it with:: python3 -m pip install aiohttp We are going to make a trivial web site with two pages served from MongoDB. To begin: .. literalinclude:: examples/aiohttp_example.py :language: python3 :start-after: setup-start :end-before: setup-end The ``AsyncIOMotorClient`` constructor does not actually connect to MongoDB. The client connects on demand, when you attempt the first operation. We create it and assign the "test" database's handle to ``db``. The ``setup_db`` coroutine drops the "pages" collection (plainly, this code is for demonstration purposes), then inserts two documents. Each document's page name is its unique id, and the "body" field is a simple HTML page. Finally, ``setup_db`` returns the database handle. We'll use the ``setup_db`` coroutine soon. First, we need a request handler that serves pages from the data we stored in MongoDB. .. literalinclude:: examples/aiohttp_example.py :language: python3 :start-after: handler-start :end-before: handler-end We start the server by running ``setup_db`` and passing the database handle to an :class:`aiohttp.web.Application`: .. literalinclude:: examples/aiohttp_example.py :language: python3 :start-after: main-start :end-before: main-end 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 returns a 404. The complete code is in the Motor repository in ``examples/aiohttp_example.py``. .. _aiohttp: https://aiohttp.readthedocs.io/ See also the :doc:`examples/aiohttp_gridfs_example`. Further Reading --------------- The handful of classes and methods introduced here are sufficient for daily tasks. The API documentation for :class:`~motor.motor_asyncio.AsyncIOMotorClient`, :class:`~motor.motor_asyncio.AsyncIOMotorDatabase`, :class:`~motor.motor_asyncio.AsyncIOMotorCollection`, and :class:`~motor.motor_asyncio.AsyncIOMotorCursor` provides a 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/ motor-3.4.0/doc/tutorial-tornado.rst000066400000000000000000000416001460060525600174710ustar00rootroot00000000000000.. currentmodule:: motor.motor_tornado Tutorial: Using Motor With Tornado ================================== .. 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. .. testsetup:: before-inserting-2000-docs import pymongo import motor import tornado.web from tornado.ioloop import IOLoop from tornado import gen db = motor.motor_tornado.MotorClient().test_database .. testsetup:: after-inserting-2000-docs import pymongo import motor 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)]) .. testcleanup:: * import pymongo pymongo.MongoClient().test_database.test_collection.delete_many({}) A guide to using MongoDB and Tornado with Motor. .. contents:: Tutorial Prerequisites ---------------------- You can learn about MongoDB with the `MongoDB Tutorial`_ before you learn Motor. Install pip_ and then do:: $ pip install tornado motor Once done, the following should run in the Python shell without raising an exception: .. doctest:: >>> import motor.motor_tornado This tutorial also assumes that a MongoDB instance is running on the default host and port. Assuming you have `downloaded and installed `_ MongoDB, you can start it like so: .. code-block:: bash $ mongod .. _pip: http://www.pip-installer.org/en/latest/installing.html .. _MongoDB Tutorial: https://mongodb.com/docs/manual/tutorial/getting-started/ Object Hierarchy ---------------- Motor, like PyMongo, represents data with a 4-level object hierarchy: * :class:`MotorClient` represents a mongod process, or a cluster of them. You explicitly create one of these client objects, connect it to a running mongod or mongods, and use it for the lifetime of your application. * :class:`MotorDatabase`: Each mongod has a set of databases (distinct sets of data files on disk). You can get a reference to a database from a client. * :class:`MotorCollection`: A database has a set of collections, which contain documents; you get a reference to a collection from a database. * :class:`MotorCursor`: Executing :meth:`~MotorCollection.find` on a :class:`MotorCollection` gets a :class:`MotorCursor`, which represents the set of documents matching a query. Creating a Client ----------------- You typically create a single instance of :class:`MotorClient` at the time your application starts up. .. doctest:: before-inserting-2000-docs >>> client = motor.motor_tornado.MotorClient() This connects to a ``mongod`` listening on the default host and port. You can specify the host and port like: .. doctest:: before-inserting-2000-docs >>> 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") 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/ Getting a Database ------------------ A single instance of MongoDB can support multiple independent `databases `_. 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"] Creating a reference to a database does no I/O and does not require an ``await`` expression. Tornado Application Startup Sequence ------------------------------------ Now that we can create a client and get a database, we're ready to start a Tornado application that uses Motor:: db = motor.motor_tornado.MotorClient().test_database application = tornado.web.Application([ (r'/', MainHandler) ], db=db) application.listen(8888) tornado.ioloop.IOLoop.current().start() There are two things to note in this code. First, the ``MotorClient`` constructor doesn't actually connect to the server; the client will initiate a connection when you attempt the first operation. Second, passing the database as the ``db`` keyword argument to ``Application`` makes it available to request handlers:: class MainHandler(tornado.web.RequestHandler): def get(self): db = self.settings['db'] It is a common mistake to create a new client object for every request; **this comes at a dire performance cost**. Create the client when your application starts and reuse that one client for the lifetime of the process, as shown in these examples. The Tornado :class:`~tornado.httpserver.HTTPServer` class's :meth:`start` method is a simple way to fork multiple web servers and use all of your machine's CPUs. However, you must create your ``MotorClient`` after forking:: # Create the application before creating a MotorClient. application = tornado.web.Application([ (r'/', MainHandler) ]) server = tornado.httpserver.HTTPServer(application) server.bind(8888) # Forks one process per CPU. server.start(0) # Now, in each child process, create a MotorClient. application.settings['db'] = MotorClient().test_database IOLoop.current().start() For production-ready, multiple-CPU deployments of Tornado there are better methods than ``HTTPServer.start()``. See Tornado's guide to :doc:`tornado:guide/running`. Getting a Collection -------------------- A `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: .. doctest:: before-inserting-2000-docs >>> 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. Inserting a Document -------------------- As in PyMongo, Motor represents MongoDB documents with Python dictionaries. To store a document in MongoDB, call :meth:`~MotorCollection.insert_one` in an ``await`` expression: .. doctest:: before-inserting-2000-docs >>> async def do_insert(): ... document = {"key": "value"} ... result = await db.test_collection.insert_one(document) ... print("result %s" % repr(result.inserted_id)) ... >>> >>> IOLoop.current().run_sync(do_insert) result ObjectId('...') .. mongodoc:: insert .. doctest:: before-inserting-2000-docs :hide: >>> # Clean up from previous insert >>> pymongo.MongoClient().test_database.test_collection.delete_many({}) DeleteResult({'n': 1, 'ok': 1.0}, acknowledged=True) 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:: >>> for i in range(2000): ... db.test_collection.insert_one({'i': i}) .. Note that the above is NOT a doctest!! In PyMongo this would insert each document in turn using a single socket, but Motor attempts to run all the :meth:`insert_one` operations at once. This requires up to ``max_pool_size`` open sockets connected to MongoDB, which taxes the client and server. To ensure instead that all inserts run in sequence, use ``await``: .. doctest:: before-inserting-2000-docs >>> async def do_insert(): ... for i in range(2000): ... await db.test_collection.insert_one({"i": i}) ... >>> IOLoop.current().run_sync(do_insert) .. seealso:: :doc:`examples/bulk`. .. mongodoc:: insert .. doctest:: before-inserting-2000-docs :hide: >>> # Clean up from previous insert >>> pymongo.MongoClient().test_database.test_collection.delete_many({}) DeleteResult({'n': 2000, 'ok': 1.0}, acknowledged=True) For better performance, insert documents in large batches with :meth:`~MotorCollection.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),)) ... >>> IOLoop.current().run_sync(do_insert) inserted 2000 docs Getting a Single Document With :meth:`~MotorCollection.find_one` ---------------------------------------------------------------- Use :meth:`~MotorCollection.find_one` to get the first document that matches a query. For example, to get a document where the value for key "i" is less than 1: .. doctest:: after-inserting-2000-docs >>> async def do_find_one(): ... document = await db.test_collection.find_one({"i": {"$lt": 1}}) ... pprint.pprint(document) ... >>> IOLoop.current().run_sync(do_find_one) {'_id': ObjectId('...'), 'i': 0} The result is a dictionary matching the one that we inserted previously. The returned document contains an ``"_id"``, which was automatically added on insert. (We use ``pprint`` here instead of ``print`` to ensure the document's key names are sorted the same in your output as ours.) .. mongodoc:: find Querying for More Than One Document ----------------------------------- Use :meth:`~MotorCollection.find` to query for a set of documents. :meth:`~MotorCollection.find` does no I/O and does not require an ``await`` expression. It merely creates an :class:`~MotorCursor` instance. The query is actually executed on the server when you call :meth:`~MotorCursor.to_list` or execute an ``async for`` loop. To find all documents with "i" less than 5: .. doctest:: after-inserting-2000-docs >>> async def do_find(): ... cursor = db.test_collection.find({"i": {"$lt": 5}}).sort("i") ... for document in await cursor.to_list(length=100): ... pprint.pprint(document) ... >>> IOLoop.current().run_sync(do_find) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 3} {'_id': ObjectId('...'), 'i': 4} A ``length`` argument is required when you call ``to_list`` to prevent Motor from buffering an unlimited number of documents. ``async for`` ~~~~~~~~~~~~~ You can handle one document at a time in an ``async for`` loop: .. doctest:: after-inserting-2000-docs >>> async def do_find(): ... c = db.test_collection ... async for document in c.find({"i": {"$lt": 2}}): ... pprint.pprint(document) ... >>> IOLoop.current().run_sync(do_find) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} 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}}) ... # Modify the query before iterating ... cursor.sort("i", -1).skip(1).limit(2) ... async for document in cursor: ... pprint.pprint(document) ... >>> IOLoop.current().run_sync(do_find) {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 1} The cursor does not actually retrieve each document from the server individually; it gets documents efficiently in `large batches`_. .. _`large batches`: https://mongodb.com/docs/manual/tutorial/iterate-a-cursor/#cursor-batches Counting Documents ------------------ Use :meth:`~MotorCollection.count_documents` to determine the number of documents in a collection, or the number of documents that match a query: .. doctest:: after-inserting-2000-docs >>> async def do_count(): ... n = await db.test_collection.count_documents({}) ... print("%s documents in collection" % n) ... n = await db.test_collection.count_documents({"i": {"$gt": 1000}}) ... print("%s documents where i > 1000" % n) ... >>> IOLoop.current().run_sync(do_count) 2000 documents in collection 999 documents where i > 1000 Updating Documents ------------------ :meth:`~MotorCollection.replace_one` changes a document. It requires two parameters: a *query* that specifies which document to replace, and a replacement document. The query follows the same syntax as for :meth:`find` or :meth:`find_one`. To replace a document: .. doctest:: after-inserting-2000-docs >>> 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)) ... >>> IOLoop.current().run_sync(do_replace) found document: {'_id': ObjectId('...'), 'i': 50} replaced 1 document document is now {'_id': ObjectId('...'), 'key': 'value'} You can see that :meth:`replace_one` replaced everything in the old document except its ``_id`` with the new document. Use :meth:`~MotorCollection.update_one` with MongoDB's modifier operators to update part of a document and leave the rest intact. We'll find the document whose "i" is 51 and use the ``$set`` operator to set "key" to "value": .. doctest:: after-inserting-2000-docs >>> 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)) ... >>> IOLoop.current().run_sync(do_update) updated 1 document document is now {'_id': ObjectId('...'), 'i': 51, 'key': 'value'} "key" is set to "value" and "i" is still 51. :meth:`update_one` only affects the first document it finds, you can update all of them with :meth:`update_many`:: await coll.update_many({'i': {'$gt': 100}}, {'$set': {'key': 'value'}}) .. mongodoc:: update Removing Documents ------------------ :meth:`~MotorCollection.delete_one` takes a query with the same syntax as :meth:`~MotorCollection.find`. :meth:`delete_one` immediately removes the first returned matching document. .. doctest:: after-inserting-2000-docs >>> async def do_delete_one(): ... coll = db.test_collection ... n = await coll.count_documents({}) ... print("%s documents before calling delete_one()" % n) ... result = await db.test_collection.delete_one({"i": {"$gte": 1000}}) ... print("%s documents after" % (await coll.count_documents({}))) ... >>> IOLoop.current().run_sync(do_delete_one) 2000 documents before calling delete_one() 1999 documents after :meth:`~MotorCollection.delete_many` takes a query with the same syntax as :meth:`~MotorCollection.find`. :meth:`delete_many` immediately removes all matching documents. .. doctest:: after-inserting-2000-docs >>> async def do_delete_many(): ... 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({}))) ... >>> IOLoop.current().run_sync(do_delete_many) 1999 documents before calling delete_many() 1000 documents after .. mongodoc:: remove Commands -------- All operations on MongoDB are implemented internally as commands. Run them using the :meth:`~motor.motor_tornado.MotorDatabase.command` method on :class:`~motor.motor_tornado.MotorDatabase`:: .. doctest:: after-inserting-2000-docs >>> from bson import SON >>> async def use_distinct_command(): ... response = await db.command(SON([("distinct", "test_collection"), ("key", "i")])) ... >>> IOLoop.current().run_sync(use_distinct_command) Since the order of command parameters matters, don't use a Python dict to pass the command's parameters. Instead, make a habit of using :class:`bson.SON`, from the ``bson`` module included with PyMongo. Many commands have special helper methods, such as :meth:`~MotorDatabase.create_collection` or :meth:`~MotorCollection.aggregate`, but these are just conveniences atop the basic :meth:`command` method. .. mongodoc:: commands Further Reading --------------- The handful of classes and methods introduced here are sufficient for daily tasks. The API documentation for :class:`MotorClient`, :class:`MotorDatabase`, :class:`MotorCollection`, and :class:`MotorCursor` provides a 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/ motor-3.4.0/motor/000077500000000000000000000000001460060525600140225ustar00rootroot00000000000000motor-3.4.0/motor/__init__.py000066400000000000000000000020631460060525600161340ustar00rootroot00000000000000# Copyright 2011-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. """Motor, an asynchronous driver for MongoDB.""" from ._version import get_version_string, version, version_tuple # noqa: F401 """Current version of Motor.""" try: import tornado except ImportError: tornado = None # type:ignore[assignment] 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 motor-3.4.0/motor/_version.py000066400000000000000000000016101460060525600162160ustar00rootroot00000000000000# 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.""" version_tuple = (3, 4, 0) def get_version_string() -> str: if isinstance(version_tuple[-1], str): return ".".join(map(str, version_tuple[:-1])) + version_tuple[-1] return ".".join(map(str, version_tuple)) # type:ignore[unreachable] version = get_version_string() motor-3.4.0/motor/aiohttp/000077500000000000000000000000001460060525600154725ustar00rootroot00000000000000motor-3.4.0/motor/aiohttp/__init__.py000066400000000000000000000223261460060525600176100ustar00rootroot00000000000000# Copyright 2016 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. """Serve GridFS files with Motor and aiohttp. Requires Python 3.5 or later and aiohttp 3.0 or later. See the :doc:`/examples/aiohttp_gridfs_example`. """ import datetime 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" def get_gridfs_file(bucket, filename, request): """Override to choose a GridFS file to serve at a URL. By default, if a URL pattern like ``/fs/{filename}`` is mapped to this :class:`AIOHTTPGridFS`, then the filename portion of the URL is used as the filename, so a request for "/fs/image.png" results in a call to :meth:`.AsyncIOMotorGridFSBucket.open_download_stream_by_name` with "image.png" as the ``filename`` argument. To customize the mapping of path to GridFS file, override ``get_gridfs_file`` and return a :class:`asyncio.Future` that resolves to a :class:`~motor.motor_asyncio.AsyncIOMotorGridOut`. For example, to retrieve the file by ``_id`` instead of filename:: def get_gridfile_by_id(bucket, filename, request): # "filename" is interpreted as _id instead of name. # Return a Future AsyncIOMotorGridOut. return bucket.open_download_stream(file_id=filename) client = AsyncIOMotorClient() gridfs_handler = AIOHTTPGridFS(client.my_database, get_gridfs_file=get_gridfile_by_id) :Parameters: - `bucket`: An :class:`~motor.motor_asyncio.AsyncIOMotorGridFSBucket` - `filename`: A string, the URL portion matching {filename} in the URL pattern - `request`: An :class:`aiohttp.web.Request` """ # A Future AsyncIOMotorGridOut. return bucket.open_download_stream_by_name(filename) def get_cache_time(filename, modified, mime_type): """Override to customize cache control behavior. Return a positive number of seconds to trigger aggressive caching or 0 to mark resource as cacheable, only. 0 is the default. For example, to allow image caching:: def image_cache_time(filename, modified, mime_type): if mime_type.startswith('image/'): return 3600 return 0 client = AsyncIOMotorClient() gridfs_handler = AIOHTTPGridFS(client.my_database, get_cache_time=image_cache_time) :Parameters: - `filename`: A string, the URL portion matching {filename} in the URL pattern - `modified`: A datetime, when the matching GridFS file was created - `mime_type`: The file's type, a string like "application/octet-stream" """ return 0 def set_extra_headers(response, gridout): """Override to modify the response before sending to client. For example, to allow image caching:: def gzip_header(response, gridout): response.headers['Content-Encoding'] = 'gzip' client = AsyncIOMotorClient() gridfs_handler = AIOHTTPGridFS(client.my_database, set_extra_headers=gzip_header) :Parameters: - `response`: An :class:`aiohttp.web.Response` - `gridout`: The :class:`~motor.motor_asyncio.AsyncIOMotorGridOut` we will serve to the client """ def _config_error(request): try: 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 raise aiohttp.web.HTTPInternalServerError(text=msg) from None class AIOHTTPGridFS: """Serve files from `GridFS`_. This class is a :ref:`request handler ` that serves GridFS files, similar to aiohttp's built-in static file server. .. code-block:: python client = AsyncIOMotorClient() gridfs_handler = AIOHTTPGridFS(client.my_database) 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) app_handler = app.make_handler() server = loop.create_server(app_handler, port=80) 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. Pass a custom :func:`get_cache_time` to customize this. :Parameters: - `database`: An :class:`AsyncIOMotorDatabase` - `get_gridfs_file`: Optional override for :func:`get_gridfs_file` - `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/ """ 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 ) self._database = database self._bucket = AsyncIOMotorGridFSBucket(self._database, root_collection) self._get_gridfs_file = get_gridfs_file self._get_cache_time = get_cache_time self._set_extra_headers = set_extra_headers async def __call__(self, request): """Send filepath to client using request.""" try: filename = request.match_info["filename"] except KeyError: _config_error(request) if request.method not in ("GET", "HEAD"): raise aiohttp.web.HTTPMethodNotAllowed( 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 resp = aiohttp.web.StreamResponse() # Get the hash for the GridFS file. checksum = _hash_gridout(gridout) self._set_standard_headers(request.path, resp, gridout, checksum) # Overridable method set_extra_headers. self._set_extra_headers(resp, gridout) # Check the If-Modified-Since, and don't send the result if the # content has not been modified ims_value = request.if_modified_since if ims_value is not None: # If our MotorClient is tz-aware, assume the naive ims_value is in # its time zone. if_since = ims_value.replace(tzinfo=gridout.upload_date.tzinfo) modified = gridout.upload_date.replace(microsecond=0) if if_since >= modified: resp.set_status(304) return resp # Same for Etag etag = request.headers.get("If-None-Match") if etag is not None and etag.strip('"') == checksum: resp.set_status(304) return resp resp.content_length = gridout.length await resp.prepare(request) if request.method == "GET": written = 0 while written < gridout.length: # Reading chunk_size at a time minimizes buffering. chunk = await gridout.read(gridout.chunk_size) await resp.write(chunk) written += len(chunk) return resp def _set_standard_headers(self, path, resp, gridout, checksum): resp.last_modified = gridout.upload_date content_type = gridout.content_type if content_type is None: content_type, encoding = mimetypes.guess_type(path) if content_type: resp.content_type = content_type resp.headers["Etag"] = '"%s"' % checksum # Overridable method get_cache_time. 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) ).strftime("%a, %d %b %Y %H:%M:%S GMT") resp.headers["Cache-Control"] = "max-age=" + str(cache_time) else: resp.headers["Cache-Control"] = "public" motor-3.4.0/motor/core.py000066400000000000000000002450751460060525600153410ustar00rootroot00000000000000# Copyright 2011-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. """Framework-agnostic core of Motor, an asynchronous driver for MongoDB.""" import functools import time import warnings import pymongo import pymongo.auth import pymongo.common import pymongo.database import pymongo.errors import pymongo.mongo_client from pymongo.change_stream import ChangeStream from pymongo.client_session import ClientSession from pymongo.collection import Collection from pymongo.command_cursor import CommandCursor, RawBatchCommandCursor from pymongo.cursor import _QUERY_OPTIONS, Cursor, RawBatchCursor from pymongo.database import Database from pymongo.driver_info import DriverInfo from pymongo.encryption import ClientEncryption from . import docstrings from . import version as motor_version from .metaprogramming import ( AsyncCommand, AsyncRead, AsyncWrite, DelegateMethod, MotorCursorChainingMethod, ReadOnlyProperty, coroutine_annotation, create_class_with_framework, unwrap_args_session, unwrap_kwargs_session, ) from .motor_common import callback_type_error HAS_SSL = True try: import ssl except ImportError: ssl = None HAS_SSL = False # From the Convenient API for Transactions spec, with_transaction must # halt retries after 120 seconds. # This limit is non-configurable and was chosen to be twice the 60 second # default value of MongoDB's `transactionLifetimeLimitSeconds` parameter. _WITH_TRANSACTION_RETRY_TIME_LIMIT = 120 def _within_time_limit(start_time): """Are we within the with_transaction retry limit?""" return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT def _max_time_expired_error(exc): """Return true if exc is a MaxTimeMSExpired error.""" return isinstance(exc, pymongo.errors.OperationFailure) and exc.code == 50 class AgnosticBase: def __eq__(self, other): if ( isinstance(other, self.__class__) and hasattr(self, "delegate") and hasattr(other, "delegate") ): return self.delegate == other.delegate return NotImplemented def __init__(self, delegate): self.delegate = delegate def __repr__(self): return f"{self.__class__.__name__}({self.delegate!r})" class AgnosticBaseProperties(AgnosticBase): codec_options = ReadOnlyProperty() read_preference = ReadOnlyProperty() read_concern = ReadOnlyProperty() write_concern = ReadOnlyProperty() class AgnosticClient(AgnosticBaseProperties): __motor_class_name__ = "MotorClient" __delegate_class__ = pymongo.mongo_client.MongoClient address = ReadOnlyProperty() arbiters = ReadOnlyProperty() close = DelegateMethod() __hash__ = DelegateMethod() drop_database = AsyncCommand().unwrap("MotorDatabase") options = ReadOnlyProperty() get_database = DelegateMethod(doc=docstrings.get_database_doc).wrap(Database) get_default_database = DelegateMethod(doc=docstrings.get_default_database_doc).wrap(Database) HOST = ReadOnlyProperty() is_mongos = ReadOnlyProperty() is_primary = ReadOnlyProperty() list_databases = AsyncRead().wrap(CommandCursor) list_database_names = AsyncRead() nodes = ReadOnlyProperty() PORT = ReadOnlyProperty() primary = ReadOnlyProperty() read_concern = ReadOnlyProperty() secondaries = ReadOnlyProperty() server_info = AsyncRead() topology_description = ReadOnlyProperty() start_session = AsyncCommand(doc=docstrings.start_session_doc).wrap(ClientSession) def __init__(self, *args, **kwargs): """Create a new connection to a single MongoDB instance at *host:port*. Takes the same constructor arguments as :class:`~pymongo.mongo_client.MongoClient`, as well as: :Parameters: - `io_loop` (optional): Special event loop instance to use instead of default. """ if "io_loop" in kwargs: io_loop = kwargs.pop("io_loop") self._framework.check_event_loop(io_loop) else: io_loop = None self._io_loop = io_loop kwargs.setdefault("connect", False) driver_info = DriverInfo("Motor", motor_version, self._framework.platform_info()) if kwargs.get("driver"): provided_info = kwargs.get("driver") if not isinstance(provided_info, DriverInfo): raise TypeError( f"Incorrect type for `driver` {type(provided_info)};" " expected value of type pymongo.driver_info.DriverInfo" ) added_version = f"|{provided_info.version}" if provided_info.version else "" added_platform = f"|{provided_info.platform}" if provided_info.platform else "" driver_info = DriverInfo( f"{driver_info.name}|{provided_info.name}", f"{driver_info.version}{added_version}", f"{driver_info.platform}{added_platform}", ) kwargs["driver"] = driver_info delegate = self.__delegate_class__(*args, **kwargs) super().__init__(delegate) @property def io_loop(self): if self._io_loop is None: self._io_loop = self._framework.get_event_loop() return self._io_loop def get_io_loop(self): return self.io_loop def watch( self, pipeline=None, full_document=None, resume_after=None, max_await_time_ms=None, batch_size=None, collation=None, start_at_operation_time=None, session=None, start_after=None, comment=None, full_document_before_change=None, show_expanded_events=None, ): """Watch changes on this cluster. Returns a :class:`~MotorChangeStream` cursor which iterates over changes on all databases in this cluster. Introduced in MongoDB 4.0. See the documentation for :meth:`MotorCollection.watch` for more details and examples. :Parameters: - `pipeline` (optional): A list of aggregation pipeline stages to append to an initial ``$changeStream`` stage. Not all pipeline stages are valid after a ``$changeStream`` stage, see the MongoDB documentation on change streams for the supported stages. - `full_document` (optional): The fullDocument option to pass to the ``$changeStream`` stage. Allowed values: 'updateLookup'. When set to 'updateLookup', the change notification for partial updates will include both a delta describing the changes to the document, as well as a copy of the entire document that was changed from some time after the change occurred. - `resume_after` (optional): A resume token. If provided, the change stream will start returning changes that occur directly after the operation specified in the resume token. A resume token is the _id value of a change document. - `max_await_time_ms` (optional): The maximum time in milliseconds for the server to wait for changes before responding to a getMore operation. - `batch_size` (optional): The maximum number of documents to return per batch. - `collation` (optional): The :class:`~pymongo.collation.Collation` to use for the aggregation. - `start_at_operation_time` (optional): If provided, the resulting change stream will only return changes that occurred at or after the specified :class:`~bson.timestamp.Timestamp`. Requires MongoDB >= 4.0. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. - `start_after` (optional): The same as `resume_after` except that `start_after` can resume notifications after an invalidate event. This option and `resume_after` are mutually exclusive. - `comment` (optional): A user-provided comment to attach to this command. - `full_document_before_change`: Allowed values: `whenAvailable` and `required`. Change events may now result in a `fullDocumentBeforeChange` response field. - `show_expanded_events` (optional): Include expanded events such as DDL events like `dropIndexes`. :Returns: A :class:`~MotorChangeStream`. .. versionchanged:: 3.2 Added ``show_expanded_events`` parameter. .. versionchanged:: 3.1 Added ``full_document_before_change`` parameter. .. versionchanged:: 3.0 Added ``comment`` parameter. .. versionchanged:: 2.1 Added the ``start_after`` parameter. .. versionadded:: 2.0 .. mongodoc:: changeStreams """ cursor_class = create_class_with_framework( AgnosticChangeStream, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class( self, pipeline, full_document, resume_after, max_await_time_ms, batch_size, collation, start_at_operation_time, session, start_after, comment, full_document_before_change, show_expanded_events, ) def __getattr__(self, name): if name.startswith("_"): raise AttributeError( f"{self.__class__.__name__} has no attribute {name!r}. To access the {name}" f" database, use client['{name}']." ) return self[name] def __getitem__(self, name): db_class = create_class_with_framework(AgnosticDatabase, self._framework, self.__module__) return db_class(self, name) def wrap(self, obj): if obj.__class__ == Database: db_class = create_class_with_framework( AgnosticDatabase, self._framework, self.__module__ ) return db_class(self, obj.name, _delegate=obj) elif obj.__class__ == CommandCursor: command_cursor_class = create_class_with_framework( AgnosticCommandCursor, self._framework, self.__module__ ) return command_cursor_class(obj, self) elif obj.__class__ == ClientSession: session_class = create_class_with_framework( AgnosticClientSession, self._framework, self.__module__ ) return session_class(obj, self) class _MotorTransactionContext: """Internal transaction context manager for start_transaction.""" def __init__(self, session): self._session = session async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self._session.in_transaction: if exc_val is None: await self._session.commit_transaction() else: await self._session.abort_transaction() class AgnosticClientSession(AgnosticBase): """A session for ordering sequential operations. Do not create an instance of :class:`MotorClientSession` directly; use :meth:`MotorClient.start_session`: .. code-block:: python3 collection = client.db.collection async with await client.start_session() as s: async with s.start_transaction(): await collection.delete_one({"x": 1}, session=s) await collection.insert_one({"x": 2}, session=s) .. versionadded:: 2.0 """ __motor_class_name__ = "MotorClientSession" __delegate_class__ = ClientSession commit_transaction = AsyncCommand() abort_transaction = AsyncCommand() end_session = AsyncCommand() cluster_time = ReadOnlyProperty() has_ended = ReadOnlyProperty() in_transaction = ReadOnlyProperty() options = ReadOnlyProperty() operation_time = ReadOnlyProperty() session_id = ReadOnlyProperty() advance_cluster_time = DelegateMethod() advance_operation_time = DelegateMethod() def __init__(self, delegate, motor_client): AgnosticBase.__init__(self, delegate=delegate) self._client = motor_client def get_io_loop(self): return self._client.get_io_loop() async def with_transaction( self, coro, read_concern=None, write_concern=None, read_preference=None, max_commit_time_ms=None, ): """Executes an awaitable in a transaction. This method starts a transaction on this session, awaits ``coro`` once, and then commits the transaction. For example:: async def coro(session): orders = session.client.db.orders inventory = session.client.db.inventory inserted_id = await orders.insert_one( {"sku": "abc123", "qty": 100}, session=session) await inventory.update_one( {"sku": "abc123", "qty": {"$gte": 100}}, {"$inc": {"qty": -100}}, session=session) return inserted_id async with await client.start_session() as session: inserted_id = await session.with_transaction(coro) To pass arbitrary arguments to the ``coro``, wrap it with a ``lambda`` like this:: async def coro(session, custom_arg, custom_kwarg=None): # Transaction operations... async with await client.start_session() as session: await session.with_transaction( lambda s: coro(s, "custom_arg", custom_kwarg=1)) In the event of an exception, ``with_transaction`` may retry the commit or the entire transaction, therefore ``coro`` may be awaited multiple times by a single call to ``with_transaction``. Developers should be mindful of this possibility when writing a ``coro`` that modifies application state or has any other side-effects. Note that even when the ``coro`` is invoked multiple times, ``with_transaction`` ensures that the transaction will be committed at-most-once on the server. The ``coro`` should not attempt to start new transactions, but should simply run operations meant to be contained within a transaction. The ``coro`` should also not commit the transaction; this is handled automatically by ``with_transaction``. If the ``coro`` does commit or abort the transaction without error, however, ``with_transaction`` will return without taking further action. When ``coro`` raises an exception, ``with_transaction`` automatically aborts the current transaction. When ``coro`` or :meth:`~ClientSession.commit_transaction` raises an exception that includes the ``"TransientTransactionError"`` error label, ``with_transaction`` starts a new transaction and re-executes the ``coro``. When :meth:`~ClientSession.commit_transaction` raises an exception with the ``"UnknownTransactionCommitResult"`` error label, ``with_transaction`` retries the commit until the result of the transaction is known. This method will cease retrying after 120 seconds has elapsed. This timeout is not configurable and any exception raised by the ``coro`` or by :meth:`ClientSession.commit_transaction` after the timeout is reached will be re-raised. Applications that desire a different timeout duration should not use this method. :Parameters: - `coro`: The coroutine to run inside a transaction. The coroutine must accept a single argument, this session. Note, under certain error conditions the coroutine may be run multiple times. - `read_concern` (optional): The :class:`~pymongo.read_concern.ReadConcern` to use for this transaction. - `write_concern` (optional): The :class:`~pymongo.write_concern.WriteConcern` to use for this transaction. - `read_preference` (optional): The read preference to use for this transaction. If ``None`` (the default) the :attr:`read_preference` of this :class:`Database` is used. See :mod:`~pymongo.read_preferences` for options. :Returns: The return value of the ``coro``. .. versionadded:: 2.1 """ start_time = time.monotonic() while True: async with self.start_transaction( read_concern, write_concern, read_preference, max_commit_time_ms ): try: ret = await coro(self) except Exception as exc: if self.in_transaction: await self.abort_transaction() if ( isinstance(exc, pymongo.errors.PyMongoError) and exc.has_error_label("TransientTransactionError") and _within_time_limit(start_time) ): # Retry the entire transaction. continue raise if not self.in_transaction: # Assume callback intentionally ended the transaction. return ret while True: try: await self.commit_transaction() except pymongo.errors.PyMongoError as exc: if ( exc.has_error_label("UnknownTransactionCommitResult") and _within_time_limit(start_time) and not _max_time_expired_error(exc) ): # Retry the commit. continue if exc.has_error_label("TransientTransactionError") and _within_time_limit( start_time ): # Retry the entire transaction. break raise # Commit succeeded. return ret def start_transaction( self, read_concern=None, write_concern=None, read_preference=None, max_commit_time_ms=None ): """Start a multi-statement transaction. Takes the same arguments as :class:`~pymongo.client_session.TransactionOptions`. Best used in a context manager block: .. code-block:: python3 # Use "await" for start_session, but not for start_transaction. async with await client.start_session() as s: async with s.start_transaction(): await collection.delete_one({"x": 1}, session=s) await collection.insert_one({"x": 2}, session=s) """ self.delegate.start_transaction( read_concern=read_concern, write_concern=write_concern, read_preference=read_preference, max_commit_time_ms=max_commit_time_ms, ) return _MotorTransactionContext(self) @property def client(self): """The :class:`~MotorClient` this session was created from.""" return self._client async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): self.delegate.__exit__(exc_type, exc_val, exc_tb) def __enter__(self): raise AttributeError( "Use Motor sessions like 'async with await client.start_session()', not 'with'" ) def __exit__(self, exc_type, exc_val, exc_tb): pass class AgnosticDatabase(AgnosticBaseProperties): __motor_class_name__ = "MotorDatabase" __delegate_class__ = Database __hash__ = DelegateMethod() __bool__ = DelegateMethod() command = AsyncCommand(doc=docstrings.cmd_doc) create_collection = AsyncCommand().wrap(Collection) dereference = AsyncRead() drop_collection = AsyncCommand().unwrap("MotorCollection") get_collection = DelegateMethod().wrap(Collection) list_collection_names = AsyncRead(doc=docstrings.list_collection_names_doc) list_collections = AsyncRead().wrap(CommandCursor) name = ReadOnlyProperty() validate_collection = AsyncRead().unwrap("MotorCollection") with_options = DelegateMethod().wrap(Database) _async_aggregate = AsyncRead(attr_name="aggregate") def __init__(self, client, name, **kwargs): self._client = client _delegate = kwargs.get("_delegate") delegate = _delegate if _delegate is not None else Database(client.delegate, name, **kwargs) super().__init__(delegate) def aggregate(self, pipeline, *args, **kwargs): """Execute an aggregation pipeline on this database. Introduced in MongoDB 3.6. The aggregation can be run on a secondary if the client is connected to a replica set and its ``read_preference`` is not :attr:`PRIMARY`. The :meth:`aggregate` method obeys the :attr:`read_preference` of this :class:`MotorDatabase`, except when ``$out`` or ``$merge`` are used, in which case :attr:`PRIMARY` is used. All optional `aggregate command`_ parameters should be passed as keyword arguments to this method. Valid options include, but are not limited to: - `allowDiskUse` (bool): Enables writing to temporary files. When set to True, aggregation stages can write data to the _tmp subdirectory of the --dbpath directory. The default is False. - `maxTimeMS` (int): The maximum amount of time to allow the operation to run in milliseconds. - `batchSize` (int): The maximum number of documents to return per batch. Ignored if the connected mongod or mongos does not support returning aggregate results using a cursor. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. - `let` (dict): A dict of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. ``"$$var"``). This option is only supported on MongoDB >= 5.0. Returns a :class:`MotorCommandCursor` that can be iterated like a cursor from :meth:`find`:: async def f(): # Lists all operations currently running on the server. pipeline = [{"$currentOp": {}}] async for operation in client.admin.aggregate(pipeline): print(operation) .. note:: This method does not support the 'explain' option. Please use :meth:`MotorDatabase.command` instead. .. note:: The :attr:`MotorDatabase.write_concern` of this database is automatically applied to this operation. .. versionadded:: 2.1 .. _aggregate command: https://www.mongodb.com/docs/manual/reference/command/aggregate/ """ cursor_class = create_class_with_framework( AgnosticLatentCommandCursor, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class( self["$cmd.aggregate"], self._async_aggregate, pipeline, *unwrap_args_session(args), **unwrap_kwargs_session(kwargs), ) def watch( self, pipeline=None, full_document=None, resume_after=None, max_await_time_ms=None, batch_size=None, collation=None, start_at_operation_time=None, session=None, start_after=None, comment=None, full_document_before_change=None, show_expanded_events=None, ): """Watch changes on this database. Returns a :class:`~MotorChangeStream` cursor which iterates over changes on this database. Introduced in MongoDB 4.0. See the documentation for :meth:`MotorCollection.watch` for more details and examples. :Parameters: - `pipeline` (optional): A list of aggregation pipeline stages to append to an initial ``$changeStream`` stage. Not all pipeline stages are valid after a ``$changeStream`` stage, see the MongoDB documentation on change streams for the supported stages. - `full_document` (optional): The fullDocument option to pass to the ``$changeStream`` stage. Allowed values: 'updateLookup'. When set to 'updateLookup', the change notification for partial updates will include both a delta describing the changes to the document, as well as a copy of the entire document that was changed from some time after the change occurred. - `resume_after` (optional): A resume token. If provided, the change stream will start returning changes that occur directly after the operation specified in the resume token. A resume token is the _id value of a change document. - `max_await_time_ms` (optional): The maximum time in milliseconds for the server to wait for changes before responding to a getMore operation. - `batch_size` (optional): The maximum number of documents to return per batch. - `collation` (optional): The :class:`~pymongo.collation.Collation` to use for the aggregation. - `start_at_operation_time` (optional): If provided, the resulting change stream will only return changes that occurred at or after the specified :class:`~bson.timestamp.Timestamp`. Requires MongoDB >= 4.0. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. - `start_after` (optional): The same as `resume_after` except that `start_after` can resume notifications after an invalidate event. This option and `resume_after` are mutually exclusive. - `comment` (optional): A user-provided comment to attach to this command. - `full_document_before_change`: Allowed values: `whenAvailable` and `required`. Change events may now result in a `fullDocumentBeforeChange` response field. - `show_expanded_events` (optional): Include expanded events such as DDL events like `dropIndexes`. :Returns: A :class:`~MotorChangeStream`. .. versionchanged:: 3.2 Added ``show_expanded_events`` parameter. .. versionchanged:: 3.1 Added ``full_document_before_change`` parameter. .. versionchanged:: 3.0 Added ``comment`` parameter. .. versionchanged:: 2.1 Added the ``start_after`` parameter. .. versionadded:: 2.0 .. mongodoc:: changeStreams """ cursor_class = create_class_with_framework( AgnosticChangeStream, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class( self, pipeline, full_document, resume_after, max_await_time_ms, batch_size, collation, start_at_operation_time, session, start_after, comment, full_document_before_change, show_expanded_events, ) async def cursor_command( self, command, value=1, read_preference=None, codec_options=None, session=None, comment=None, max_await_time_ms=None, **kwargs, ): """Issue a MongoDB command and parse the response as a cursor. If the response from the server does not include a cursor field, an error will be thrown. Otherwise, behaves identically to issuing a normal MongoDB command. :Parameters: - `command`: document representing the command to be issued, or the name of the command (for simple commands only). .. note:: the order of keys in the `command` document is significant (the "verb" must come first), so commands which require multiple keys (e.g. `findandmodify`) should use an instance of :class:`~bson.son.SON` or a string and kwargs instead of a Python `dict`. - `value` (optional): value to use for the command verb when `command` is passed as a string - `read_preference` (optional): The read preference for this operation. See :mod:`~pymongo.read_preferences` for options. If the provided `session` is in a transaction, defaults to the read preference configured for the transaction. Otherwise, defaults to :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`. - `codec_options`: A :class:`~bson.codec_options.CodecOptions` instance. - `session` (optional): A :class:`MotorClientSession`. - `comment` (optional): A user-provided comment to attach to future getMores for this command. - `max_await_time_ms` (optional): The number of ms to wait for more data on future getMores for this command. - `**kwargs` (optional): additional keyword arguments will be added to the command document before it is sent .. note:: :meth:`command` does **not** obey this Database's :attr:`read_preference` or :attr:`codec_options`. You must use the ``read_preference`` and ``codec_options`` parameters instead. .. note:: :meth:`command` does **not** apply any custom TypeDecoders when decoding the command response. .. note:: If this client has been configured to use MongoDB Stable API (see :ref:`versioned-api-ref`), then :meth:`command` will automatically add API versioning options to the given command. Explicitly adding API versioning options in the command and declaring an API version on the client is not supported. .. seealso:: The MongoDB documentation on `commands `_. """ args = (command,) kwargs["value"] = value kwargs["read_preference"] = read_preference kwargs["codec_options"] = codec_options kwargs["session"] = session kwargs["comment"] = comment kwargs["max_await_time_ms"] = max_await_time_ms def inner(): return self.delegate.cursor_command( *unwrap_args_session(args), **unwrap_kwargs_session(kwargs) ) loop = self.get_io_loop() cursor = await self._framework.run_on_executor(loop, inner) cursor_class = create_class_with_framework( AgnosticCommandCursor, self._framework, self.__module__ ) return cursor_class(cursor, self) @property def client(self): """This MotorDatabase's :class:`MotorClient`.""" return self._client def __getattr__(self, name): if name.startswith("_"): raise AttributeError( f"{self.__class__.__name__} has no attribute {name!r}. To access the {name}" " collection, use database['{name}']." ) return self[name] def __getitem__(self, name): collection_class = create_class_with_framework( AgnosticCollection, self._framework, self.__module__ ) return collection_class(self, name) def __call__(self, *args, **kwargs): database_name = self.delegate.name client_class_name = self._client.__class__.__name__ if database_name == "open_sync": raise TypeError( f"{client_class_name}.open_sync() is unnecessary Motor 0.2, " "see changelog for details." ) raise TypeError( "MotorDatabase object is not callable. If you meant to " f"call the '{database_name}' method on a {client_class_name} object it is " "failing because no such method exists." ) def wrap(self, obj): if obj.__class__ is Collection: # Replace pymongo.collection.Collection with MotorCollection. klass = create_class_with_framework( AgnosticCollection, self._framework, self.__module__ ) return klass(self, obj.name, _delegate=obj) elif obj.__class__ is Database: return self.__class__(self._client, obj.name, _delegate=obj) elif obj.__class__ is CommandCursor: command_cursor_class = create_class_with_framework( AgnosticCommandCursor, self._framework, self.__module__ ) return command_cursor_class(obj, self) else: return obj def get_io_loop(self): return self._client.get_io_loop() class AgnosticCollection(AgnosticBaseProperties): __motor_class_name__ = "MotorCollection" __delegate_class__ = Collection __hash__ = DelegateMethod() __bool__ = DelegateMethod() bulk_write = AsyncCommand(doc=docstrings.bulk_write_doc) count_documents = AsyncRead() create_index = AsyncCommand(doc=docstrings.create_index_doc) create_indexes = AsyncCommand(doc=docstrings.create_indexes_doc) create_search_index = AsyncCommand() create_search_indexes = AsyncCommand() delete_many = AsyncCommand(doc=docstrings.delete_many_doc) delete_one = AsyncCommand(doc=docstrings.delete_one_doc) distinct = AsyncRead() drop = AsyncCommand(doc=docstrings.drop_doc) drop_index = AsyncCommand() drop_search_index = AsyncCommand() drop_indexes = AsyncCommand() estimated_document_count = AsyncCommand() find_one = AsyncRead(doc=docstrings.find_one_doc) find_one_and_delete = AsyncCommand(doc=docstrings.find_one_and_delete_doc) find_one_and_replace = AsyncCommand(doc=docstrings.find_one_and_replace_doc) find_one_and_update = AsyncCommand(doc=docstrings.find_one_and_update_doc) full_name = ReadOnlyProperty() index_information = AsyncRead(doc=docstrings.index_information_doc) insert_many = AsyncWrite(doc=docstrings.insert_many_doc) insert_one = AsyncCommand(doc=docstrings.insert_one_doc) name = ReadOnlyProperty() options = AsyncRead() rename = AsyncCommand() replace_one = AsyncCommand(doc=docstrings.replace_one_doc) update_many = AsyncCommand(doc=docstrings.update_many_doc) update_one = AsyncCommand(doc=docstrings.update_one_doc) update_search_index = AsyncCommand() with_options = DelegateMethod().wrap(Collection) _async_aggregate = AsyncRead(attr_name="aggregate") _async_aggregate_raw_batches = AsyncRead(attr_name="aggregate_raw_batches") _async_list_indexes = AsyncRead(attr_name="list_indexes") _async_list_search_indexes = AsyncRead(attr_name="list_search_indexes") def __init__( self, database, name, codec_options=None, read_preference=None, write_concern=None, read_concern=None, _delegate=None, ): db_class = create_class_with_framework(AgnosticDatabase, self._framework, self.__module__) if not isinstance(database, db_class): raise TypeError( "First argument to MotorCollection must be MotorDatabase, not %r" % database ) delegate = ( _delegate if _delegate is not None else Collection( database.delegate, name, codec_options=codec_options, read_preference=read_preference, write_concern=write_concern, read_concern=read_concern, ) ) super().__init__(delegate) self.database = database def __getattr__(self, name): # Dotted collection name, like "foo.bar". if name.startswith("_"): full_name = f"{self.name}.{name}" raise AttributeError( f"{self.__class__.__name__} has no attribute {name!r}. To access the {full_name}" f" collection, use database['{full_name}']." ) return self[name] def __getitem__(self, name): collection_class = create_class_with_framework( AgnosticCollection, self._framework, self.__module__ ) return collection_class( self.database, self.name + "." + name, _delegate=self.delegate[name] ) def __call__(self, *args, **kwargs): raise TypeError( "MotorCollection object is not callable. If you meant to " "call the '%s' method on a MotorCollection object it is " "failing because no such method exists." % self.delegate.name ) def find(self, *args, **kwargs): """Create a :class:`MotorCursor`. Same parameters as for PyMongo's :meth:`~pymongo.collection.Collection.find`. Note that ``find`` does not require an ``await`` expression, because ``find`` merely creates a :class:`MotorCursor` without performing any operations on the server. ``MotorCursor`` methods such as :meth:`~MotorCursor.to_list` perform actual operations. """ cursor = self.delegate.find(*unwrap_args_session(args), **unwrap_kwargs_session(kwargs)) cursor_class = create_class_with_framework(AgnosticCursor, self._framework, self.__module__) return cursor_class(cursor, self) def find_raw_batches(self, *args, **kwargs): """Query the database and retrieve batches of raw BSON. Similar to the :meth:`find` method but returns each batch as bytes. This example demonstrates how to work with raw batches, but in practice raw batches should be passed to an external library that can decode BSON into another data type, rather than used with PyMongo's :mod:`bson` module. .. code-block:: python3 async def get_raw(): cursor = db.test.find_raw_batches() async for batch in cursor: print(bson.decode_all(batch)) Note that ``find_raw_batches`` does not support sessions. .. versionadded:: 2.0 """ cursor = self.delegate.find_raw_batches( *unwrap_args_session(args), **unwrap_kwargs_session(kwargs) ) cursor_class = create_class_with_framework( AgnosticRawBatchCursor, self._framework, self.__module__ ) return cursor_class(cursor, self) def aggregate(self, pipeline, *args, **kwargs): """Execute an aggregation pipeline on this collection. The aggregation can be run on a secondary if the client is connected to a replica set and its ``read_preference`` is not :attr:`PRIMARY`. :Parameters: - `pipeline`: a single command or list of aggregation commands - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `**kwargs`: send arbitrary parameters to the aggregate command All optional `aggregate command`_ parameters should be passed as keyword arguments to this method. Valid options include, but are not limited to: - `allowDiskUse` (bool): Enables writing to temporary files. When set to True, aggregation stages can write data to the _tmp subdirectory of the --dbpath directory. The default is False. - `maxTimeMS` (int): The maximum amount of time to allow the operation to run in milliseconds. - `batchSize` (int): The maximum number of documents to return per batch. Ignored if the connected mongod or mongos does not support returning aggregate results using a cursor. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. - `let` (dict): A dict of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. ``"$$var"``). This option is only supported on MongoDB >= 5.0. Returns a :class:`MotorCommandCursor` that can be iterated like a cursor from :meth:`find`:: async def f(): pipeline = [{'$project': {'name': {'$toUpper': '$name'}}}] async for doc in collection.aggregate(pipeline): print(doc) Note that this method returns a :class:`MotorCommandCursor` which lazily runs the aggregate command when first iterated. In order to run an aggregation with ``$out`` or ``$merge`` the application needs to iterate the cursor, for example:: cursor = motor_coll.aggregate([{'$out': 'out'}]) # Iterate the cursor to run the $out (or $merge) operation. await cursor.to_list(length=None) # Or more succinctly: await motor_coll.aggregate([{'$out': 'out'}]).to_list(length=None) # Or: async for _ in motor_coll.aggregate([{'$out': 'out'}]): pass :class:`MotorCommandCursor` does not allow the ``explain`` option. To explain MongoDB's query plan for the aggregation, use :meth:`MotorDatabase.command`:: async def f(): plan = await db.command( 'aggregate', 'COLLECTION-NAME', pipeline=[{'$project': {'x': 1}}], explain=True) print(plan) .. versionchanged:: 2.1 This collection's read concern is now applied to pipelines containing the `$out` stage when connected to MongoDB >= 4.2. .. versionchanged:: 1.0 :meth:`aggregate` now **always** returns a cursor. .. versionchanged:: 0.5 :meth:`aggregate` now returns a cursor by default, and the cursor is returned immediately without an ``await``. See :ref:`aggregation changes in Motor 0.5 `. .. versionchanged:: 0.2 Added cursor support. .. _aggregate command: https://mongodb.com/docs/manual/applications/aggregation """ cursor_class = create_class_with_framework( AgnosticLatentCommandCursor, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class( self, self._async_aggregate, pipeline, *unwrap_args_session(args), **unwrap_kwargs_session(kwargs), ) def aggregate_raw_batches(self, pipeline, **kwargs): """Perform an aggregation and retrieve batches of raw BSON. Similar to the :meth:`aggregate` method but returns each batch as bytes. This example demonstrates how to work with raw batches, but in practice raw batches should be passed to an external library that can decode BSON into another data type, rather than used with PyMongo's :mod:`bson` module. .. code-block:: python3 async def get_raw(): cursor = db.test.aggregate_raw_batches() async for batch in cursor: print(bson.decode_all(batch)) Note that ``aggregate_raw_batches`` does not support sessions. .. versionadded:: 2.0 """ cursor_class = create_class_with_framework( AgnosticLatentCommandCursor, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class( self, self._async_aggregate_raw_batches, pipeline, **unwrap_kwargs_session(kwargs) ) def watch( self, pipeline=None, full_document=None, resume_after=None, max_await_time_ms=None, batch_size=None, collation=None, start_at_operation_time=None, session=None, start_after=None, comment=None, full_document_before_change=None, show_expanded_events=None, ): """Watch changes on this collection. Performs an aggregation with an implicit initial ``$changeStream`` stage and returns a :class:`~MotorChangeStream` cursor which iterates over changes on this collection. Introduced in MongoDB 3.6. A change stream continues waiting indefinitely for matching change events. Code like the following allows a program to cancel the change stream and exit. .. code-block:: python3 change_stream = None async def watch_collection(): global change_stream # Using the change stream in an "async with" block # ensures it is canceled promptly if your code breaks # from the loop or throws an exception. async with db.collection.watch() as change_stream: async for change in change_stream: print(change) # Tornado from tornado.ioloop import IOLoop def main(): loop = IOLoop.current() # Start watching collection for changes. try: loop.run_sync(watch_collection) except KeyboardInterrupt: if change_stream: loop.run_sync(change_stream.close) # asyncio try: asyncio.run(watch_collection()) except KeyboardInterrupt: pass The :class:`~MotorChangeStream` async iterable blocks until the next change document is returned or an error is raised. If the :meth:`~MotorChangeStream.next` method encounters a network error when retrieving a batch from the server, it will automatically attempt to recreate the cursor such that no change events are missed. Any error encountered during the resume attempt indicates there may be an outage and will be raised. .. code-block:: python3 try: pipeline = [{"$match": {"operationType": "insert"}}] async with db.collection.watch(pipeline) as stream: async for change in stream: print(change) except pymongo.errors.PyMongoError: # The ChangeStream encountered an unrecoverable error or the # resume attempt failed to recreate the cursor. logging.error("...") For a precise description of the resume process see the `change streams specification`_. :Parameters: - `pipeline` (optional): A list of aggregation pipeline stages to append to an initial ``$changeStream`` stage. Not all pipeline stages are valid after a ``$changeStream`` stage, see the MongoDB documentation on change streams for the supported stages. - `full_document` (optional): The fullDocument option to pass to the ``$changeStream`` stage. Allowed values: 'updateLookup'. When set to 'updateLookup', the change notification for partial updates will include both a delta describing the changes to the document, as well as a copy of the entire document that was changed from some time after the change occurred. - `resume_after` (optional): A resume token. If provided, the change stream will start returning changes that occur directly after the operation specified in the resume token. A resume token is the _id value of a change document. - `max_await_time_ms` (optional): The maximum time in milliseconds for the server to wait for changes before responding to a getMore operation. - `batch_size` (optional): The maximum number of documents to return per batch. - `collation` (optional): The :class:`~pymongo.collation.Collation` to use for the aggregation. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. - `start_after` (optional): The same as `resume_after` except that `start_after` can resume notifications after an invalidate event. This option and `resume_after` are mutually exclusive. - `comment` (optional): A user-provided comment to attach to this command. - `full_document_before_change`: Allowed values: `whenAvailable` and `required`. Change events may now result in a `fullDocumentBeforeChange` response field. - `show_expanded_events` (optional): Include expanded events such as DDL events like `dropIndexes`. :Returns: A :class:`~MotorChangeStream`. See the :ref:`tornado_change_stream_example`. .. versionchanged:: 3.2 Added ``show_expanded_events`` parameter. .. versionchanged:: 3.1 Added ``full_document_before_change`` parameter. .. versionchanged:: 3.0 Added ``comment`` parameter. .. versionchanged:: 2.1 Added the ``start_after`` parameter. .. versionadded:: 1.2 .. mongodoc:: changeStreams .. _change streams specification: https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst """ cursor_class = create_class_with_framework( AgnosticChangeStream, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class( self, pipeline, full_document, resume_after, max_await_time_ms, batch_size, collation, start_at_operation_time, session, start_after, comment, full_document_before_change, show_expanded_events, ) def list_indexes(self, session=None, **kwargs): """Get a cursor over the index documents for this collection. :: async def print_indexes(): async for index in db.test.list_indexes(): print(index) If the only index is the default index on ``_id``, this might print:: SON([('v', 1), ('key', SON([('_id', 1)])), ('name', '_id_')]) """ cursor_class = create_class_with_framework( AgnosticLatentCommandCursor, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class(self, self._async_list_indexes, session=session, **kwargs) def list_search_indexes(self, *args, **kwargs): """Return a cursor over search indexes for the current collection.""" cursor_class = create_class_with_framework( AgnosticLatentCommandCursor, self._framework, self.__module__ ) # Latent cursor that will send initial command on first "async for". return cursor_class(self, self._async_list_search_indexes, *args, **kwargs) def wrap(self, obj): if obj.__class__ is Collection: # Replace pymongo.collection.Collection with MotorCollection. return self.__class__(self.database, obj.name, _delegate=obj) elif obj.__class__ is Cursor: return AgnosticCursor(obj, self) elif obj.__class__ is CommandCursor: command_cursor_class = create_class_with_framework( AgnosticCommandCursor, self._framework, self.__module__ ) return command_cursor_class(obj, self) elif obj.__class__ is ChangeStream: change_stream_class = create_class_with_framework( AgnosticChangeStream, self._framework, self.__module__ ) return change_stream_class(obj, self) else: return obj def get_io_loop(self): return self.database.get_io_loop() class AgnosticBaseCursor(AgnosticBase): """Base class for AgnosticCursor and AgnosticCommandCursor""" _async_close = AsyncRead(attr_name="close") _refresh = AsyncRead() address = ReadOnlyProperty() cursor_id = ReadOnlyProperty() alive = ReadOnlyProperty() session = ReadOnlyProperty() def __init__(self, cursor, collection): """Don't construct a cursor yourself, but acquire one from methods like :meth:`MotorCollection.find` or :meth:`MotorCollection.aggregate`. .. note:: There is no need to manually close cursors; they are closed by the server after being fully iterated with :meth:`to_list`, :meth:`each`, or `async for`, or automatically closed by the client when the :class:`MotorCursor` is cleaned up by the garbage collector. """ # 'cursor' is a PyMongo Cursor, CommandCursor, or a _LatentCursor. super().__init__(delegate=cursor) self.collection = collection self.started = False self.closed = False # python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions def __aiter__(self): return self async def next(self): """Advance the cursor. .. versionadded:: 2.2 """ if self.alive and (self._buffer_size() or await self._get_more()): return next(self.delegate) raise StopAsyncIteration __anext__ = next async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self.delegate: await self.close() def _get_more(self): """Initial query or getMore. Returns a Future.""" if not self.alive: raise pymongo.errors.InvalidOperation( "Can't call get_more() on a MotorCursor that has been exhausted or killed." ) self.started = True return self._refresh() @property @coroutine_annotation def fetch_next(self): """**DEPRECATED** - A Future used with `gen.coroutine`_ to asynchronously retrieve the next document in the result set, fetching a batch of documents from the server if necessary. Resolves to ``False`` if there are no more documents, otherwise :meth:`next_object` is guaranteed to return a document: .. doctest:: fetch_next :hide: >>> _ = MongoClient().test.test_collection.delete_many({}) >>> collection = MotorClient().test.test_collection .. attention:: The :attr:`fetch_next` property is deprecated and will be removed in Motor 3.0. Use `async for` to iterate elegantly and efficiently over :class:`MotorCursor` objects instead.: .. doctest:: fetch_next >>> async def f(): ... await collection.drop() ... await collection.insert_many([{"_id": i} for i in range(5)]) ... async for doc in collection.find(): ... sys.stdout.write(str(doc["_id"]) + ", ") ... print("done") ... >>> IOLoop.current().run_sync(f) 0, 1, 2, 3, 4, done While it appears that fetch_next retrieves each document from the server individually, the cursor actually fetches documents efficiently in `large batches`_. Example usage: .. doctest:: fetch_next >>> async def f(): ... await collection.drop() ... await collection.insert_many([{"_id": i} for i in range(5)]) ... cursor = collection.find().sort([("_id", 1)]) ... while await cursor.fetch_next: ... doc = cursor.next_object() ... sys.stdout.write(str(doc["_id"]) + ", ") ... print("done") ... >>> IOLoop.current().run_sync(f) 0, 1, 2, 3, 4, done .. versionchanged:: 2.2 Deprecated. .. _`large batches`: https://www.mongodb.com/docs/manual/tutorial/iterate-a-cursor/#cursor-batches .. _`gen.coroutine`: http://tornadoweb.org/en/stable/gen.html """ warnings.warn( "The fetch_next property is deprecated and may be " "removed in a future major release. Use `async for` to iterate " "over Cursor objects instead.", DeprecationWarning, stacklevel=2, ) if not self._buffer_size() and self.alive: # Return the Future, which resolves to number of docs fetched or 0. return self._get_more() elif self._buffer_size(): future = self._framework.get_future(self.get_io_loop()) future.set_result(True) return future else: # Dead future = self._framework.get_future(self.get_io_loop()) future.set_result(False) return future def next_object(self): """**DEPRECATED** - Get a document from the most recently fetched batch, or ``None``. See :attr:`fetch_next`. The :meth:`next_object` method is deprecated and may be removed in a future major release. Use `async for` to elegantly iterate over :class:`MotorCursor` objects instead. .. versionchanged:: 2.2 Deprecated. """ warnings.warn( "The next_object method is deprecated and may be " "removed in a future major release. Use `async for` to iterate " "over Cursor objects instead.", DeprecationWarning, stacklevel=2, ) if not self._buffer_size(): return None return next(self.delegate) def each(self, callback): """Iterates over all the documents for this cursor. :meth:`each` returns immediately, and `callback` is executed asynchronously for each document. `callback` is passed ``(None, None)`` when iteration is complete. Cancel iteration early by returning ``False`` from the callback. (Only ``False`` cancels iteration: returning ``None`` or 0 does not.) .. testsetup:: each from tornado.ioloop import IOLoop MongoClient().test.test_collection.delete_many({}) MongoClient().test.test_collection.insert_many([{"_id": i} for i in range(5)]) collection = MotorClient().test.test_collection .. doctest:: each >>> def each(result, error): ... if error: ... raise error ... elif result: ... sys.stdout.write(str(result["_id"]) + ", ") ... else: ... # Iteration complete ... IOLoop.current().stop() ... print("done") ... >>> cursor = collection.find().sort([("_id", 1)]) >>> cursor.each(callback=each) >>> IOLoop.current().start() 0, 1, 2, 3, 4, done .. note:: Unlike other Motor methods, ``each`` requires a callback and does not return a Future, so it cannot be used in a coroutine. ``async for`` and :meth:`to_list` are much easier to use. :Parameters: - `callback`: function taking (document, error) """ if not callable(callback): raise callback_type_error self._each_got_more(callback, None) def _each_got_more(self, callback, future): if future: try: future.result() except Exception as error: callback(None, error) return while self._buffer_size() > 0: doc = next(self.delegate) # decrements self.buffer_size # Quit if callback returns exactly False (not None). Note we # don't close the cursor: user may want to resume iteration. if callback(doc, None) is False: return # The callback closed this cursor? if self.closed: return if self.alive and (self.cursor_id or not self.started): self._framework.add_future( self.get_io_loop(), self._get_more(), self._each_got_more, callback ) else: # Complete self._framework.call_soon(self.get_io_loop(), functools.partial(callback, None, None)) @coroutine_annotation def to_list(self, length): """Get a list of documents. .. testsetup:: to_list MongoClient().test.test_collection.delete_many({}) MongoClient().test.test_collection.insert_many([{"_id": i} for i in range(4)]) from tornado import ioloop .. doctest:: to_list >>> from motor.motor_tornado import MotorClient >>> collection = MotorClient().test.test_collection >>> >>> async def f(): ... cursor = collection.find().sort([("_id", 1)]) ... docs = await cursor.to_list(length=2) ... while docs: ... print(docs) ... docs = await cursor.to_list(length=2) ... print("done") ... >>> ioloop.IOLoop.current().run_sync(f) [{'_id': 0}, {'_id': 1}] [{'_id': 2}, {'_id': 3}] done :Parameters: - `length`: maximum number of documents to return for this call, or None Returns a Future. .. versionchanged:: 2.0 No longer accepts a callback argument. .. versionchanged:: 0.2 `callback` must be passed as a keyword argument, like ``to_list(10, callback=callback)``, and the `length` parameter is no longer optional. """ if length is not None: if not isinstance(length, int): raise TypeError("length must be an int, not %r" % length) elif length < 0: raise ValueError("length must be non-negative") if self._query_flags() & _QUERY_OPTIONS["tailable_cursor"]: raise pymongo.errors.InvalidOperation("Can't call to_list on tailable cursor") future = self._framework.get_future(self.get_io_loop()) if not self.alive: future.set_result([]) else: the_list = [] self._framework.add_future( self.get_io_loop(), self._get_more(), self._to_list, length, the_list, future, ) return future def _to_list(self, length, the_list, future, get_more_result): # get_more_result is the result of self._get_more(). # to_list_future will be the result of the user's to_list() call. try: result = get_more_result.result() # Return early if the task was cancelled. if future.done(): return if length is None: n = result else: n = min(length - len(the_list), result) for _ in range(n): the_list.append(self._data().popleft()) reached_length = length is not None and len(the_list) >= length if reached_length or not self.alive: future.set_result(the_list) else: self._framework.add_future( self.get_io_loop(), self._get_more(), self._to_list, length, the_list, future ) except Exception as exc: if not future.done(): future.set_exception(exc) def get_io_loop(self): return self.collection.get_io_loop() async def close(self): """Explicitly kill this cursor on the server. Call like:: await cursor.close() """ if not self.closed: self.closed = True await self._async_close() def batch_size(self, batch_size): self.delegate.batch_size(batch_size) return self def _buffer_size(self): return len(self._data()) # Paper over some differences between PyMongo Cursor and CommandCursor. def _query_flags(self): raise NotImplementedError def _data(self): raise NotImplementedError def _killed(self): raise NotImplementedError class AgnosticCursor(AgnosticBaseCursor): __motor_class_name__ = "MotorCursor" __delegate_class__ = Cursor address = ReadOnlyProperty() collation = MotorCursorChainingMethod() distinct = AsyncRead() explain = AsyncRead() add_option = MotorCursorChainingMethod() remove_option = MotorCursorChainingMethod() limit = MotorCursorChainingMethod() skip = MotorCursorChainingMethod() max_scan = MotorCursorChainingMethod() sort = MotorCursorChainingMethod(doc=docstrings.cursor_sort_doc) hint = MotorCursorChainingMethod() where = MotorCursorChainingMethod(doc=docstrings.where_doc) max_await_time_ms = MotorCursorChainingMethod() max_time_ms = MotorCursorChainingMethod() min = MotorCursorChainingMethod() max = MotorCursorChainingMethod() comment = MotorCursorChainingMethod() allow_disk_use = MotorCursorChainingMethod() _Cursor__die = AsyncRead() def rewind(self): """Rewind this cursor to its unevaluated state.""" self.delegate.rewind() self.started = False return self def clone(self): """Get a clone of this cursor.""" return self.__class__(self.delegate.clone(), self.collection) def __copy__(self): return self.__class__(self.delegate.__copy__(), self.collection) def __deepcopy__(self, memo): return self.__class__(self.delegate.__deepcopy__(memo), self.collection) def _query_flags(self): return self.delegate._Cursor__query_flags def _data(self): return self.delegate._Cursor__data def _killed(self): return self.delegate._Cursor__killed class AgnosticRawBatchCursor(AgnosticCursor): __motor_class_name__ = "MotorRawBatchCursor" __delegate_class__ = RawBatchCursor class AgnosticCommandCursor(AgnosticBaseCursor): __motor_class_name__ = "MotorCommandCursor" __delegate_class__ = CommandCursor _CommandCursor__die = AsyncRead() async def try_next(self): """Advance the cursor without blocking indefinitely. This method returns the next document without waiting indefinitely for data. If no document is cached locally then this method runs a single getMore command. If the getMore yields any documents, the next document is returned, otherwise, if the getMore returns no documents (because there is no additional data) then ``None`` is returned. :Returns: The next document or ``None`` when no document is available after running a single getMore or when the cursor is closed. """ def inner(): return self.delegate.try_next() loop = self.get_io_loop() return await self._framework.run_on_executor(loop, inner) def _query_flags(self): return 0 def _data(self): return self.delegate._CommandCursor__data def _killed(self): return self.delegate._CommandCursor__killed class AgnosticRawBatchCommandCursor(AgnosticCommandCursor): __motor_class_name__ = "MotorRawBatchCommandCursor" __delegate_class__ = RawBatchCommandCursor class _LatentCursor: """Take the place of a PyMongo CommandCursor until aggregate() begins.""" alive = True _CommandCursor__data = [] _CommandCursor__id = None _CommandCursor__killed = False _CommandCursor__sock_mgr = None _CommandCursor__session = None _CommandCursor__explicit_session = None cursor_id = None def __init__(self, collection): self._CommandCursor__collection = collection.delegate def _CommandCursor__end_session(self, *args, **kwargs): pass def _CommandCursor__die(self, *args, **kwargs): pass def clone(self): return _LatentCursor(self._CommandCursor__collection) def rewind(self): pass class AgnosticLatentCommandCursor(AgnosticCommandCursor): __motor_class_name__ = "MotorLatentCommandCursor" def __init__(self, collection, start, *args, **kwargs): # We're being constructed without await, like: # # cursor = collection.aggregate(pipeline) # # ... so we can't send the "aggregate" command to the server and get # a PyMongo CommandCursor back yet. Set self.delegate to a latent # cursor until the first await triggers _get_more(), which # will execute the callback "start", which gets a PyMongo CommandCursor. super().__init__(_LatentCursor(collection), collection) self.start = start self.args = args self.kwargs = kwargs def batch_size(self, batch_size): self.kwargs["batchSize"] = batch_size return self def _get_more(self): if not self.started: self.started = True original_future = self._framework.get_future(self.get_io_loop()) future = self.start(*self.args, **self.kwargs) self.start = self.args = self.kwargs = None self._framework.add_future( self.get_io_loop(), future, self._on_started, original_future ) return original_future return super()._get_more() def _on_started(self, original_future, future): try: # "result" is a PyMongo command cursor from PyMongo's aggregate() or # aggregate_raw_batches(). Set its batch size from our latent # cursor's batch size. pymongo_cursor = future.result() self.delegate = pymongo_cursor except Exception as exc: if not original_future.done(): original_future.set_exception(exc) else: # Return early if the task was cancelled. if original_future.done(): return if self.delegate._CommandCursor__data or not self.delegate.alive: # _get_more is complete. original_future.set_result(len(self.delegate._CommandCursor__data)) else: # Send a getMore. future = super()._get_more() self._framework.chain_future(future, original_future) class AgnosticChangeStream(AgnosticBase): """A change stream cursor. Should not be called directly by application developers. See :meth:`~MotorCollection.watch` for example usage. .. versionadded: 1.2 .. mongodoc:: changeStreams """ __delegate_class__ = ChangeStream __motor_class_name__ = "MotorChangeStream" _close = AsyncCommand(attr_name="close") resume_token = ReadOnlyProperty() def __init__( self, target, pipeline, full_document, resume_after, max_await_time_ms, batch_size, collation, start_at_operation_time, session, start_after, comment, full_document_before_change, show_expanded_events, ): super().__init__(delegate=None) # The "target" object is a client, database, or collection. self._target = target self._kwargs = { "pipeline": pipeline, "full_document": full_document, "resume_after": resume_after, "max_await_time_ms": max_await_time_ms, "batch_size": batch_size, "collation": collation, "start_at_operation_time": start_at_operation_time, "session": session, "start_after": start_after, "comment": comment, "full_document_before_change": full_document_before_change, "show_expanded_events": show_expanded_events, } def _lazy_init(self): if not self.delegate: self.delegate = self._target.delegate.watch(**unwrap_kwargs_session(self._kwargs)) def _try_next(self): # This method is run on a thread. self._lazy_init() return self.delegate.try_next() @property def alive(self): """Does this cursor have the potential to return more data? .. note:: Even if :attr:`alive` is ``True``, :meth:`next` can raise :exc:`StopAsyncIteration` and :meth:`try_next` can return ``None``. """ if not self.delegate: # Not yet fully initialized, so we may return data. return True return self.delegate.alive async def next(self): """Advance the cursor. This method blocks until the next change document is returned or an unrecoverable error is raised. This method is used when iterating over all changes in the cursor. For example:: async def watch_collection(): resume_token = None pipeline = [{'$match': {'operationType': 'insert'}}] try: async with db.collection.watch(pipeline) as stream: async for insert_change in stream: print(insert_change) resume_token = stream.resume_token except pymongo.errors.PyMongoError: # The ChangeStream encountered an unrecoverable error or the # resume attempt failed to recreate the cursor. if resume_token is None: # There is no usable resume token because there was a # failure during ChangeStream initialization. logging.error('...') else: # Use the interrupted ChangeStream's resume token to # create a new ChangeStream. The new stream will # continue from the last seen insert change without # missing any events. async with db.collection.watch( pipeline, resume_after=resume_token) as stream: async for insert_change in stream: print(insert_change) Raises :exc:`StopAsyncIteration` if this change stream is closed. In addition to using an "async for" loop as shown in the code example above, you can also iterate the change stream by calling ``await change_stream.next()`` repeatedly. """ while self.alive: doc = await self.try_next() if doc is not None: return doc raise StopAsyncIteration() async def try_next(self): """Advance the cursor without blocking indefinitely. This method returns the next change document without waiting indefinitely for the next change. If no changes are available, it returns None. For example: .. code-block:: python3 while change_stream.alive: change = await change_stream.try_next() # Note that the ChangeStream's resume token may be updated # even when no changes are returned. print("Current resume token: %r" % (change_stream.resume_token,)) if change is not None: print("Change document: %r" % (change,)) continue # We end up here when there are no recent changes. # Sleep for a while before trying again to avoid flooding # the server with getMore requests when no changes are # available. await asyncio.sleep(10) If no change document is cached locally then this method runs a single getMore command. If the getMore yields any documents, the next document is returned, otherwise, if the getMore returns no documents (because there have been no changes) then ``None`` is returned. :Returns: The next change document or ``None`` when no document is available after running a single getMore or when the cursor is closed. .. versionadded:: 2.1 """ loop = self.get_io_loop() return await self._framework.run_on_executor(loop, self._try_next) async def close(self): """Close this change stream. Stops any "async for" loops using this change stream. """ if self.delegate: await self._close() def __aiter__(self): return self __anext__ = next async def __aenter__(self): if not self.delegate: loop = self.get_io_loop() await self._framework.run_on_executor(loop, self._lazy_init) return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self.delegate: await self.close() def get_io_loop(self): return self._target.get_io_loop() def __enter__(self): raise RuntimeError('Use a change stream in "async with", not "with"') def __exit__(self, exc_type, exc_val, exc_tb): pass class AgnosticClientEncryption(AgnosticBase): """Explicit client-side field level encryption.""" __motor_class_name__ = "MotorClientEncryption" __delegate_class__ = ClientEncryption create_data_key = AsyncCommand(doc=docstrings.create_data_key_doc) encrypt = AsyncCommand() encrypt_expression = AsyncCommand() decrypt = AsyncCommand() close = AsyncCommand(doc=docstrings.close_doc) # Key Management API rewrap_many_data_key = AsyncCommand() delete_key = AsyncCommand() get_key = AsyncCommand() add_key_alt_name = AsyncCommand() get_key_by_alt_name = AsyncCommand() remove_key_alt_name = AsyncCommand() def __init__( self, kms_providers, key_vault_namespace, key_vault_client, codec_options, io_loop=None, kms_tls_options=None, ): """Explicit client-side field level encryption. Takes the same constructor arguments as :class:`pymongo.encryption.ClientEncryption`, as well as: :Parameters: - `io_loop` (optional): Special event loop instance to use instead of default. """ if io_loop: self._framework.check_event_loop(io_loop) else: io_loop = None sync_client = key_vault_client.delegate delegate = self.__delegate_class__( kms_providers, key_vault_namespace, sync_client, codec_options, kms_tls_options ) super().__init__(delegate) self._io_loop = io_loop @property def io_loop(self): if self._io_loop is None: self._io_loop = self._framework.get_event_loop() return self._io_loop def get_io_loop(self): return self.io_loop async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self.delegate: await self.close() def __enter__(self): raise RuntimeError(f'Use {self.__class__.__name__} in "async with", not "with"') def __exit__(self, exc_type, exc_val, exc_tb): pass async def get_keys(self): cursor_class = create_class_with_framework(AgnosticCursor, self._framework, self.__module__) return cursor_class(self.delegate.get_keys(), self) async def create_encrypted_collection( self, database, name, encrypted_fields, kms_provider=None, master_key=None, **kwargs, ): """Create a collection with encryptedFields. .. warning:: This function does not update the encryptedFieldsMap in the client's AutoEncryptionOpts, thus the user must create a new client after calling this function with the encryptedFields returned. Normally collection creation is automatic. This method should only be used to specify options on creation. :class:`~pymongo.errors.EncryptionError` will be raised if the collection already exists. :Parameters: - `database`: the database in which to create a collection - `name`: the name of the collection to create - `encrypted_fields` (dict): Document that describes the encrypted fields for Queryable Encryption. For example:: { "escCollection": "enxcol_.encryptedCollection.esc", "ecocCollection": "enxcol_.encryptedCollection.ecoc", "fields": [ { "path": "firstName", "keyId": Binary.from_uuid(UUID('00000000-0000-0000-0000-000000000000')), "bsonType": "string", "queries": {"queryType": "equality"} }, { "path": "ssn", "keyId": Binary.from_uuid(UUID('04104104-1041-0410-4104-104104104104')), "bsonType": "string" } ] } The "keyId" may be set to ``None`` to auto-generate the data keys. - `kms_provider` (optional): the KMS provider to be used - `master_key` (optional): Identifies a KMS-specific key used to encrypt the new data key. If the kmsProvider is "local" the `master_key` is not applicable and may be omitted. - `**kwargs` (optional): additional keyword arguments are the same as "create_collection". All optional `create collection command`_ parameters should be passed as keyword arguments to this method. See the documentation for :meth:`~pymongo.database.Database.create_collection` for all valid options. :Raises: - :class:`~pymongo.errors.EncryptedCollectionError`: When either data-key creation or creating the collection fails. .. versionadded:: 3.2 .. _create collection command: https://mongodb.com/docs/manual/reference/command/create """ collection_class = create_class_with_framework( AgnosticCollection, self._framework, self.__module__ ) loop = self.get_io_loop() coll, ef = await self._framework.run_on_executor( loop, self.delegate.create_encrypted_collection, database=database.delegate, name=name, encrypted_fields=encrypted_fields, kms_provider=kms_provider, master_key=master_key, **kwargs, ) return collection_class(database, coll.name, _delegate=coll), ef motor-3.4.0/motor/core.pyi000066400000000000000000001016621460060525600155030ustar00rootroot00000000000000# 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 typing import ( Any, Callable, Coroutine, Generic, Iterable, Mapping, MutableMapping, NoReturn, Optional, Sequence, 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.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 _T, ClientSession, SessionOptions, TransactionOptions from pymongo.collection import Collection, ReturnDocument, _WriteOp # noqa: F401 from pymongo.command_cursor import CommandCursor, RawBatchCommandCursor from pymongo.cursor import Cursor, RawBatchCursor, _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, DeleteResult, InsertManyResult, InsertOneResult, UpdateResult, ) 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], 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]: ... 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: ... 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, *args: Any, **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: ... def __getattr__(self, name: str) -> AgnosticDatabase: ... def __getitem__(self, name: str) -> AgnosticDatabase: ... 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: ... @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: ... 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], 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: ... 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: ... @property def name(self) -> str: ... async def validate_collection( self, name_or_collection: Union[str, AgnosticCollection], 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: ... async def _async_aggregate( self, pipeline: _Pipeline, session: Optional[AgnosticClientSession] = None, **kwargs: Any ) -> AgnosticCommandCursor: ... def __init__(self, client: AgnosticClient, name: str, **kwargs: Any) -> None: ... def aggregate( self, pipeline: _Pipeline, *args: Any, **kwargs: Any ) -> AgnosticLatentCommandCursor: ... 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: ... @property def client(self) -> AgnosticClient: ... def __getattr__(self, name: str) -> AgnosticCollection: ... def __getitem__(self, name: str) -> AgnosticCollection: ... 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[Mapping[str, Any]]: ... def list_search_indexes( self, name: Optional[str] = None, session: Optional[AgnosticClientSession] = None, comment: Optional[Any] = None, **kwargs: Any, ) -> AgnosticLatentCommandCursor: ... 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: ... def __getitem__(self, name: str) -> AgnosticCollection: ... def __call__(self, *args: Any, **kwargs: Any) -> Any: ... def find(self, *args: Any, **kwargs: Any) -> AgnosticCursor: ... def find_raw_batches(self, *args: Any, **kwargs: Any) -> AgnosticCursor: ... def aggregate( self, pipeline: _Pipeline, *args: Any, **kwargs: Any ) -> AgnosticCommandCursor: ... def aggregate_raw_batches( self, pipeline: _Pipeline, **kwargs: Any ) -> AgnosticCommandCursor: ... 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: ... def wrap(self, obj: Any) -> Any: ... def get_io_loop(self) -> Any: ... class AgnosticBaseCursor(AgnosticBase): def __init__( self, cursor: Union[Cursor, CommandCursor, _LatentCursor], collection: AgnosticCollection ) -> 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: Union[int, None]) -> Future[list]: ... 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: ... 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): __motor_class_name__: str __delegate_class__: type[Cursor] def collation(self, collation: Optional[_CollationIn]) -> AgnosticCursor: ... async def distinct(self, key: str) -> list: ... async def explain(self) -> _DocumentType: ... def add_option(self, mask: int) -> AgnosticCursor: ... def remove_option(self, mask: int) -> AgnosticCursor: ... def limit(self, limit: int) -> AgnosticCursor: ... def skip(self, skip: int) -> AgnosticCursor: ... def max_scan(self, max_scan: Optional[int]) -> AgnosticCursor: ... def sort( self, key_or_list: _Hint, direction: Optional[Union[int, str]] = None ) -> AgnosticCursor: ... def hint(self, index: Optional[_Hint]) -> AgnosticCursor: ... def where(self, code: Union[str, Code]) -> AgnosticCursor: ... def max_await_time_ms(self, max_await_time_ms: Optional[int]) -> AgnosticCursor: ... def max_time_ms(self, max_time_ms: Optional[int]) -> AgnosticCursor: ... def min(self, spec: _Sort) -> AgnosticCursor: ... def max(self, spec: _Sort) -> AgnosticCursor: ... def comment(self, comment: Any) -> AgnosticCursor: ... def allow_disk_use(self, allow_disk_use: bool) -> AgnosticCursor: ... def rewind(self) -> AgnosticCursor: ... def clone(self) -> AgnosticCursor: ... def __copy__(self) -> AgnosticCursor: ... def __deepcopy__(self, memo: Any) -> AgnosticCursor: ... def _query_flags(self) -> int: ... def _data(self) -> Any: ... def _killed(self) -> Any: ... class AgnosticRawBatchCursor(AgnosticCursor): __motor_class_name__: str __delegate_class__: type[RawBatchCursor] class AgnosticCommandCursor(AgnosticBaseCursor): __motor_class_name__: str __delegate_class__: type[CommandCursor] def _query_flags(self) -> int: ... def _data(self) -> Any: ... def _killed(self) -> Any: ... class AgnosticRawBatchCommandCursor(AgnosticCommandCursor): __motor_class_name__: str __delegate_class__: type[RawBatchCommandCursor] class _LatentCursor: def __init__(self, collection: AgnosticCollection): ... def _CommandCursor__end_session(self, *args: Any, **kwargs: Any) -> None: ... def _CommandCursor__die(self, *args: Any, **kwargs: Any) -> None: ... def clone(self) -> _LatentCursor: ... def rewind(self) -> _LatentCursor: ... class AgnosticLatentCommandCursor(AgnosticCommandCursor): __motor_class_name__: str def __init__(self, collection: AgnosticCollection, start: Any, *args: Any, **kwargs: Any): ... def _on_started(self, original_future: Any, future: Any) -> None: ... class AgnosticChangeStream(AgnosticBase): __motor_class_name__: str __delegate_class__: type[ChangeStream] async def _close(self) -> None: ... 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: ... __anext__ = next async def __aenter__(self) -> AgnosticChangeStream: ... 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): __motor_class_name__: str __delegate_class__: type[ClientEncryption] def __init__( self, kms_providers: Mapping[str, Any], key_vault_namespace: str, key_vault_client: AgnosticClient, 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: ... 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: ... async def create_encrypted_collection( self, database: AgnosticDatabase, name: str, encrypted_fields: Mapping[str, Any], kms_provider: Optional[str] = None, master_key: Optional[Mapping[str, Any]] = None, **kwargs: Any, ) -> tuple[AgnosticCollection, Mapping[str, Any]]: ... motor-3.4.0/motor/docstrings.py000066400000000000000000001643371460060525600165710ustar00rootroot00000000000000# Copyright 2016 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. get_database_doc = """ Get a :class:`MotorDatabase` with the given name and options. Useful for creating a :class:`MotorDatabase` with different codec options, read preference, and/or write concern from this :class:`MotorClient`. >>> from pymongo import ReadPreference >>> client.read_preference == ReadPreference.PRIMARY True >>> db1 = client.test >>> db1.read_preference == ReadPreference.PRIMARY True >>> db2 = client.get_database( ... 'test', read_preference=ReadPreference.SECONDARY) >>> db2.read_preference == ReadPreference.SECONDARY True :Parameters: - `name`: The name of the database - a string. - `codec_options` (optional): An instance of :class:`~bson.codec_options.CodecOptions`. If ``None`` (the default) the :attr:`codec_options` of this :class:`MotorClient` is used. - `read_preference` (optional): The read preference to use. If ``None`` (the default) the :attr:`read_preference` of this :class:`MotorClient` is used. See :mod:`~pymongo.read_preferences` for options. - `write_concern` (optional): An instance of :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the default) the :attr:`write_concern` of this :class:`MotorClient` is used. """ get_default_database_doc = """ Get the database named in the MongoDB connection URI. >>> uri = 'mongodb://host/my_database' >>> client = MotorClient(uri) >>> db = client.get_default_database() >>> assert db.name == 'my_database' >>> db = client.get_default_database('fallback_db_name') >>> assert db.name == 'my_database' >>> uri_without_database = 'mongodb://host/' >>> client = MotorClient(uri_without_database) >>> db = client.get_default_database('fallback_db_name') >>> assert db.name == 'fallback_db_name' Useful in scripts where you want to choose which database to use based only on the URI in a configuration file. :Parameters: - `default` (optional): the database name to use if no database name was provided in the URI. - `codec_options` (optional): An instance of :class:`~bson.codec_options.CodecOptions`. If ``None`` (the default) the :attr:`codec_options` of this :class:`MotorClient` is used. - `read_preference` (optional): The read preference to use. If ``None`` (the default) the :attr:`read_preference` of this :class:`MotorClient` is used. See :mod:`~pymongo.read_preferences` for options. - `write_concern` (optional): An instance of :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the default) the :attr:`write_concern` of this :class:`MotorClient` is used. - `read_concern` (optional): An instance of :class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the default) the :attr:`read_concern` of this :class:`MotorClient` is used. - `comment` (optional): A user-provided comment to attach to this command. .. versionchanged:: 3.0 Added ``comment`` parameter. .. versionadded:: 2.1 Revived this method. Added the ``default``, ``codec_options``, ``read_preference``, ``write_concern`` and ``read_concern`` parameters. .. versionchanged:: 2.0 Removed this method. """ list_collection_names_doc = """ Get a list of all the collection names in this database. For example, to list all non-system collections:: filter = {"name": {"$regex": r"^(?!system\\.)"}} names = await db.list_collection_names(filter=filter) :Parameters: - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `filter` (optional): A query document to filter the list of collections returned from the listCollections command. - `comment` (optional): A user-provided comment to attach to this command. - `**kwargs` (optional): Optional parameters of the `listCollections `_ command. can be passed as keyword arguments to this method. The supported options differ by server version. .. versionchanged:: 3.0 Added the ``comment`` parameter. .. versionchanged:: 2.1 Added the ``filter`` and ``**kwargs`` parameters. .. versionadded:: 1.2 """ bulk_write_doc = """Send a batch of write operations to the server. Requests are passed as a list of write operation instances imported from :mod:`pymongo`: :class:`~pymongo.operations.InsertOne`, :class:`~pymongo.operations.UpdateOne`, :class:`~pymongo.operations.UpdateMany`, :class:`~pymongo.operations.ReplaceOne`, :class:`~pymongo.operations.DeleteOne`, or :class:`~pymongo.operations.DeleteMany`). For example, say we have these documents:: {'x': 1, '_id': ObjectId('54f62e60fba5226811f634ef')} {'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')} We can insert a document, delete one, and replace one like so:: # DeleteMany, UpdateOne, and UpdateMany are also available. from pymongo import InsertOne, DeleteOne, ReplaceOne async def modify_data(): requests = [InsertOne({'y': 1}), DeleteOne({'x': 1}), ReplaceOne({'w': 1}, {'z': 1}, upsert=True)] result = await db.test.bulk_write(requests) print("inserted %d, deleted %d, modified %d" % ( result.inserted_count, result.deleted_count, result.modified_count)) print("upserted_ids: %s" % result.upserted_ids) print("collection:") async for doc in db.test.find(): print(doc) This will print something like:: inserted 1, deleted 1, modified 0 upserted_ids: {2: ObjectId('54f62ee28891e756a6e1abd5')} collection: {'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')} {'y': 1, '_id': ObjectId('54f62ee2fba5226811f634f1')} {'z': 1, '_id': ObjectId('54f62ee28891e756a6e1abd5')} :Parameters: - `requests`: A list of write operations (see examples above). - `ordered` (optional): If ``True`` (the default) requests will be performed on the server serially, in the order provided. If an error occurs all remaining operations are aborted. If ``False`` requests will be performed on the server in arbitrary order, possibly in parallel, and all operations will be attempted. - `bypass_document_validation`: (optional) If ``True``, allows the write to opt-out of document level validation. Default is ``False``. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `comment` (optional): A user-provided comment to attach to this command. :Returns: An instance of :class:`~pymongo.results.BulkWriteResult`. .. seealso:: :ref:`writes-and-ids` .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 3.0 Added comment parameter. .. versionchanged:: 1.2 Added session parameter. """ create_index_doc = """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`: Used to create an expiring (TTL) collection. MongoDB will automatically delete documents from this collection after 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`. 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. :Parameters: - `keys`: a single key or a list of (key, direction) pairs specifying the index to create. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `comment` (optional): A user-provided comment to attach to this command. - `**kwargs` (optional): any additional index creation options (see the above list) should be passed as keyword arguments Returns a Future. .. mongodoc:: indexes """ create_indexes_doc = """Create one or more indexes on this collection:: from pymongo import IndexModel, ASCENDING, DESCENDING async def create_two_indexes(): index1 = IndexModel([("hello", DESCENDING), ("world", ASCENDING)], name="hello_world") index2 = IndexModel([("goodbye", DESCENDING)]) print(await db.test.create_indexes([index1, index2])) This prints:: ['hello_world', 'goodbye_-1'] :Parameters: - `indexes`: A list of :class:`~pymongo.operations.IndexModel` instances. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `comment` (optional): A user-provided comment to attach to this command. - `**kwargs` (optional): optional arguments to the createIndexes command (like maxTimeMS) can be passed as keyword arguments. The :attr:`~pymongo.collection.Collection.write_concern` of this collection is automatically applied to this operation. .. versionchanged:: 3.0 Added comment parameter. .. versionchanged:: 1.2 Added session parameter. """ cmd_doc = """Issue a MongoDB command. Send command ``command`` to the database and return the response. If ``command`` is a string then the command ``{command: value}`` will be sent. Otherwise, ``command`` must be a :class:`dict` and will be sent as-is. Additional keyword arguments are added to the final command document before it is sent. For example, a command like ``{buildinfo: 1}`` can be sent using:: result = await db.command("buildinfo") For a command where the value matters, like ``{count: collection_name}`` we can do:: result = await db.command("count", collection_name) For commands that take additional arguments we can use kwargs. So ``{count: collection_name, query: query}`` becomes:: result = await db.command("count", collection_name, query=query) :Parameters: - `command`: document representing the command to be issued, or the name of the command (for simple commands only). .. note:: the order of keys in the `command` document is significant (the "verb" must come first), so commands which require multiple keys (e.g. `findandmodify`) should use an instance of :class:`~bson.son.SON` or a string and kwargs instead of a Python :class:`dict`. - `value` (optional): value to use for the command verb when `command` is passed as a string - `check` (optional): check the response for errors, raising :class:`~pymongo.errors.OperationFailure` if there are any - `allowable_errors`: if `check` is ``True``, error messages in this list will be ignored by error-checking - `read_preference`: The read preference for this operation. See :mod:`~pymongo.read_preferences` for options. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `comment` (optional): A user-provided comment to attach to this command. - `**kwargs` (optional): additional keyword arguments will be added to the command document before it is sent .. versionchanged:: 3.0 Added comment parameter. .. versionchanged:: 1.2 Added session parameter. .. mongodoc:: commands """ delete_many_doc = """Delete one or more documents matching the filter. If we have a collection with 3 documents like ``{'x': 1}``, then:: async def clear_collection(): result = await db.test.delete_many({'x': 1}) print(result.deleted_count) This deletes all matching documents and prints "3". :Parameters: - `filter`: A query that matches the documents to delete. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. - `hint` (optional): An index used to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. :Returns: - An instance of :class:`~pymongo.results.DeleteResult`. .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``session`` parameter. """ delete_one_doc = """Delete a single document matching the filter. If we have a collection with 3 documents like ``{'x': 1}``, then:: async def clear_collection(): result = await db.test.delete_one({'x': 1}) print(result.deleted_count) This deletes one matching document and prints "1". :Parameters: - `filter`: A query that matches the document to delete. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. - `hint` (optional): An index used to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. :Returns: - An instance of :class:`~pymongo.results.DeleteResult`. .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``session`` parameter. """ drop_doc = """Alias for ``drop_collection``. The following two calls are equivalent:: await db.foo.drop() await db.drop_collection("foo") """ find_one_doc = """Get a single document from the database. All arguments to :meth:`find` are also valid arguments for :meth:`find_one`, although any `limit` argument will be ignored. Returns a single document, or ``None`` if no matching document is found. The :meth:`find_one` method obeys the :attr:`read_preference` of this Motor collection instance. :Parameters: - `filter` (optional): a dictionary specifying the query to be performed OR any other type to be used as the value for a query for ``"_id"``. - `*args` (optional): any additional positional arguments are the same as the arguments to :meth:`find`. - `**kwargs` (optional): any additional keyword arguments are the same as the arguments to :meth:`find`. - `max_time_ms` (optional): a value for max_time_ms may be specified as part of `**kwargs`, e.g.: .. code-block:: python3 await collection.find_one(max_time_ms=100) .. versionchanged:: 1.2 Added session parameter. """ find_one_and_delete_doc = """Finds a single document and deletes it, returning the document. If we have a collection with 2 documents like ``{'x': 1}``, then this code retrieves and deletes one of them:: async def delete_one_document(): print(await db.test.count_documents({'x': 1})) doc = await db.test.find_one_and_delete({'x': 1}) print(doc) print(await db.test.count_documents({'x': 1})) This outputs something like:: 2 {'x': 1, '_id': ObjectId('54f4e12bfba5220aa4d6dee8')} 1 If multiple documents match *filter*, a *sort* can be applied. Say we have 3 documents like:: {'x': 1, '_id': 0} {'x': 1, '_id': 1} {'x': 1, '_id': 2} This code retrieves and deletes the document with the largest ``_id``:: async def delete_with_largest_id(): doc = await db.test.find_one_and_delete( {'x': 1}, sort=[('_id', pymongo.DESCENDING)]) This deletes one document and prints it:: {'x': 1, '_id': 2} The *projection* option can be used to limit the fields returned:: async def delete_and_return_x(): db.test.find_one_and_delete({'x': 1}, projection={'_id': False}) This prints:: {'x': 1} :Parameters: - `filter`: A query that matches the document to delete. - `projection` (optional): a list of field names that should be returned in the result document or a mapping specifying the fields to include or exclude. If `projection` is a list "_id" will always be returned. Use a mapping to exclude fields from the result (e.g. projection={'_id': False}). - `sort` (optional): a list of (key, direction) pairs specifying the sort order for the query. If multiple documents match the query, they are sorted and the first is deleted. - `hint` (optional): An index used to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). This command uses the :class:`~pymongo.write_concern.WriteConcern` of this :class:`~pymongo.collection.Collection` when connected to MongoDB >= 3.2. Note that using an elevated write concern with this command may be slower compared to using the default write concern. .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``session`` parameter. """ find_one_and_replace_doc = """Finds a single document and replaces it, returning either the original or the replaced document. The :meth:`find_one_and_replace` method differs from :meth:`find_one_and_update` by replacing the document matched by *filter*, rather than modifying the existing document. Say we have 3 documents like:: {'x': 1, '_id': 0} {'x': 1, '_id': 1} {'x': 1, '_id': 2} Replace one of them like so:: async def replace_one_doc(): original_doc = await db.test.find_one_and_replace({'x': 1}, {'y': 1}) print("original: %s" % original_doc) print("collection:") async for doc in db.test.find(): print(doc) This will print:: original: {'x': 1, '_id': 0} collection: {'y': 1, '_id': 0} {'x': 1, '_id': 1} {'x': 1, '_id': 2} :Parameters: - `filter`: A query that matches the document to replace. - `replacement`: The replacement document. - `projection` (optional): A list of field names that should be returned in the result document or a mapping specifying the fields to include or exclude. If `projection` is a list "_id" will always be returned. Use a mapping to exclude fields from the result (e.g. projection={'_id': False}). - `sort` (optional): a list of (key, direction) pairs specifying the sort order for the query. If multiple documents match the query, they are sorted and the first is replaced. - `upsert` (optional): When ``True``, inserts a new document if no document matches the query. Defaults to ``False``. - `return_document`: If :attr:`ReturnDocument.BEFORE` (the default), returns the original document before it was replaced, or ``None`` if no document matches. If :attr:`ReturnDocument.AFTER`, returns the replaced or inserted document. - `hint` (optional): An index to use to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). This command uses the :class:`~pymongo.write_concern.WriteConcern` of this :class:`~pymongo.collection.Collection` when connected to MongoDB >= 3.2. Note that using an elevated write concern with this command may be slower compared to using the default write concern. .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``session`` parameter. """ find_one_and_update_doc = """Finds a single document and updates it, returning either the original or the updated document. By default :meth:`find_one_and_update` returns the original version of the document before the update was applied:: async def set_done(): print(await db.test.find_one_and_update( {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})) This outputs:: {'_id': 665, 'done': False, 'count': 25}} To return the updated version of the document instead, use the *return_document* option. :: from pymongo import ReturnDocument async def increment_by_userid(): print(await db.example.find_one_and_update( {'_id': 'userid'}, {'$inc': {'seq': 1}}, return_document=ReturnDocument.AFTER)) This prints:: {'_id': 'userid', 'seq': 1} You can limit the fields returned with the *projection* option. :: async def increment_by_userid(): print(await db.example.find_one_and_update( {'_id': 'userid'}, {'$inc': {'seq': 1}}, projection={'seq': True, '_id': False}, return_document=ReturnDocument.AFTER)) This results in:: {'seq': 2} The *upsert* option can be used to create the document if it doesn't already exist. :: async def increment_by_userid(): print(await db.example.find_one_and_update( {'_id': 'userid'}, {'$inc': {'seq': 1}}, projection={'seq': True, '_id': False}, upsert=True, return_document=ReturnDocument.AFTER)) The result:: {'seq': 1} If multiple documents match *filter*, a *sort* can be applied. Say we have these two documents:: {'_id': 665, 'done': True, 'result': {'count': 26}} {'_id': 701, 'done': True, 'result': {'count': 17}} Then to update the one with the great ``_id``:: async def set_done(): print(await db.test.find_one_and_update( {'done': True}, {'$set': {'final': True}}, sort=[('_id', pymongo.DESCENDING)])) This would print:: {'_id': 701, 'done': True, 'result': {'count': 17}} :Parameters: - `filter`: A query that matches the document to update. - `update`: The update operations to apply. - `projection` (optional): A list of field names that should be returned in the result document or a mapping specifying the fields to include or exclude. If `projection` is a list "_id" will always be returned. Use a dict to exclude fields from the result (e.g. projection={'_id': False}). - `sort` (optional): a list of (key, direction) pairs specifying the sort order for the query. If multiple documents match the query, they are sorted and the first is updated. - `upsert` (optional): When ``True``, inserts a new document if no document matches the query. Defaults to ``False``. - `return_document`: If :attr:`ReturnDocument.BEFORE` (the default), returns the original document before it was updated, or ``None`` if no document matches. If :attr:`ReturnDocument.AFTER`, returns the updated or inserted document. - `array_filters` (optional): A list of filters specifying which array elements an update should apply. Requires MongoDB 3.6+. - `hint` (optional): An index to use to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). This command uses the :class:`~pymongo.write_concern.WriteConcern` of this :class:`~pymongo.collection.Collection` when connected to MongoDB >= 3.2. Note that using an elevated write concern with this command may be slower compared to using the default write concern. .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``array_filters`` and ``session`` parameters. """ index_information_doc = """Get information on this collection's indexes. Returns a dictionary where the keys are index names (as returned by create_index()) and the values are dictionaries containing information about each index. The dictionary is guaranteed to contain at least a single key, ``"key"`` which is a list of (key, direction) pairs specifying the index (as passed to create_index()). It will also contain any other metadata about the indexes, except for the ``"ns"`` and ``"name"`` keys, which are cleaned. For example:: async def create_x_index(): print(await db.test.create_index("x", unique=True)) print(await db.test.index_information()) This prints:: 'x_1' {'_id_': {'key': [('_id', 1)]}, 'x_1': {'unique': True, 'key': [('x', 1)]}} .. versionchanged:: 3.0 Added comment parameter. .. versionchanged:: 1.2 Added session parameter. """ insert_many_doc = """Insert an iterable of documents. :: async def insert_2_docs(): result = db.test.insert_many([{'x': i} for i in range(2)]) result.inserted_ids This prints something like:: [ObjectId('54f113fffba522406c9cc20e'), ObjectId('54f113fffba522406c9cc20f')] :Parameters: - `documents`: A iterable of documents to insert. - `ordered` (optional): If ``True`` (the default) documents will be inserted on the server serially, in the order provided. If an error occurs all remaining inserts are aborted. If ``False``, documents will be inserted on the server in arbitrary order, possibly in parallel, and all document inserts will be attempted. - `bypass_document_validation`: (optional) If ``True``, allows the write to opt-out of document level validation. Default is ``False``. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `comment` (optional): A user-provided comment to attach to this command. :Returns: An instance of :class:`~pymongo.results.InsertManyResult`. .. seealso:: :ref:`writes-and-ids` .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 3.0 Added comment parameter. .. versionchanged:: 1.2 Added session parameter. """ insert_one_doc = """Insert a single document. :: async def insert_x(): result = await db.test.insert_one({'x': 1}) print(result.inserted_id) This code outputs the new document's ``_id``:: ObjectId('54f112defba522406c9cc208') :Parameters: - `document`: The document to insert. Must be a mutable mapping type. If the document does not have an _id field one will be added automatically. - `bypass_document_validation`: (optional) If ``True``, allows the write to opt-out of document level validation. Default is ``False``. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `comment` (optional): A user-provided comment to attach to this command. :Returns: - An instance of :class:`~pymongo.results.InsertOneResult`. .. seealso:: :ref:`writes-and-ids` .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 3.0 Added comment parameter. .. versionchanged:: 1.2 Added session parameter. """ mr_doc = """Perform a map/reduce operation on this collection. If `full_response` is ``False`` (default) returns a :class:`MotorCollection` instance containing the results of the operation. Otherwise, returns the full response from the server to the `map reduce command`_. :Parameters: - `map`: map function (as a JavaScript string) - `reduce`: reduce function (as a JavaScript string) - `out`: output collection name or `out object` (dict). See the `map reduce command`_ documentation for available options. Note: `out` options are order sensitive. :class:`~bson.son.SON` can be used to specify multiple options. e.g. SON([('replace', ), ('db', )]) - `full_response` (optional): if ``True``, return full response to this command - otherwise just return the result collection - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `**kwargs` (optional): additional arguments to the `map reduce command`_ may be passed as keyword arguments to this helper method, e.g.:: result = await db.test.map_reduce(map, reduce, "myresults", limit=2) Returns a Future. .. note:: The :meth:`map_reduce` method does **not** obey the :attr:`read_preference` of this :class:`MotorCollection`. To run mapReduce on a secondary use the :meth:`inline_map_reduce` method instead. .. _map reduce command: https://mongodb.com/docs/manual/reference/command/mapReduce/ .. mongodoc:: mapreduce .. versionchanged:: 1.2 Added session parameter. """ replace_one_doc = """Replace a single document matching the filter. Say our collection has one document:: {'x': 1, '_id': ObjectId('54f4c5befba5220aa4d6dee7')} Then to replace it with another:: async def_replace_x_with_y(): result = await db.test.replace_one({'x': 1}, {'y': 1}) print('matched %d, modified %d' % (result.matched_count, result.modified_count)) print('collection:') async for doc in db.test.find(): print(doc) This prints:: matched 1, modified 1 collection: {'y': 1, '_id': ObjectId('54f4c5befba5220aa4d6dee7')} The *upsert* option can be used to insert a new document if a matching document does not exist:: async def_replace_or_upsert(): result = await db.test.replace_one({'x': 1}, {'x': 1}, True) print('matched %d, modified %d, upserted_id %r' % (result.matched_count, result.modified_count, result.upserted_id)) print('collection:') async for doc in db.test.find(): print(doc) This prints:: matched 1, modified 1, upserted_id ObjectId('54f11e5c8891e756a6e1abd4') collection: {'y': 1, '_id': ObjectId('54f4c5befba5220aa4d6dee7')} :Parameters: - `filter`: A query that matches the document to replace. - `replacement`: The new document. - `upsert` (optional): If ``True``, perform an insert if no documents match the filter. - `bypass_document_validation`: (optional) If ``True``, allows the write to opt-out of document level validation. Default is ``False``. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. - `hint` (optional): An index to use to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``session`` parameter. """ update_many_doc = """Update one or more documents that match the filter. Say our collection has 3 documents:: {'x': 1, '_id': 0} {'x': 1, '_id': 1} {'x': 1, '_id': 2} We can add 3 to each "x" field:: async def add_3_to_x(): result = await db.test.update_many({'x': 1}, {'$inc': {'x': 3}}) print('matched %d, modified %d' % (result.matched_count, result.modified_count)) print('collection:') async for doc in db.test.find(): print(doc) This prints:: matched 3, modified 3 collection: {'x': 4, '_id': 0} {'x': 4, '_id': 1} {'x': 4, '_id': 2} :Parameters: - `filter`: A query that matches the documents to update. - `update`: The modifications to apply. - `upsert` (optional): If ``True``, perform an insert if no documents match the filter. - `bypass_document_validation` (optional): If ``True``, allows the write to opt-out of document level validation. Default is ``False``. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. - `array_filters` (optional): A list of filters specifying which array elements an update should apply. Requires MongoDB 3.6+. - `hint` (optional): An index to use to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``array_filters`` and ``session`` parameters. """ update_one_doc = """Update a single document matching the filter. Say our collection has 3 documents:: {'x': 1, '_id': 0} {'x': 1, '_id': 1} {'x': 1, '_id': 2} We can add 3 to the "x" field of one of the documents:: async def add_3_to_x(): result = await db.test.update_one({'x': 1}, {'$inc': {'x': 3}}) print('matched %d, modified %d' % (result.matched_count, result.modified_count)) print('collection:') async for doc in db.test.find(): print(doc) This prints:: matched 1, modified 1 collection: {'x': 4, '_id': 0} {'x': 1, '_id': 1} {'x': 1, '_id': 2} :Parameters: - `filter`: A query that matches the document to update. - `update`: The modifications to apply. - `upsert` (optional): If ``True``, perform an insert if no documents match the filter. - `bypass_document_validation`: (optional) If ``True``, allows the write to opt-out of document level validation. Default is ``False``. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. - `array_filters` (optional): A list of filters specifying which array elements an update should apply. Requires MongoDB 3.6+. - `hint` (optional): An index to use to support the query predicate specified either by its string name, or in the same format as passed to :meth:`~MotorDatabase.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `let` (optional): Map of parameter names and values. Values must be constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - `comment` (optional): A user-provided comment to attach to this command. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 3.0 Added ``let`` and ``comment`` parameters. .. versionchanged:: 2.2 Added ``hint`` parameter. .. versionchanged:: 1.2 Added ``array_filters`` and ``session`` parameters. """ cursor_sort_doc = """Sorts this cursor's results. Pass a field name and a direction, either :data:`~pymongo.ASCENDING` or :data:`~pymongo.DESCENDING`: .. testsetup:: sort MongoClient().test.test_collection.drop() MongoClient().test.test_collection.insert_many( [{"_id": i, "field1": i % 2, "field2": i} for i in range(5)] ) collection = MotorClient().test.test_collection .. doctest:: sort >>> async def f(): ... cursor = collection.find().sort("_id", pymongo.DESCENDING) ... docs = await cursor.to_list(None) ... print([d["_id"] for d in docs]) ... >>> IOLoop.current().run_sync(f) [4, 3, 2, 1, 0] To sort by multiple fields, pass a list of (key, direction) pairs: .. doctest:: sort >>> async def f(): ... cursor = collection.find().sort( ... [("field1", pymongo.ASCENDING), ("field2", pymongo.DESCENDING)] ... ) ... docs = await cursor.to_list(None) ... print([(d["field1"], d["field2"]) for d in docs]) ... >>> IOLoop.current().run_sync(f) [(0, 4), (0, 2), (0, 0), (1, 3), (1, 1)] Text search results can be sorted by relevance: .. testsetup:: sort_text MongoClient().test.test_collection.drop() MongoClient().test.test_collection.insert_many( [{"field": "words"}, {"field": "words about some words"}] ) MongoClient().test.test_collection.create_index([("field", "text")]) collection = MotorClient().test.test_collection .. doctest:: sort_text >>> async def f(): ... cursor = collection.find( ... {"$text": {"$search": "some words"}}, {"score": {"$meta": "textScore"}} ... ) ... # Sort by 'score' field. ... cursor.sort([("score", {"$meta": "textScore"})]) ... async for doc in cursor: ... print("%.1f %s" % (doc["score"], doc["field"])) ... >>> IOLoop.current().run_sync(f) 1.5 words about some words 1.0 words Raises :class:`~pymongo.errors.InvalidOperation` if this cursor has already been used. Only the last :meth:`sort` applied to this cursor has any effect. :Parameters: - `key_or_list`: a single key or a list of (key, direction) pairs specifying the keys to sort on - `direction` (optional): only used if `key_or_list` is a single key, if not given :data:`~pymongo.ASCENDING` is assumed """ start_session_doc = """Start a logical session. This method takes the same parameters as PyMongo's :class:`~pymongo.client_session.SessionOptions`. See the :mod:`~pymongo.client_session` module for details. This session is created uninitialized, use it in an ``await`` expression to initialize it, or an ``async with`` statement. .. code-block:: python3 async def coro(): collection = client.db.collection # End the session after using it. s = await client.start_session() await s.end_session() # Or, use an "async with" statement to end the session # automatically. async with await client.start_session() as s: doc = {"_id": ObjectId(), "x": 1} await collection.insert_one(doc, session=s) secondary = collection.with_options(read_preference=ReadPreference.SECONDARY) # Sessions are causally consistent by default, so we can read # the doc we just inserted, even reading from a secondary. async for doc in secondary.find(session=s): print(doc) # Run a multi-document transaction: async with await client.start_session() as s: # Note, start_transaction doesn't require "await". async with s.start_transaction(): await collection.delete_one({"x": 1}, session=s) await collection.insert_one({"x": 2}, session=s) # Exiting the "with s.start_transaction()" block while throwing an # exception automatically aborts the transaction, exiting the block # normally automatically commits it. # You can run additional transactions in the same session, so long as # you run them one at a time. async with s.start_transaction(): await collection.insert_one({"x": 3}, session=s) await collection.insert_many( {"x": {"$gte": 2}}, {"$inc": {"x": 1}}, session=s ) Requires MongoDB 3.6. Do **not** use the same session for multiple operations concurrently. A :class:`~MotorClientSession` may only be used with the MotorClient that started it. :Returns: An instance of :class:`~MotorClientSession`. .. versionchanged:: 2.0 Returns a :class:`~MotorClientSession`. Before, this method returned a PyMongo :class:`~pymongo.client_session.ClientSession`. .. versionadded:: 1.2 """ where_doc = """Adds a `$where`_ clause to this query. The `code` argument must be an instance of :class:`str` :class:`~bson.code.Code` containing a JavaScript expression. This expression will be evaluated for each document scanned. Only those documents for which the expression evaluates to *true* will be returned as results. The keyword *this* refers to the object currently being scanned. For example:: # Find all documents where field "a" is less than "b" plus "c". async for doc in db.test.find().where('this.a < (this.b + this.c)'): print(doc) Raises :class:`TypeError` if `code` is not an instance of :class:`str`. Raises :class:`~pymongo.errors.InvalidOperation` if this :class:`~motor.motor_tornado.MotorCursor` has already been used. Only the last call to :meth:`where` applied to a :class:`~motor.motor_tornado.MotorCursor` has any effect. .. note:: MongoDB 4.4 drops support for :class:`~bson.code.Code` with scope variables. Consider using `$expr`_ instead. :Parameters: - `code`: JavaScript expression to use as a filter .. _$expr: https://mongodb.com/docs/manual/reference/operator/query/expr/ .. _$where: https://mongodb.com/docs/manual/reference/operator/query/where/ """ create_data_key_doc = """Create and insert a new data key into the key vault collection. Takes the same arguments as :class:`pymongo.encryption.ClientEncryption.create_data_key`, with only the following slight difference using async syntax. The following example shows creating and referring to a data key by alternate name:: await client_encryption.create_data_key("local", keyAltNames=["name1"]) # reference the key with the alternate name await client_encryption.encrypt("457-55-5462", keyAltName="name1", algorithm=Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) """ close_doc = """Release resources. Note that using this class in a with-statement will automatically call :meth:`close`:: async with AsyncIOMotorClientEncryption(...) as client_encryption: encrypted = await client_encryption.encrypt(value, ...) decrypted = await client_encryption.decrypt(encrypted) """ gridfs_delete_doc = """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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. """ gridfs_download_to_stream_doc = """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`. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. """ gridfs_download_to_stream_by_name_doc = """ 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). - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. :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 """ gridfs_open_download_stream_doc = """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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. Returns a :class:`AsyncIOMotorGridOut`. """ gridfs_open_download_stream_by_name_doc = """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). - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. 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 """ gridfs_open_upload_stream_doc = """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 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') :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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. """ gridfs_open_upload_stream_with_id_doc = """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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. """ gridfs_rename_doc = """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. """ gridfs_upload_from_stream_doc = """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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. Returns the _id of the uploaded file. """ gridfs_upload_from_stream_with_id_doc = """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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. """ motor-3.4.0/motor/frameworks/000077500000000000000000000000001460060525600162025ustar00rootroot00000000000000motor-3.4.0/motor/frameworks/__init__.py000066400000000000000000000000001460060525600203010ustar00rootroot00000000000000motor-3.4.0/motor/frameworks/asyncio/000077500000000000000000000000001460060525600176475ustar00rootroot00000000000000motor-3.4.0/motor/frameworks/asyncio/__init__.py000066400000000000000000000111131460060525600217550ustar00rootroot00000000000000# Copyright 2014-2016 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. """asyncio compatibility layer for Motor, an asynchronous MongoDB driver. 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 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" def is_event_loop(loop): return isinstance(loop, asyncio.AbstractEventLoop) 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) def get_future(loop): return loop.create_future() 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)) # Adapted from tornado.gen. def chain_future(a, b): def copy(future): assert future is a if b.done(): return if a.exception() is not None: b.set_exception(a.exception()) else: b.set_result(a.result()) a.add_done_callback(copy) def chain_return_value(future, loop, return_value): """Compatible way to return a value in all Pythons. PEP 479, raise StopIteration(value) from a coroutine won't work forever, but "return value" doesn't work in Python 2. Instead, Motor methods that return values resolve a Future with it, and are implemented with callbacks rather than a coroutine internally. """ chained = loop.create_future() def copy(_future): # Return early if the task was cancelled. if chained.done(): return if _future.exception() is not None: chained.set_exception(_future.exception()) else: chained.set_result(return_value) future.add_done_callback(functools.partial(loop.call_soon_threadsafe, copy)) return chained def is_future(f): return asyncio.isfuture(f) def call_soon(loop, callback, *args, **kwargs): if kwargs: loop.call_soon(functools.partial(callback, *args, **kwargs)) else: loop.call_soon(callback, *args) def add_future(loop, future, callback, *args): future.add_done_callback(functools.partial(loop.call_soon_threadsafe, callback, *args)) def pymongo_class_wrapper(f, pymongo_class): """Executes the coroutine f and wraps its result in a Motor class. See WrapAsync. """ @functools.wraps(f) async def _wrapper(self, *args, **kwargs): result = await f(self, *args, **kwargs) # Don't call isinstance(), not checking subclasses. if result.__class__ == pymongo_class: # Delegate to the current object to wrap the result. return self.wrap(result) else: return result return _wrapper def yieldable(future): warnings.warn( "The yieldable function is deprecated and may be removed in a future major release", DeprecationWarning, stacklevel=2, ) return next(iter(future)) def platform_info(): return "asyncio" motor-3.4.0/motor/frameworks/tornado/000077500000000000000000000000001460060525600176505ustar00rootroot00000000000000motor-3.4.0/motor/frameworks/tornado/__init__.py000066400000000000000000000102151460060525600217600ustar00rootroot00000000000000# Copyright 2014-2016 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. """Tornado compatibility layer for Motor, an asynchronous MongoDB driver. 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. try: import contextvars except ImportError: contextvars = None # mypy: ignore-errors CLASS_PREFIX = "" def get_event_loop(): return ioloop.IOLoop.current() def is_event_loop(loop): return isinstance(loop, ioloop.IOLoop) def check_event_loop(loop): if not is_event_loop(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"]) 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)) def chain_return_value(future, loop, return_value): """Compatible way to return a value in all Pythons. PEP 479, raise StopIteration(value) from a coroutine won't work forever, but "return value" doesn't work in Python 2. Instead, Motor methods that return values resolve a Future with it, and are implemented with callbacks rather than a coroutine internally. """ chained = concurrent.Future() def copy(_future): # Return early if the task was cancelled. if chained.done(): return if _future.exception() is not None: chained.set_exception(_future.exception()) else: chained.set_result(return_value) future.add_done_callback(functools.partial(loop.add_callback, copy)) return chained def is_future(f): return isinstance(f, concurrent.Future) def call_soon(loop, callback, *args, **kwargs): if args or kwargs: loop.add_callback(functools.partial(callback, *args, **kwargs)) else: loop.add_callback(callback) def add_future(loop, future, callback, *args): loop.add_future(future, functools.partial(callback, *args)) def pymongo_class_wrapper(f, pymongo_class): """Executes the coroutine f and wraps its result in a Motor class. See WrapAsync. """ @functools.wraps(f) async def _wrapper(self, *args, **kwargs): result = await f(self, *args, **kwargs) # Don't call isinstance(), not checking subclasses. if result.__class__ == pymongo_class: # Delegate to the current object to wrap the result. return self.wrap(result) else: return result return _wrapper def yieldable(future): warnings.warn( "The yieldable function is deprecated and may be removed in a future major release.", DeprecationWarning, stacklevel=2, ) return future def platform_info(): return f"Tornado {tornado_version}" motor-3.4.0/motor/metaprogramming.py000066400000000000000000000233171460060525600175730ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Dynamic class-creation for Motor.""" import functools import inspect from typing import Any, Callable, Dict, TypeVar _class_cache: Dict[Any, Any] = {} # mypy: ignore-errors def asynchronize(framework, sync_method: Callable, 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. :Parameters: - `motor_class`: Motor class being created, e.g. MotorClient. - `framework`: An asynchronous framework - `sync_method`: Unbound method of pymongo Collection, Database, MongoClient, etc. - `doc`: Optionally override sync_method's docstring - `wrap_class`: Optional PyMongo class, wrap a returned object of this PyMongo class in the equivalent Motor class - `unwrap_class` Optional Motor class name, unwrap an argument with this Motor class name and pass the wrapped PyMongo object instead """ @functools.wraps(sync_method) def method(self, *args, **kwargs): if unwrap_class is not None: # Don't call isinstance(), not checking subclasses. unwrapped_args = [ obj.delegate if obj.__class__.__name__.endswith((unwrap_class, "MotorClientSession")) else obj for obj in args ] unwrapped_kwargs = { key: ( obj.delegate if obj.__class__.__name__.endswith((unwrap_class, "MotorClientSession")) else obj ) for key, obj in kwargs.items() } else: # For speed, don't call unwrap_args_session/unwrap_kwargs_session. unwrapped_args = [ obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj for obj in args ] unwrapped_kwargs = { key: ( obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj ) for key, obj in kwargs.items() } loop = self.get_io_loop() return framework.run_on_executor( loop, sync_method, self.delegate, *unwrapped_args, **unwrapped_kwargs ) if wrap_class is not None: method = framework.pymongo_class_wrapper(method, wrap_class) method.is_wrap_method = True # For Synchro. # This is for the benefit of motor_extensions.py, which needs this info to # generate documentation with Sphinx. method.is_async_method = True name = sync_method.__name__ method.pymongo_method_name = name if doc is not None: method.__doc__ = doc return method def unwrap_args_session(args): return ( obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj for obj in args ) def unwrap_kwargs_session(kwargs): return { key: (obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj) for key, obj in kwargs.items() } _coro_token = object() def coroutine_annotation(f): """In docs, annotate a function that returns a Future with 'coroutine'. This doesn't affect behavior. """ # Like: # @coroutine_annotation # def method(self): # f.coroutine_annotation = True return f class MotorAttributeFactory: """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 def create_attribute(self, cls, attr_name): raise NotImplementedError class Async(MotorAttributeFactory): def __init__(self, attr_name, doc=None): """A descriptor that wraps a PyMongo method, such as insert_one, and returns an asynchronous version of the method that returns a Future. :Parameters: - `attr_name`: The name of the attribute on the PyMongo class, if different from attribute on the Motor class """ super().__init__(doc) self.attr_name = attr_name self.wrap_class = None self.unwrap_class = None def create_attribute(self, cls, attr_name): name = self.attr_name or attr_name method = getattr(cls.__delegate_class__, name) return asynchronize( framework=cls._framework, sync_method=method, doc=self.doc, wrap_class=self.wrap_class, unwrap_class=self.unwrap_class, ) def wrap(self, original_class): self.wrap_class = original_class return self def unwrap(self, class_name): self.unwrap_class = class_name return self class AsyncRead(Async): def __init__(self, attr_name=None, doc=None): """A descriptor that wraps a PyMongo read method like find_one() that returns a Future. """ Async.__init__(self, attr_name=attr_name, doc=doc) class AsyncWrite(Async): def __init__(self, attr_name=None, doc=None): """A descriptor that wraps a PyMongo write method like update_one() that accepts getLastError options and returns a Future. """ Async.__init__(self, attr_name=attr_name, doc=doc) class AsyncCommand(Async): def __init__(self, attr_name=None, doc=None): """A descriptor that wraps a PyMongo command like copy_database() that returns a Future and does not accept getLastError options. """ Async.__init__(self, attr_name=attr_name, doc=doc) class ReadOnlyProperty(MotorAttributeFactory): """Creates a readonly attribute on the wrapped PyMongo object.""" def create_attribute(self, cls, attr_name): def fget(obj): return getattr(obj.delegate, attr_name) if self.doc: doc = self.doc else: doc = getattr(cls.__delegate_class__, attr_name).__doc__ if doc: return property(fget=fget, doc=doc) else: return property(fget=fget) class DelegateMethod(ReadOnlyProperty): """A method on the wrapped PyMongo object that does no I/O and can be called synchronously""" def __init__(self, doc=None): ReadOnlyProperty.__init__(self, doc) self.wrap_class = None def wrap(self, original_class): self.wrap_class = original_class return self def create_attribute(self, cls, attr_name): if self.wrap_class is None: return ReadOnlyProperty.create_attribute(self, cls, attr_name) method = getattr(cls.__delegate_class__, attr_name) original_class = self.wrap_class @functools.wraps(method) def wrapper(self_, *args, **kwargs): result = method(self_.delegate, *args, **kwargs) # Don't call isinstance(), not checking subclasses. if result.__class__ == original_class: # Delegate to the current object to wrap the result. return self_.wrap(result) else: return result if self.doc: wrapper.__doc__ = self.doc wrapper.is_wrap_method = True # For Synchro. return wrapper class MotorCursorChainingMethod(MotorAttributeFactory): def create_attribute(self, cls, attr_name): cursor_method = getattr(cls.__delegate_class__, attr_name) @functools.wraps(cursor_method) def return_clone(self, *args, **kwargs): cursor_method(self.delegate, *args, **kwargs) return self # This is for the benefit of Synchro, and motor_extensions.py return_clone.is_motorcursor_chaining_method = True return_clone.pymongo_method_name = attr_name if self.doc: return_clone.__doc__ = self.doc return return_clone T = TypeVar("T") def create_class_with_framework(cls: T, framework: Any, module_name: str) -> T: motor_class_name = framework.CLASS_PREFIX + cls.__motor_class_name__ cache_key = (cls, motor_class_name, framework) cached_class = _class_cache.get(cache_key) if cached_class: return cached_class new_class = type(str(motor_class_name), (cls,), {}) new_class.__module__ = module_name new_class._framework = framework assert hasattr(new_class, "__delegate_class__") # If we're constructing MotorClient from AgnosticClient, for example, # the method resolution order is (AgnosticClient, AgnosticBase, object). # Iterate over bases looking for attributes and coroutines that must be # replaced with framework-specific ones. for base in reversed(inspect.getmro(cls)): # Turn attribute factories into real methods or descriptors. for name, attr in base.__dict__.items(): if isinstance(attr, MotorAttributeFactory): new_class_attr = attr.create_attribute(new_class, name) setattr(new_class, name, new_class_attr) _class_cache[cache_key] = new_class return new_class motor-3.4.0/motor/motor_asyncio.py000066400000000000000000000043751460060525600172720ustar00rootroot00000000000000# Copyright 2011-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """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 __all__ = [ "AsyncIOMotorClient", "AsyncIOMotorClientSession", "AsyncIOMotorDatabase", "AsyncIOMotorCollection", "AsyncIOMotorCursor", "AsyncIOMotorCommandCursor", "AsyncIOMotorChangeStream", "AsyncIOMotorGridFSBucket", "AsyncIOMotorGridIn", "AsyncIOMotorGridOut", "AsyncIOMotorGridOutCursor", "AsyncIOMotorClientEncryption", ] def create_asyncio_class(cls: T) -> T: return create_class_with_framework(cls, asyncio_framework, "motor.motor_asyncio") AsyncIOMotorClient = create_asyncio_class(core.AgnosticClient) AsyncIOMotorClientSession = create_asyncio_class(core.AgnosticClientSession) AsyncIOMotorDatabase = create_asyncio_class(core.AgnosticDatabase) AsyncIOMotorCollection = create_asyncio_class(core.AgnosticCollection) AsyncIOMotorCursor = create_asyncio_class(core.AgnosticCursor) AsyncIOMotorCommandCursor = create_asyncio_class(core.AgnosticCommandCursor) AsyncIOMotorLatentCommandCursor = create_asyncio_class(core.AgnosticLatentCommandCursor) AsyncIOMotorChangeStream = create_asyncio_class(core.AgnosticChangeStream) AsyncIOMotorGridFSBucket = create_asyncio_class(motor_gridfs.AgnosticGridFSBucket) AsyncIOMotorGridIn = create_asyncio_class(motor_gridfs.AgnosticGridIn) AsyncIOMotorGridOut = create_asyncio_class(motor_gridfs.AgnosticGridOut) AsyncIOMotorGridOutCursor = create_asyncio_class(motor_gridfs.AgnosticGridOutCursor) AsyncIOMotorClientEncryption = create_asyncio_class(core.AgnosticClientEncryption) motor-3.4.0/motor/motor_asyncio.pyi000066400000000000000000000255721460060525600174450ustar00rootroot00000000000000from typing import Any, Mapping, MutableMapping, Optional, Union from bson import Code, CodecOptions, Timestamp from pymongo.client_session import TransactionOptions from pymongo.cursor 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): 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: ... 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: ... def __getattr__(self, name: str) -> AsyncIOMotorDatabase: ... def __getitem__(self, name: str) -> AsyncIOMotorDatabase: ... class AsyncIOMotorClientSession(core.AgnosticClientSession): @property def client(self) -> AsyncIOMotorClient: ... async def __aenter__(self) -> AsyncIOMotorClientSession: ... class AsyncIOMotorDatabase(core.AgnosticDatabase): 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: ... 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: ... 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: ... async def list_collections( self, session: Optional[core.AgnosticClientSession] = None, filter: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, ) -> AsyncIOMotorCommandCursor: ... 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: ... def aggregate( self, pipeline: _Pipeline, *args: Any, **kwargs: Any ) -> AsyncIOMotorLatentCommandCursor: ... 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: ... @property def client(self) -> AsyncIOMotorClient: ... def __getattr__(self, name: str) -> AsyncIOMotorCollection: ... def __getitem__(self, name: str) -> AsyncIOMotorCollection: ... class AsyncIOMotorCollection(core.AgnosticCollection): 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[Mapping[str, Any]]: ... def list_search_indexes( self, name: Optional[str] = None, session: Optional[core.AgnosticClientSession] = None, comment: Optional[Any] = None, **kwargs: Any, ) -> AsyncIOMotorLatentCommandCursor: ... def __getattr__(self, name: str) -> AsyncIOMotorCollection: ... def __getitem__(self, name: str) -> AsyncIOMotorCollection: ... def find(self, *args: Any, **kwargs: Any) -> AsyncIOMotorCursor: ... def find_raw_batches(self, *args: Any, **kwargs: Any) -> AsyncIOMotorCursor: ... def aggregate( self, pipeline: _Pipeline, *args: Any, **kwargs: Any ) -> AsyncIOMotorCommandCursor: ... def aggregate_raw_batches( self, pipeline: _Pipeline, **kwargs: Any ) -> AsyncIOMotorCommandCursor: ... def list_indexes( self, session: Optional[core.AgnosticClientSession] = None, **kwargs: Any ) -> AsyncIOMotorLatentCommandCursor: ... class AsyncIOMotorLatentCommandCursor(core.AgnosticLatentCommandCursor): ... class AsyncIOMotorCursor(core.AgnosticCursor): def collation(self, collation: Optional[_CollationIn]) -> AsyncIOMotorCursor: ... def add_option(self, mask: int) -> AsyncIOMotorCursor: ... def remove_option(self, mask: int) -> AsyncIOMotorCursor: ... def limit(self, limit: int) -> AsyncIOMotorCursor: ... def skip(self, skip: int) -> AsyncIOMotorCursor: ... def max_scan(self, max_scan: Optional[int]) -> AsyncIOMotorCursor: ... def sort( self, key_or_list: _Hint, direction: Optional[Union[int, str]] = None ) -> AsyncIOMotorCursor: ... def hint(self, index: Optional[_Hint]) -> AsyncIOMotorCursor: ... def where(self, code: Union[str, Code]) -> AsyncIOMotorCursor: ... def max_await_time_ms(self, max_await_time_ms: Optional[int]) -> AsyncIOMotorCursor: ... def max_time_ms(self, max_time_ms: Optional[int]) -> AsyncIOMotorCursor: ... def min(self, spec: _Sort) -> AsyncIOMotorCursor: ... def max(self, spec: _Sort) -> AsyncIOMotorCursor: ... def comment(self, comment: Any) -> AsyncIOMotorCursor: ... def allow_disk_use(self, allow_disk_use: bool) -> AsyncIOMotorCursor: ... def rewind(self) -> AsyncIOMotorCursor: ... def clone(self) -> AsyncIOMotorCursor: ... def __copy__(self) -> AsyncIOMotorCursor: ... def __deepcopy__(self, memo: Any) -> AsyncIOMotorCursor: ... class AsyncIOMotorRawBatchCursor(core.AgnosticRawBatchCursor): ... class AsyncIOMotorCommandCursor(core.AgnosticCommandCursor): ... class AsyncIOMotorRawBatchCommandCursor(core.AgnosticRawBatchCommandCursor): ... class AsyncIOMotorChangeStream(core.AgnosticChangeStream): def __aiter__(self) -> AsyncIOMotorChangeStream: ... async def __aenter__(self) -> AsyncIOMotorChangeStream: ... class AsyncIOMotorClientEncryption(core.AgnosticClientEncryption): async def __aenter__(self) -> AsyncIOMotorClientEncryption: ... async def get_keys(self) -> AsyncIOMotorCursor: ... async def create_encrypted_collection( self, database: core.AgnosticDatabase, name: str, encrypted_fields: Mapping[str, Any], kms_provider: Optional[str] = None, master_key: Optional[Mapping[str, Any]] = None, **kwargs: Any, ) -> tuple[AsyncIOMotorCollection, 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: ... motor-3.4.0/motor/motor_common.py000066400000000000000000000012651460060525600171100ustar00rootroot00000000000000# Copyright 2011-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Common code to support all async frameworks.""" callback_type_error = TypeError("callback must be a callable") motor-3.4.0/motor/motor_gridfs.py000066400000000000000000000445731460060525600171070ustar00rootroot00000000000000# Copyright 2011-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """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 motor import docstrings from motor.core import AgnosticCollection, AgnosticCursor, AgnosticDatabase from motor.metaprogramming import ( AsyncCommand, AsyncRead, DelegateMethod, ReadOnlyProperty, coroutine_annotation, create_class_with_framework, ) class AgnosticGridOutCursor(AgnosticCursor): __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__ ) return grid_out_class(self.collection, delegate=grid_out) else: # Exhausted. return None 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 ) return getattr(obj.delegate, attr_name) doc = getattr(cls.__delegate_class__, attr_name).__doc__ return property(fget=fget, doc=doc) class AgnosticGridOut: """Class to read data out of GridFS. MotorGridOut supports the same attributes as PyMongo's :class:`~gridfs.grid_file.GridOut`, such as ``_id``, ``content_type``, etc. You don't need to instantiate this class directly - use the methods provided by :class:`~motor.MotorGridFSBucket`. If it **is** instantiated directly, call :meth:`open`, :meth:`read`, or :meth:`readline` before accessing its attributes. """ __motor_class_name__ = "MotorGridOut" __delegate_class__ = gridfs.GridOut _ensure_file = AsyncCommand() _id = MotorGridOutProperty() aliases = MotorGridOutProperty() chunk_size = MotorGridOutProperty() close = MotorGridOutProperty() content_type = MotorGridOutProperty() filename = MotorGridOutProperty() length = 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 ): collection_class = create_class_with_framework( 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 ) if delegate: self.delegate = delegate else: self.delegate = self.__delegate_class__( root_collection.delegate, file_id, file_document, session=session ) self.io_loop = root_collection.get_io_loop() def __aiter__(self): return self async def __anext__(self): chunk = await self.readchunk() if chunk: return chunk raise StopAsyncIteration() def __getattr__(self, item): if not self.delegate._file: raise pymongo.errors.InvalidOperation( "You must call MotorGridOut.open() before accessing the %s property" % item ) return getattr(self.delegate, item) @coroutine_annotation def open(self): """Retrieve this file's attributes from the server. Returns a Future. .. versionchanged:: 2.0 No longer accepts a callback argument. .. versionchanged:: 0.2 :class:`~motor.MotorGridOut` now opens itself on demand, calling ``open`` explicitly is rarely needed. """ return self._framework.chain_return_value(self._ensure_file(), self.get_io_loop(), self) def get_io_loop(self): return self.io_loop async def stream_to_handler(self, request_handler): """Write the contents of this file to a :class:`tornado.web.RequestHandler`. This method calls :meth:`~tornado.web.RequestHandler.flush` on the RequestHandler, so ensure all headers have already been set. For a more complete example see the implementation of :class:`~motor.web.GridFSHandler`. .. code-block:: python class FileHandler(tornado.web.RequestHandler): @tornado.web.asynchronous @gen.coroutine def get(self, filename): db = self.settings["db"] fs = await motor.MotorGridFSBucket(db()) try: gridout = await fs.open_download_stream_by_name(filename) except gridfs.NoFile: raise tornado.web.HTTPError(404) self.set_header("Content-Type", gridout.content_type) self.set_header("Content-Length", gridout.length) await gridout.stream_to_handler(self) self.finish() .. seealso:: Tornado `RequestHandler `_ """ written = 0 while written < self.length: # Reading chunk_size at a time minimizes buffering. chunk = await self.read(self.chunk_size) # write() simply appends the output to a list; flush() sends it # over the network and minimizes buffering in the handler. request_handler.write(chunk) request_handler.flush() written += len(chunk) class AgnosticGridIn: __motor_class_name__ = "MotorGridIn" __delegate_class__ = gridfs.GridIn __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=""" 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 document on the server. Metadata set on the file appears as attributes on a :class:`~motor.MotorGridOut` object created from the file. :Parameters: - `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): """ 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 `_ 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:`~bson.objectid.ObjectId`) - this ``"_id"`` must not have already been used for another file - ``"filename"``: human name for the file - ``"contentType"`` or ``"content_type"``: valid mime-type for the file - ``"chunkSize"`` or ``"chunk_size"``: size of each of the chunks, in bytes (default: 256 kb) - ``"encoding"``: encoding used for this file. In Python 2, any :class:`unicode` that is written to the file will be converted to a :class:`str`. In Python 3, any :class:`str` that is written to the file will be converted to :class:`bytes`. :Parameters: - `root_collection`: root collection to write to - `session` (optional): a :class:`~pymongo.client_session.ClientSession` to use for all commands - `**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__ ) if not isinstance(root_collection, collection_class): raise TypeError( "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 ) # 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) def get_io_loop(self): return self.io_loop class AgnosticGridFSBucket: __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) def __init__( self, database, bucket_name="fs", 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` is not acknowledged. This class conforms to the `GridFS API Spec `_ for MongoDB drivers. :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. - `collection` (optional): Deprecated, an alias for `bucket_name` that exists solely to provide backwards compatibility. .. versionchanged:: 3.0 Removed support for the `disable_md5` parameter (to match the GridFSBucket class in PyMongo). .. versionchanged:: 2.1 Added support for the `bucket_name`, `chunk_size_bytes`, `write_concern`, and `read_preference` parameters. Deprecated the `collection` parameter which is now an alias to `bucket_name` (to match the GridFSBucket class in PyMongo). .. versionadded:: 1.0 .. mongodoc:: gridfs """ # Preserve backwards compatibility of "collection" parameter if collection is not None: warnings.warn( 'the "collection" parameter is deprecated, use "bucket_name" instead', DeprecationWarning, stacklevel=2, ) bucket_name = collection db_class = create_class_with_framework(AgnosticDatabase, self._framework, self.__module__) if not isinstance(database, db_class): raise TypeError( f"First argument to {self.__class__} must be MotorDatabase, not {database!r}" ) self.io_loop = database.get_io_loop() self.collection = database.get_collection( bucket_name, write_concern=write_concern, read_preference=read_preference ) self.delegate = self.__delegate_class__( database.delegate, bucket_name, chunk_size_bytes=chunk_size_bytes, write_concern=write_concern, read_preference=read_preference, ) def get_io_loop(self): return self.io_loop def wrap(self, obj): if obj.__class__ is grid_file.GridIn: grid_in_class = create_class_with_framework( AgnosticGridIn, self._framework, self.__module__ ) return grid_in_class(root_collection=self.collection, delegate=obj) elif obj.__class__ is grid_file.GridOut: grid_out_class = create_class_with_framework( AgnosticGridOut, self._framework, self.__module__ ) return grid_out_class(root_collection=self.collection, delegate=obj) elif obj.__class__ is gridfs.GridOutCursor: grid_out_class = create_class_with_framework( AgnosticGridOutCursor, self._framework, self.__module__ ) return grid_out_class(cursor=obj, collection=self.collection) def 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:: cursor = bucket.find({"filename": "lisa.txt"}, no_cursor_timeout=True) while (await cursor.fetch_next): grid_out = cursor.next_object() data = await grid_out.read() This iterates through all versions of "lisa.txt" stored in GridFS. Note that setting no_cursor_timeout to True may be important to prevent the cursor from timing out during long multi-file processing work. As another example, the call:: most_recent_three = fs.find().sort("uploadDate", -1).limit(3) would return a cursor to the three most recently uploaded files in GridFS. Follows a similar interface to :meth:`~motor.MotorCollection.find` in :class:`~motor.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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. If a :class:`~pymongo.client_session.ClientSession` is passed to :meth:`find`, all returned :class:`MotorGridOut` instances are associated with that session. .. versionchanged:: 1.2 Added session parameter. """ cursor = self.delegate.find(*args, **kwargs) grid_out_cursor = create_class_with_framework( AgnosticGridOutCursor, self._framework, self.__module__ ) 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() motor-3.4.0/motor/motor_gridfs.pyi000066400000000000000000000145641460060525600172550ustar00rootroot00000000000000# 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 typing import Any, Iterable, Mapping, 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] async def _Cursor__die(self, synchronous: bool = False) -> None: ... 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 _ensure_file(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[AgnosticCollection] = 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: ... motor-3.4.0/motor/motor_tornado.py000066400000000000000000000040631460060525600172650ustar00rootroot00000000000000# Copyright 2011-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Tornado support for Motor, an asynchronous driver for MongoDB.""" from . import core, motor_gridfs from .frameworks import tornado as tornado_framework from .metaprogramming import T, create_class_with_framework __all__ = [ "MotorClient", "MotorClientSession", "MotorDatabase", "MotorCollection", "MotorCursor", "MotorCommandCursor", "MotorChangeStream", "MotorGridFSBucket", "MotorGridIn", "MotorGridOut", "MotorGridOutCursor", "MotorClientEncryption", ] def create_motor_class(cls: T) -> T: return create_class_with_framework(cls, tornado_framework, "motor.motor_tornado") MotorClient = create_motor_class(core.AgnosticClient) MotorClientSession = create_motor_class(core.AgnosticClientSession) MotorDatabase = create_motor_class(core.AgnosticDatabase) MotorCollection = create_motor_class(core.AgnosticCollection) MotorCursor = create_motor_class(core.AgnosticCursor) MotorCommandCursor = create_motor_class(core.AgnosticCommandCursor) MotorLatentCommandCursor = create_motor_class(core.AgnosticLatentCommandCursor) MotorChangeStream = create_motor_class(core.AgnosticChangeStream) MotorGridFSBucket = create_motor_class(motor_gridfs.AgnosticGridFSBucket) MotorGridIn = create_motor_class(motor_gridfs.AgnosticGridIn) MotorGridOut = create_motor_class(motor_gridfs.AgnosticGridOut) MotorGridOutCursor = create_motor_class(motor_gridfs.AgnosticGridOutCursor) MotorClientEncryption = create_motor_class(core.AgnosticClientEncryption) motor-3.4.0/motor/motor_tornado.pyi000066400000000000000000000243261460060525600174420ustar00rootroot00000000000000from typing import Any, Mapping, MutableMapping, Optional, Union from bson import Code, CodecOptions, Timestamp from pymongo.client_session import TransactionOptions from pymongo.cursor 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): 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: ... 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: ... def __getattr__(self, name: str) -> MotorDatabase: ... def __getitem__(self, name: str) -> MotorDatabase: ... class MotorClientSession(core.AgnosticClientSession): @property def client(self) -> MotorClient: ... async def __aenter__(self) -> MotorClientSession: ... class MotorDatabase(core.AgnosticDatabase): 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: ... 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: ... 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: ... async def list_collections( self, session: Optional[core.AgnosticClientSession] = None, filter: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, **kwargs: Any, ) -> MotorCommandCursor: ... 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: ... def aggregate( self, pipeline: _Pipeline, *args: Any, **kwargs: Any ) -> MotorLatentCommandCursor: ... 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: ... @property def client(self) -> MotorClient: ... def __getattr__(self, name: str) -> MotorCollection: ... def __getitem__(self, name: str) -> MotorCollection: ... class MotorCollection(core.AgnosticCollection): 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[Mapping[str, Any]]: ... def list_search_indexes( self, name: Optional[str] = None, session: Optional[core.AgnosticClientSession] = None, comment: Optional[Any] = None, **kwargs: Any, ) -> MotorLatentCommandCursor: ... def __getattr__(self, name: str) -> MotorCollection: ... def __getitem__(self, name: str) -> MotorCollection: ... def find(self, *args: Any, **kwargs: Any) -> MotorCursor: ... def find_raw_batches(self, *args: Any, **kwargs: Any) -> MotorCursor: ... def aggregate(self, pipeline: _Pipeline, *args: Any, **kwargs: Any) -> MotorCommandCursor: ... def aggregate_raw_batches(self, pipeline: _Pipeline, **kwargs: Any) -> MotorCommandCursor: ... def list_indexes( self, session: Optional[core.AgnosticClientSession] = None, **kwargs: Any ) -> MotorLatentCommandCursor: ... class MotorLatentCommandCursor(core.AgnosticLatentCommandCursor): ... class MotorCursor(core.AgnosticCursor): def collation(self, collation: Optional[_CollationIn]) -> MotorCursor: ... def add_option(self, mask: int) -> MotorCursor: ... def remove_option(self, mask: int) -> MotorCursor: ... def limit(self, limit: int) -> MotorCursor: ... def skip(self, skip: int) -> MotorCursor: ... def max_scan(self, max_scan: Optional[int]) -> MotorCursor: ... def sort( self, key_or_list: _Hint, direction: Optional[Union[int, str]] = None ) -> MotorCursor: ... def hint(self, index: Optional[_Hint]) -> MotorCursor: ... def where(self, code: Union[str, Code]) -> MotorCursor: ... def max_await_time_ms(self, max_await_time_ms: Optional[int]) -> MotorCursor: ... def max_time_ms(self, max_time_ms: Optional[int]) -> MotorCursor: ... def min(self, spec: _Sort) -> MotorCursor: ... def max(self, spec: _Sort) -> MotorCursor: ... def comment(self, comment: Any) -> MotorCursor: ... def allow_disk_use(self, allow_disk_use: bool) -> MotorCursor: ... def rewind(self) -> MotorCursor: ... def clone(self) -> MotorCursor: ... def __copy__(self) -> MotorCursor: ... def __deepcopy__(self, memo: Any) -> MotorCursor: ... class MotorRawBatchCursor(core.AgnosticRawBatchCursor): ... class MotorCommandCursor(core.AgnosticCommandCursor): ... class MotorRawBatchCommandCursor(core.AgnosticRawBatchCommandCursor): ... class MotorChangeStream(core.AgnosticChangeStream): def __aiter__(self) -> MotorChangeStream: ... async def __aenter__(self) -> MotorChangeStream: ... class MotorClientEncryption(core.AgnosticClientEncryption): async def __aenter__(self) -> MotorClientEncryption: ... async def get_keys(self) -> MotorCursor: ... async def create_encrypted_collection( self, database: core.AgnosticDatabase, name: str, encrypted_fields: Mapping[str, Any], kms_provider: Optional[str] = None, master_key: Optional[Mapping[str, Any]] = None, **kwargs: Any, ) -> tuple[MotorCollection, 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: ... motor-3.4.0/motor/py.typed000066400000000000000000000002521460060525600155200ustar00rootroot00000000000000# 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". motor-3.4.0/motor/web.py000066400000000000000000000156511460060525600151610ustar00rootroot00000000000000# Copyright 2011-2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Utilities for using Motor with Tornado web applications.""" import datetime import email.utils import mimetypes import time import gridfs import tornado.web 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 # {{ static_url('image.png') }} and get "/static/image.png?v=1234abcdef", # which is cached forever. Problem is, it calculates the checksum 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. class GridFSHandler(tornado.web.RequestHandler): """A handler that can serve content from GridFS, very similar to :class:`tornado.web.StaticFileHandler`. .. code-block:: python db = motor.MotorClient().my_database 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"): self.database = database self.root_collection = root_collection def get_gridfs_file(self, bucket, filename, request): """Overridable method to choose a GridFS file to serve at a URL. By default, if a URL pattern like ``"/static/(.*)"`` is mapped to this ``GridFSHandler``, then the trailing portion of the URL is used as the filename, so a request for "/static/image.png" results in a call to :meth:`MotorGridFSBucket.open_download_stream_by_name` with "image.png" as the ``filename`` argument. To customize the mapping of path to GridFS file, override ``get_gridfs_file`` and return a Future :class:`~motor.MotorGridOut` from it. For example, to retrieve the file by ``_id`` instead of filename:: class CustomGridFSHandler(motor.web.GridFSHandler): def get_gridfs_file(self, bucket, filename, request): # Path is interpreted as _id instead of name. # Return a Future MotorGridOut. return fs.open_download_stream(file_id=ObjectId(path)) :Parameters: - `bucket`: A :class:`~motor.motor_tornado.MotorGridFSBucket` - `filename`: A string, the matched group of the URL pattern - `request`: An :class:`tornado.httputil.HTTPServerRequest` .. versionchanged:: 1.0 **BREAKING CHANGE**: Now takes a :class:`~motor.motor_tornado.MotorGridFSBucket`, not a ``MotorGridFS``. Also takes an additional ``request`` parameter. .. versionchanged:: 0.2 ``get_gridfs_file`` no longer accepts a callback, instead returns a Future. """ return bucket.open_download_stream_by_name(filename) async def get(self, path, include_body=True): fs = motor.MotorGridFSBucket(self.database, self.root_collection) try: gridout = await self.get_gridfs_file(fs, path, self.request) except gridfs.NoFile: raise tornado.web.HTTPError(404) from None # 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) mime_type = gridout.content_type # If content type is not defined, try to check it with mimetypes if mime_type is None: mime_type, encoding = mimetypes.guess_type(path) # Starting from here, largely a copy of StaticFileHandler if mime_type: self.set_header("Content-Type", mime_type) 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("Cache-Control", "max-age=" + str(cache_time)) else: self.set_header("Cache-Control", "public") self.set_extra_headers(path, gridout) # Check the If-Modified-Since, and don't send the result if the # content has not been modified ims_value = self.request.headers.get("If-Modified-Since") if ims_value is not None: date_tuple = email.utils.parsedate(ims_value) # 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 if_since >= modified: self.set_status(304) return # Same for Etag etag = self.request.headers.get("If-None-Match") if etag is not None and etag.strip('"') == checksum: self.set_status(304) return self.set_header("Content-Length", gridout.length) if include_body: await gridout.stream_to_handler(self) # Needed until fix for Tornado bug 751 is released, see # https://github.com/facebook/tornado/issues/751 and # https://github.com/facebook/tornado/commit/5491685 self.finish() def head(self, path): # get() is a coroutine. Return its Future. return self.get(path, include_body=False) def get_cache_time(self, path, modified, mime_type): """Override to customize cache control behavior. Return a positive number of seconds to trigger aggressive caching or 0 to mark resource as cacheable, only. 0 is the default. """ return 0 def set_extra_headers(self, path, gridout): """For subclass to add extra headers to the response""" motor-3.4.0/pyproject.toml000066400000000000000000000130431460060525600155770ustar00rootroot00000000000000[build-system] requires = ["setuptools>61.0"] build-backend = "setuptools.build_meta" [project] name = "motor" dynamic = ["version"] description = "Non-blocking MongoDB driver for Tornado or asyncio" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.7" authors = [ { name = "A. Jesse Jiryu Davis", email = "jesse@mongodb.com" }, ] keywords = [ "asyncio", "bson", "gridfs", "mongo", "mongodb", "motor", "pymongo", "tornado", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: Unix", "Typing :: Typed", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] dependencies = [ "pymongo>=4.5,<5", ] [project.optional-dependencies] aws = [ "pymongo[aws]>=4.5,<5", ] encryption = [ "pymongo[encryption]>=4.5,<5", ] gssapi = [ "pymongo[gssapi]>=4.5,<5", ] ocsp = [ "pymongo[ocsp]>=4.5,<5", ] snappy = [ "pymongo[snappy]>=4.5,<5", ] srv = [ "pymongo[srv]>=4.5,<5", ] test = [ "pytest>=7", "mockupdb", "tornado>=5", "aiohttp!=3.8.6", "motor[encryption]" ] zstd = [ "pymongo[zstd]>=4.5,<5", ] [project.urls] Homepage = "https://www.mongodb.org" Documentation = "https://motor.readthedocs.io" Source = "https://github.com/mongodb/motor" Tracker = "https://jira.mongodb.org/projects/MOTOR/issues" [tool.setuptools.dynamic] version = {attr = "motor._version.version"} [tool.setuptools.packages.find] include = ["motor"] [tool.mypy] python_version = "3.7" strict = true pretty = true show_error_context = true show_error_codes = true warn_redundant_casts = true warn_unreachable = true disable_error_code = ["type-arg"] enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] exclude = ["aiohttp"] # Remove when drop support for 3.7 [tool.pytest.ini_options] minversion = "7" testpaths = ["test"] xfail_strict = true log_cli_level = "INFO" addopts = ["-ra", "--strict-config", "--strict-markers", "--maxfail=10", "--durations=5", "--junitxml=xunit-results/TEST-results.xml"] faulthandler_timeout = 1500 filterwarnings = [ "error", "ignore:Bare functions are deprecated, use async ones:DeprecationWarning", "ignore:Application.make_handler:DeprecationWarning", "ignore:unclosed .tar.gz # motor--py3-none-any.whl set -o xtrace # Write all commands first to stderr set -o errexit # Exit the script with error if any of the commands fail # Cleanup destinations rm -rf build rm -rf dist # Install deps python3 -m pip install build # Build the source dist and wheel python3 -m build . # Cleanup rm -rf build ls dist motor-3.4.0/setup.py000066400000000000000000000000461460060525600143740ustar00rootroot00000000000000from setuptools import setup setup() motor-3.4.0/synchro/000077500000000000000000000000001460060525600143475ustar00rootroot00000000000000motor-3.4.0/synchro/__init__.py000066400000000000000000000646631460060525600164770ustar00rootroot00000000000000# Copyright 2012-2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Synchro, a fake synchronous PyMongo implementation built on top of Motor, for the sole purpose of checking that Motor passes the same unittests as PyMongo. DO NOT USE THIS MODULE. """ import functools import inspect import unittest import warnings from typing import Generic, TypeVar # Make e.g. "from pymongo.errors import AutoReconnect" work. Note that # importing * won't pick up underscore-prefixed attrs. from gridfs import * from gridfs import _disallow_transactions from gridfs.errors import * from gridfs.grid_file import ( _SEEK_CUR, _SEEK_END, _UPLOAD_BUFFER_CHUNKS, _UPLOAD_BUFFER_SIZE, DEFAULT_CHUNK_SIZE, _clear_entity_type_registry, ) # Work around circular imports. from pymongo import * from pymongo import ( GEOSPHERE, HASHED, auth, change_stream, collation, compression_support, encryption_options, errors, event_loggers, message, operations, read_preferences, saslprep, server_selectors, server_type, srv_resolver, ssl_support, write_concern, ) # Added for API compat with pymongo. try: from pymongo import _csot except ImportError: pass from pymongo import client_session from pymongo.auth import * from pymongo.auth import _build_credentials_tuple, _password_digest from pymongo.client_session import TransactionOptions, _TxnState from pymongo.collation import * from pymongo.common import * from pymongo.common import _MAX_END_SESSIONS, _UUID_REPRESENTATIONS from pymongo.compression_support import _HAVE_SNAPPY, _HAVE_ZLIB, _HAVE_ZSTD from pymongo.cursor import * from pymongo.cursor import _QUERY_OPTIONS from pymongo.encryption import * from pymongo.encryption import _MONGOCRYPTD_TIMEOUT_MS, _Encrypter from pymongo.encryption_options import * from pymongo.encryption_options import _HAVE_PYMONGOCRYPT from pymongo.errors import * from pymongo.event_loggers import * from pymongo.helpers import _check_command_response from pymongo.lock import _create_lock from pymongo.message import ( _COMMAND_OVERHEAD, _CursorAddress, _gen_find_command, _maybe_add_read_preference, ) from pymongo.monitor import * from pymongo.monitoring import * from pymongo.monitoring import _LISTENERS, _SENSITIVE_COMMANDS, _Listeners from pymongo.ocsp_cache import _OCSPCache from pymongo.operations import * from pymongo.periodic_executor import * from pymongo.periodic_executor import _EXECUTORS from pymongo.pool import * from pymongo.pool import _METADATA, Connection, Pool, _PoolClosedError from pymongo.read_concern import * from pymongo.read_preferences import * from pymongo.read_preferences import _ServerMode from pymongo.results import * from pymongo.results import _WriteResult from pymongo.saslprep import * from pymongo.server import * from pymongo.server_selectors import * from pymongo.settings import * from pymongo.srv_resolver import _resolve, _SrvResolver from pymongo.ssl_support import * from pymongo.topology import * from pymongo.topology_description import * from pymongo.uri_parser import * from pymongo.uri_parser import _HAVE_DNSPYTHON from pymongo.write_concern import * from tornado.ioloop import IOLoop import motor import motor.frameworks.tornado import motor.motor_tornado # Internal classes not declared in motor_tornado.py: retrieve from class cache. from motor.core import AgnosticRawBatchCommandCursor as _AgnosticRawBatchCommandCursor from motor.core import AgnosticRawBatchCursor as _AgnosticRawBatchCursor from motor.core import _MotorTransactionContext from motor.metaprogramming import MotorAttributeFactory from motor.motor_tornado import create_motor_class _MotorRawBatchCursor = create_motor_class(_AgnosticRawBatchCursor) _MotorRawBatchCommandCursor = create_motor_class(_AgnosticRawBatchCommandCursor) def wrap_synchro(fn): """If decorated Synchro function returns a Motor object, wrap in a Synchro object. """ @functools.wraps(fn) def _wrap_synchro(*args, **kwargs): motor_obj = fn(*args, **kwargs) # Not all Motor classes appear here, only those we need to return # from methods like create_collection() if isinstance(motor_obj, motor.MotorCollection): client = MongoClient(delegate=motor_obj.database.client) database = Database(client, motor_obj.database.name) return Collection(database, motor_obj.name, delegate=motor_obj) if isinstance(motor_obj, motor.motor_tornado.MotorClientSession): return ClientSession(delegate=motor_obj) if isinstance(motor_obj, _MotorTransactionContext): return _SynchroTransactionContext(motor_obj) if isinstance(motor_obj, motor.MotorDatabase): client = MongoClient(delegate=motor_obj.client) return Database(client, motor_obj.name, delegate=motor_obj) if isinstance(motor_obj, motor.motor_tornado.MotorChangeStream): # Send the initial aggregate as PyMongo expects. motor_obj._lazy_init() return ChangeStream(motor_obj) if isinstance(motor_obj, motor.motor_tornado.MotorLatentCommandCursor): synchro_cursor = CommandCursor(motor_obj) # Send the initial command as PyMongo expects. if not motor_obj.started: synchro_cursor.synchronize(motor_obj._get_more)() return synchro_cursor if isinstance(motor_obj, motor.motor_tornado.MotorCommandCursor): return CommandCursor(motor_obj) if isinstance(motor_obj, _MotorRawBatchCommandCursor): return CommandCursor(motor_obj) if isinstance(motor_obj, motor.motor_tornado.MotorCursor): return Cursor(motor_obj) if isinstance(motor_obj, _MotorRawBatchCursor): return Cursor(motor_obj) if isinstance(motor_obj, motor.MotorGridIn): return GridIn(None, delegate=motor_obj) if isinstance(motor_obj, motor.MotorGridOut): return GridOut(None, delegate=motor_obj) if isinstance(motor_obj, motor.motor_tornado.MotorGridOutCursor): return GridOutCursor(motor_obj) else: return motor_obj return _wrap_synchro def unwrap_synchro(fn): """Unwrap Synchro objects passed to a method and pass Motor objects instead.""" @functools.wraps(fn) def _unwrap_synchro(*args, **kwargs): def _unwrap_obj(obj): if isinstance(obj, Synchro): return obj.delegate else: return obj args = [_unwrap_obj(arg) for arg in args] kwargs = dict([(key, _unwrap_obj(value)) for key, value in kwargs.items()]) # noqa: C404 return fn(*args, **kwargs) return _unwrap_synchro class SynchroAttr: # Name can be set by SynchroMeta if Sync() is used directly in class defn. def __init__(self, name=None): self.name = name class Sync(SynchroAttr): def __get__(self, obj, objtype): async_method = getattr(obj.delegate, self.name) return wrap_synchro(unwrap_synchro(obj.synchronize(async_method))) class WrapOutgoing(SynchroAttr): def __get__(self, obj, objtype): # self.name is set by SynchroMeta. name = self.name def synchro_method(*args, **kwargs): motor_method = getattr(obj.delegate, name) return wrap_synchro(unwrap_synchro(motor_method))(*args, **kwargs) return synchro_method class SynchroProperty(SynchroAttr): """Used to fake private properties like MongoClient.__member - don't use for real properties like write_concern or you'll mask missing features in Motor! """ def __get__(self, obj, objtype): # self.name is set by SynchroMeta. return getattr(obj.delegate.delegate, self.name) def __set__(self, obj, val): # self.name is set by SynchroMeta. return setattr(obj.delegate.delegate, self.name, val) def wrap_outgoing(delegate_attr): for decoration in ("is_motorcursor_chaining_method", "is_wrap_method"): if getattr(delegate_attr, decoration, False): return True return False class SynchroMeta(type): """This metaclass customizes creation of Synchro's MongoClient, Database, etc., classes: - All asynchronized methods of Motor classes, such as MotorDatabase.command(), are re-synchronized. - Properties delegated from Motor's classes to PyMongo's, such as ``name`` or ``host``, are delegated **again** from Synchro's class to Motor's. - Motor methods which return Motor class instances are wrapped to return Synchro class instances. - Certain internals accessed by PyMongo's unittests, such as _Cursor__data, are delegated from Synchro directly to PyMongo. """ def __new__(cls, name, bases, attrs): # Create the class, e.g. the Synchro MongoClient or Database class. new_class = type.__new__(cls, name, bases, attrs) # delegate_class is a Motor class like MotorClient. delegate_class = new_class.__delegate_class__ if delegate_class: delegated_attrs = {} for klass in reversed(inspect.getmro(delegate_class)): delegated_attrs.update(klass.__dict__) for attrname, delegate_attr in delegated_attrs.items(): # If attrname is in attrs, it means Synchro has overridden # this attribute, e.g. Collection.aggregate which is # special-cased. Ignore such attrs. if attrname in attrs: continue if getattr(delegate_attr, "is_async_method", False): # Re-synchronize the method. setattr(new_class, attrname, Sync(attrname)) elif wrap_outgoing(delegate_attr): # Wrap Motor objects in Synchro objects. wrapper = WrapOutgoing() wrapper.name = attrname setattr(new_class, attrname, wrapper) elif isinstance(delegate_attr, property): # Delegate the property from Synchro to Motor. setattr(new_class, attrname, delegate_attr) # Set DelegateProperties' and SynchroProperties' names. for name, attr in attrs.items(): if isinstance(attr, (MotorAttributeFactory, SynchroAttr)): # noqa: SIM102 if attr.name is None: attr.name = name return new_class class Synchro(metaclass=SynchroMeta): """ Wraps a MotorClient, MotorDatabase, MotorCollection, etc. and makes it act like the synchronous pymongo equivalent """ __delegate_class__ = None def __eq__(self, other): if ( isinstance(other, self.__class__) and hasattr(self, "delegate") and hasattr(other, "delegate") ): return self.delegate == other.delegate return NotImplemented def __hash__(self): return self.delegate.__hash__() def synchronize(self, async_method): """ @param async_method: Bound method of a MotorClient, MotorDatabase, etc. @return: A synchronous wrapper around the method """ @functools.wraps(async_method) def synchronized_method(*args, **kwargs): @functools.wraps(async_method) def partial(): return async_method(*args, **kwargs) with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) loop = IOLoop.current() return loop.run_sync(partial) return synchronized_method _T = TypeVar("_T") class MongoClient(Synchro, Generic[_T]): __delegate_class__ = motor.MotorClient HOST = "localhost" PORT = 27017 get_database = WrapOutgoing() max_pool_size = SynchroProperty() start_session = Sync() watch = WrapOutgoing() __iter__ = None # PYTHON-3084 __next__ = Sync() def __init__(self, host=None, port=None, *args, **kwargs): # So that TestClient.test_constants and test_types work. host = host if host is not None else MongoClient.HOST port = port if port is not None else MongoClient.PORT self.delegate = kwargs.pop("delegate", None) # Motor passes connect=False by default. kwargs.setdefault("connect", True) if not self.delegate: self.delegate = self.__delegate_class__(host, port, *args, **kwargs) # PyMongo expects this to return a real MongoClient, unwrap it. def _duplicate(self, **kwargs): client = self.delegate._duplicate(**kwargs) if isinstance(client, Synchro): return client.delegate.delegate return client def __enter__(self): return self def __exit__(self, *args): self.delegate.close() def __getattr__(self, name): return Database(self, name, delegate=getattr(self.delegate, name)) def __getitem__(self, name): return Database(self, name, delegate=self.delegate[name]) # For PyMongo tests that access client internals. _MongoClient__all_credentials = SynchroProperty() _MongoClient__kill_cursors_queue = SynchroProperty() _MongoClient__options = SynchroProperty() _cache_credentials = SynchroProperty() _close_cursor_now = SynchroProperty() _get_topology = SynchroProperty() _topology = SynchroProperty() _kill_cursors_executor = SynchroProperty() _topology_settings = SynchroProperty() _process_periodic_tasks = SynchroProperty() class _SynchroTransactionContext(Synchro): def __init__(self, delegate): self.delegate = delegate def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): motor_session = self.delegate._session if motor_session.in_transaction: if exc_val is None: self.synchronize(motor_session.commit_transaction)() else: self.synchronize(motor_session.abort_transaction)() class ClientSession(Synchro): __delegate_class__ = motor.motor_tornado.MotorClientSession start_transaction = WrapOutgoing() client = SynchroProperty() def __init__(self, delegate): self.delegate = delegate def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.synchronize(self.delegate.end_session) def with_transaction(self, *args, **kwargs): raise unittest.SkipTest("MOTOR-606 Synchro does not support with_transaction") # For PyMongo tests that access session internals. _client = SynchroProperty() _pinned_address = SynchroProperty() _server_session = SynchroProperty() _transaction = SynchroProperty() _transaction_id = SynchroProperty() _txn_read_preference = SynchroProperty() class Database(Synchro): __delegate_class__ = motor.MotorDatabase get_collection = WrapOutgoing() watch = WrapOutgoing() aggregate = WrapOutgoing() __bool__ = Sync() def __init__(self, client, name, **kwargs): assert isinstance(client, MongoClient), "Expected MongoClient, got %s" % repr(client) self._client = client self.delegate = kwargs.get("delegate") if self.delegate is None: self.delegate = motor.MotorDatabase(client.delegate, name, **kwargs) assert isinstance(self.delegate, motor.MotorDatabase), ( "synchro.Database delegate must be MotorDatabase, not %s" % repr(self.delegate) ) @property def client(self): return self._client def cursor_command(self, *args, **kwargs): if "session" in kwargs: # Workaround for validation added in PYTHON-3228. kwargs["session"] = kwargs["session"].delegate cursor = self.synchronize(self.delegate.cursor_command)(*args, **kwargs) return CommandCursor(cursor) def __getattr__(self, name): return Collection(self, name, delegate=getattr(self.delegate, name)) def __getitem__(self, name): return Collection(self, name, delegate=self.delegate[name]) class Collection(Synchro): __delegate_class__ = motor.MotorCollection find = WrapOutgoing() find_raw_batches = WrapOutgoing() aggregate = WrapOutgoing() aggregate_raw_batches = WrapOutgoing() list_indexes = WrapOutgoing() list_search_indexes = WrapOutgoing() watch = WrapOutgoing() __bool__ = WrapOutgoing() def __init__(self, database, name, **kwargs): if not isinstance(database, Database): raise TypeError( "First argument to synchro Collection must be synchro " "Database, not %s" % repr(database) ) self.database = database self.delegate = kwargs.get("delegate") if self.delegate is None: self.delegate = motor.MotorCollection(self.database.delegate, name, **kwargs) if not isinstance(self.delegate, motor.MotorCollection): raise TypeError( "Expected to get synchro Collection from Database got %s" % repr(self.delegate) ) def __getattr__(self, name): # Access to collections with dotted names, like db.test.mike fullname = self.name + "." + name return Collection(self.database, fullname, delegate=getattr(self.delegate, name)) def __getitem__(self, name): # Access to collections with dotted names, like db.test['mike'] fullname = self.name + "." + name return Collection(self.database, fullname, delegate=self.delegate[name]) class ChangeStream(Synchro): __delegate_class__ = motor.motor_tornado.MotorChangeStream _next = Sync("next") try_next = Sync("try_next") close = Sync("close") def next(self): try: return self._next() except StopAsyncIteration: raise StopIteration def __init__(self, motor_change_stream): self.delegate = motor_change_stream def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __iter__(self): return self __next__ = next # For PyMongo tests that access change stream internals. @property def _cursor(self): raise unittest.SkipTest("test accesses internal _cursor field") _batch_size = SynchroProperty() _client = SynchroProperty() _full_document = SynchroProperty() _max_await_time_ms = SynchroProperty() _pipeline = SynchroProperty() _target = SynchroProperty() class Cursor(Synchro): __delegate_class__ = motor.motor_tornado.MotorCursor batch_size = WrapOutgoing() rewind = WrapOutgoing() clone = WrapOutgoing() close = Sync("close") _next = Sync("next") def __init__(self, motor_cursor): self.delegate = motor_cursor def __iter__(self): return self # These are special cases, they need to be accessed on the class, not just # on instances. @wrap_synchro def __copy__(self): return self.delegate.__copy__() @wrap_synchro def __deepcopy__(self, memo): return self.delegate.__deepcopy__(memo) def next(self): try: return self._next() except StopAsyncIteration: raise StopIteration __next__ = next @property @wrap_synchro def collection(self): return self.delegate.collection def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() # Don't suppress exceptions. return False # For PyMongo tests that access cursor internals. _Cursor__data = SynchroProperty() _Cursor__exhaust = SynchroProperty() _Cursor__max_await_time_ms = SynchroProperty() _Cursor__max_time_ms = SynchroProperty() _Cursor__query_flags = SynchroProperty() _Cursor__query_spec = SynchroProperty() _Cursor__retrieved = SynchroProperty() _Cursor__spec = SynchroProperty() _read_preference = SynchroProperty() class CommandCursor(Cursor): __delegate_class__ = motor.motor_tornado.MotorCommandCursor try_next = Sync("try_next") class GridOutCursor(Cursor): __delegate_class__ = motor.motor_tornado.MotorGridOutCursor def __init__(self, delegate): if not isinstance(delegate, motor.motor_tornado.MotorGridOutCursor): raise TypeError("Expected MotorGridOutCursor, got %r" % delegate) super().__init__(delegate) def next(self): motor_grid_out = super().next() if motor_grid_out: return GridOut(self.collection, delegate=motor_grid_out) __next__ = next class CursorManager: # Motor doesn't support cursor managers, just avoid ImportError. pass class BulkOperationBuilder: pass class GridFSBucket(Synchro): __delegate_class__ = motor.MotorGridFSBucket find = WrapOutgoing() def __init__(self, database, *args, **kwargs): if not isinstance(database, Database): raise TypeError("Expected Database, got %s" % repr(database)) self.delegate = motor.MotorGridFSBucket(database.delegate, *args, **kwargs) class GridIn(Synchro): __delegate_class__ = motor.MotorGridIn _chunk_number = SynchroProperty() _closed = SynchroProperty() def __init__(self, collection, **kwargs): """Can be created with collection and kwargs like a PyMongo GridIn, or with a 'delegate' keyword arg, where delegate is a MotorGridIn. """ delegate = kwargs.pop("delegate", None) if delegate: self.delegate = delegate else: if not isinstance(collection, Collection): raise TypeError("Expected Collection, got %s" % repr(collection)) self.delegate = motor.MotorGridIn(collection.delegate, **kwargs) def __getattr__(self, item): return getattr(self.delegate, item) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): return self.synchronize(self.delegate.__aexit__)(exc_type, exc_val, exc_tb) class SynchroGridOutProperty: def __init__(self, name): self.name = name def __get__(self, obj, objtype): obj.synchronize(obj.delegate.open)() return getattr(obj.delegate, self.name) class GridOut(Synchro): __delegate_class__ = motor.MotorGridOut _id = SynchroGridOutProperty("_id") aliases = SynchroGridOutProperty("aliases") chunk_size = SynchroGridOutProperty("chunk_size") close = SynchroGridOutProperty("close") content_type = SynchroGridOutProperty("content_type") filename = SynchroGridOutProperty("filename") length = SynchroGridOutProperty("length") metadata = SynchroGridOutProperty("metadata") name = SynchroGridOutProperty("name") upload_date = SynchroGridOutProperty("upload_date") def __init__( self, root_collection, file_id=None, file_document=None, session=None, delegate=None ): """Can be created with collection and kwargs like a PyMongo GridOut, or with a 'delegate' keyword arg, where delegate is a MotorGridOut. """ if delegate: self.delegate = delegate else: if not isinstance(root_collection, Collection): raise TypeError("Expected Collection, got %s" % repr(root_collection)) self.delegate = motor.MotorGridOut( root_collection.delegate, file_id, file_document, session=session ) def __getattr__(self, item): self.synchronize(self.delegate.open)() return getattr(self.delegate, item) def __setattr__(self, key, value): # PyMongo's GridOut prohibits setting these values; do the same # to make PyMongo's assertRaises tests pass. if key in ( "_id", "name", "content_type", "length", "chunk_size", "upload_date", "aliases", "metadata", "md5", ): raise AttributeError() super().__setattr__(key, value) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __next__(self): try: return self.synchronize(self.delegate.__anext__)() except StopAsyncIteration: raise StopIteration() def __iter__(self): return self # Unwrap key_vault_client, pymongo expects it to be a regular MongoClient. class AutoEncryptionOpts(encryption_options.AutoEncryptionOpts): def __init__(self, kms_providers, key_vault_namespace, key_vault_client=None, **kwargs): if key_vault_client is not None: key_vault_client = key_vault_client.delegate.delegate super().__init__( kms_providers, key_vault_namespace, key_vault_client=key_vault_client, **kwargs ) class ClientEncryption(Synchro): __delegate_class__ = motor.MotorClientEncryption _enc_col = Sync("create_encrypted_collection") encrypt_expression = Sync("encrypt_expression") def __init__( self, kms_providers, key_vault_namespace, key_vault_client, codec_options, kms_tls_options=None, ): self.delegate = motor.MotorClientEncryption( kms_providers, key_vault_namespace, key_vault_client.delegate, codec_options, kms_tls_options=kms_tls_options, ) def __enter__(self): return self def __exit__(self, *args): return self.synchronize(self.delegate.__aexit__)(*args) def get_keys(self): return Cursor(self.synchronize(self.delegate.get_keys)()) def create_encrypted_collection( self, database, name, encrypted_fields, kms_provider=None, master_key=None, **kwargs, ): coll, ef = self.synchronize(self.delegate.create_encrypted_collection)( database=database.delegate, name=name, encrypted_fields=encrypted_fields, kms_provider=kms_provider, master_key=master_key, **kwargs, ) return Collection(database, coll.name, delegate=coll), ef motor-3.4.0/synchro/synchrotest.py000066400000000000000000000321031460060525600173050ustar00rootroot00000000000000# Copyright 2012-2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor by testing that Synchro, a fake PyMongo implementation built on top of Motor, passes the same unittests as PyMongo. This program monkey-patches sys.modules, so run it alone, rather than as part of a larger test suite. """ import fnmatch import importlib import importlib.abc import importlib.machinery import os import re import shutil import sys from pathlib import Path import pytest import synchro excluded_modules = [ # Exclude some PyMongo tests that can't be applied to Synchro. "test.test_examples", "test.test_threads", "test.test_pooling", "test.test_saslprep", # Complex PyMongo-specific mocking. "test.test_replica_set_reconfig", # Accesses PyMongo internals. "test.test_retryable_writes", # Accesses PyMongo internals. Tested directly in Motor. "test.test_session", # Deprecated in PyMongo, removed in Motor 2.0. "test.test_gridfs", # Skip mypy/typing tests. "test.test_typing", # Relies of specifics of __all__ "test.test_default_exports", # Motor does not support CSOT. "test.test_csot", ] # Patterns consist of the TestClass.test_method # You can use * for any portion of the class or method pattern excluded_tests = [ # Motor's reprs aren't the same as PyMongo's. "*.test_repr", "TestClient.test_unix_socket", # Motor extends the handshake metadata. "ClientUnitTest.test_metadata", # Lazy-connection tests require multithreading; we test concurrent # lazy connection directly. "TestClientLazyConnect.*", # Motor doesn't support forking or threading. "*.test_interrupt_signal", "TestSCRAM.test_scram_threaded", "TestGSSAPI.test_gssapi_threaded", "*.test_concurrent_close", "TestUnifiedInterruptInUsePoolClear.*", # These are in test_gridfs_bucket. "TestGridfs.test_threaded_reads", "TestGridfs.test_threaded_writes", # Can't do MotorCollection(name, create=True), Motor constructors do no I/O. "TestCollection.test_create", # Requires indexing / slicing cursors, which Motor doesn't do, see MOTOR-84. "TestCollection.test_min_query", "TestCursor.test_clone", "TestCursor.test_clone_empty", "TestCursor.test_getitem_numeric_index", "TestCursor.test_getitem_slice_index", "TestCursor.test_tailable", "TestRawBatchCursor.test_get_item", "TestRawBatchCommandCursor.test_get_item", # No context-manager protocol for MotorCursor. "TestCursor.test_with_statement", # Motor's cursors initialize lazily. "TestRawBatchCommandCursor.test_monitoring", # Can't iterate a GridOut in Motor. "TestGridFile.test_iterator", "TestGridfs.test_missing_length_iter", # No context-manager protocol for MotorGridIn, and can't set attrs. "TestGridFile.test_context_manager", "TestGridFile.test_grid_in_default_opts", "TestGridFile.test_set_after_close", # GridOut always connects lazily in Motor. "TestGridFile.test_grid_out_lazy_connect", "TestGridfs.test_gridfs_lazy_connect", # In test_gridfs_bucket. # Complex PyMongo-specific mocking. "*.test_wire_version", "TestClient.test_heartbeat_frequency_ms", "TestExhaustCursor.*", "TestHeartbeatMonitoring.*", "TestMongoClientFailover.*", "TestMongosLoadBalancing.*", "TestSSL.test_system_certs_config_error", "TestCMAP.test_cmap_wait_queue_timeout_must_aggressively_timeout_threads_enqueued_longer_than_waitQueueTimeoutMS", # Accesses PyMongo internals. "TestClient.test_close_kills_cursors", "TestClient.test_stale_getmore", "TestClient.test_direct_connection", "TestCollection.test_aggregation_cursor", "TestCommandAndReadPreference.*", "TestCommandMonitoring.test_get_more_failure", "TestCommandMonitoring.test_sensitive_commands", "TestCursor.test_allow_disk_use", "TestCursor.test_close_kills_cursor_synchronously", "TestCursor.test_delete_not_initialized", "TestGridFile.test_grid_out_cursor_options", "TestGridFile.test_survive_cursor_not_found", "TestMaxStaleness.test_last_write_date", "TestSelections.test_bool", "TestCollation.*", "TestCollection.test_find_one_and_write_concern", "TestCollection.test_write_error_text_handling", "TestBinary.test_uuid_queries", "TestCursor.test_comment", "TestCursor.test_where", "TestGridfs.test_gridfs_find", # Uses patching on grid_file._UPLOAD_BUFFER_SIZE which fails in synchro. "TestGridfs.test_abort", "TestGridfs.test_upload_batching", "TestGridfs.test_upload_bulk_write_error", "TestKmsTLSOptions.test_05_tlsDisableOCSPEndpointCheck_is_permitted", # Tests that use "authenticate" or "logoout", removed in Motor 2.0. "TestSASLPlain.test_sasl_plain_bad_credentials", "TestSCRAM.test_scram", "TestSCRAMSHA1.test_scram_sha1", # Uses "collection_names", deprecated in PyMongo, removed in Motor 2.0. "TestSingleSecondaryOk.test_reads_from_secondary", # Slow. "TestDatabase.test_list_collection_names", # MOTOR-425 these tests fail with duplicate key errors. "TestClusterChangeStreamsWCustomTypes.*", "TestCollectionChangeStreamsWCustomTypes.*", "TestDatabaseChangeStreamsWCustomTypes.*", # Tests that use warnings.catch_warnings which don't show up in Motor. "TestCursor.test_min_max_without_hint", # TODO: MOTOR-606 "TestTransactionsConvenientAPI.*", "TestTransactions.test_create_collection", # Motor's change streams need Python 3.5 to support async iteration but # these change streams tests spawn threads which don't work without an # IO loop. "*.test_next_blocks", "*.test_aggregate_cursor_blocks", # Can't run these tests because they use threads. "*.test_ignore_stale_connection_errors", "*.test_pool_paused_error_is_retryable", # Needs synchro.GridFS class, see MOTOR-609. "TestTransactions.test_gridfs_does_not_support_transactions", # PYTHON-3228 _tmp_session should validate session input "*.test_helpers_with_let", # Relies on comment being in the method signatures, which would force use # to rewrite much of AgnosticCollection. "*.test_collection_helpers", "*.test_database_helpers", "*.test_client_helpers", # This test is too slow given all of the wrapping logic. "*.test_transaction_starts_with_batched_write", # This test is too flaky given all the wrapping logic. "TestProse.test_load_balancing", # This feature is going away in PyMongo 5 "*.test_iteration", # MD5 is deprecated "*.test_md5", # Causes a deadlock. "TestFork.*", # Also causes a deadlock. "TestClientSimple.test_fork", # This method requires credentials. "TestOnDemandAWSCredentials.test_02_success", # These methods are picked up by nose despite not being a unittest. "TestRewrapWithSeparateClientEncryption.run_test", "TestCustomEndpoint.run_test_expected_success", "TestDataKeyDoubleEncryption.run_test", # These tests are failing right now. "TestUnifiedFindShutdownError.test_Concurrent_shutdown_error_on_find", "TestUnifiedInsertShutdownError.test_Concurrent_shutdown_error_on_insert", "TestUnifiedPoolClearedError.test_PoolClearedError_does_not_mark_server_unknown", # These tests have hard-coded values that differ from motor. "TestClient.test_handshake*", # This test is not a valid unittest target. "TestRangeQueryProse.run_test_cases", ] excluded_modules_matched = set() excluded_tests_matched = set() # Validate the exclude lists. for item in excluded_modules: if not re.match(r"^test\.[a-zA-Z_]+$", item): raise ValueError(f"Improper excluded module {item}") for item in excluded_tests: cls_pattern, _, mod_pattern = item.partition(".") if cls_pattern != "*" and not re.match(r"^[a-zA-Z\d]+$", cls_pattern): raise ValueError(f"Ill-formatted excluded test: {item}") if mod_pattern != "*" and not re.match(r"^[a-zA-Z\d_\*]+$", mod_pattern): raise ValueError(f"Ill-formatted excluded test: {item}") class SynchroModuleFinder(importlib.abc.MetaPathFinder): def __init__(self): self._loader = SynchroModuleLoader() def find_spec(self, fullname, path, target=None): if self._loader.patch_spec(fullname): return importlib.machinery.ModuleSpec(fullname, self._loader) # Let regular module search continue. return None class SynchroModuleLoader(importlib.abc.Loader): def patch_spec(self, fullname): parts = fullname.split(".") if parts[-1] in ("gridfs", "pymongo"): # E.g. "import pymongo" return True elif len(parts) >= 2 and parts[-2] in ("gridfs", "pymongo"): # E.g. "import pymongo.mongo_client" return True return False def exec_module(self, module): pass def create_module(self, spec): if self.patch_spec(spec.name): return synchro # Let regular module search continue. return None class SynchroPytestPlugin: def pytest_collection_modifyitems(self, session, config, items): for item in items[:]: if not want_module(item.module): item.addSkip("", reason="Synchro excluded module") continue fn = item.function if item.parent == item.module: if not want_function(fn): item.addSkip("", reason="Synchro excluded function") elif not want_method(fn, item.parent.name): item.addSkip("", reason="Synchro excluded method") def want_module(module): # Depending on PYTHONPATH, Motor's direct tests may be imported - don't # run them now. if module.__name__.startswith("test.test_motor_"): return False for module_name in excluded_modules: if module_name.endswith("*"): if module.__name__.startswith(module_name.rstrip("*")): # E.g., test_motor_cursor matches "test_motor_*". excluded_modules_matched.add(module_name) return False elif module.__name__ == module_name: excluded_modules_matched.add(module_name) return False return True def want_function(fn): # PyMongo's test generators run at import time; tell pytest not to run # them as unittests. if fn.__name__ in ("test_cases",): return False return True def want_method(method, classname): for excluded_name in excluded_tests: # Should we exclude this method's whole TestCase? cls_pattern, _, method_pattern = excluded_name.partition(".") suite_matches = fnmatch.fnmatch(classname, cls_pattern) # Should we exclude this particular method? method_matches = method.__name__ == method_pattern or fnmatch.fnmatch( method.__name__, method_pattern ) if suite_matches and method_matches: excluded_tests_matched.add(excluded_name) return False return True if __name__ == "__main__": # Monkey-patch all pymongo's unittests so they think Synchro is the # real PyMongo. sys.meta_path[0:0] = [SynchroModuleFinder()] # Delete the cached pymongo/gridfs modules so that SynchroModuleFinder will # be invoked in Python 3, see # https://docs.python.org/3/reference/import.html#import-hooks for n in [ "pymongo", "pymongo.collection", "pymongo.client_session", "pymongo.command_cursor", "pymongo.change_stream", "pymongo.cursor", "pymongo.encryption", "pymongo.encryption_options", "pymongo.mongo_client", "pymongo.database", "pymongo.srv_resolver", "gridfs", "gridfs.grid_file", ]: sys.modules.pop(n) # Prep the xUnit report dir. root = Path(__file__).absolute().parent.parent xunit_dir = root / "xunit-results" if xunit_dir.exists(): shutil.rmtree(xunit_dir) # Run the tests from the pymongo target dir with our custom plugin. os.chdir(sys.argv[1]) code = pytest.main(sys.argv[2:], plugins=[SynchroPytestPlugin()]) if code != 0: sys.exit(code) # Copy over the xUnit report. xunit_dir.mkdir() target = Path(sys.argv[1]) / "xunit-results" shutil.copy(target / "TEST-results.xml", xunit_dir / "TEST-results.xml") if os.environ.get("CHECK_EXCLUDE_PATTERNS"): unused_module_pats = set(excluded_modules) - excluded_modules_matched assert not unused_module_pats, "Unused module patterns: {unused_module_pats}" unused_test_pats = set(excluded_tests) - excluded_tests_matched assert not unused_test_pats, f"Unused test patterns: {unused_test_pats}" motor-3.4.0/test/000077500000000000000000000000001460060525600136415ustar00rootroot00000000000000motor-3.4.0/test/__init__.py000066400000000000000000000025741460060525600157620ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" from test.test_environment import CLIENT_PEM, db_user, env # noqa: F401 from typing import Any from unittest import SkipTest # noqa: F401 try: # Enable the fault handler to dump the traceback of each running # thread # after a segfault. import faulthandler faulthandler.enable() # Dump the tracebacks of all threads after 25 minutes. if hasattr(faulthandler, "dump_traceback_later"): faulthandler.dump_traceback_later(25 * 60) except ImportError: pass class MockRequestHandler: """For testing MotorGridOut.stream_to_handler.""" def __init__(self) -> None: self.n_written = 0 def write(self, data: Any) -> None: self.n_written += len(data) def flush(self) -> None: pass motor-3.4.0/test/assert_logs_backport.py000066400000000000000000000050771460060525600204360ustar00rootroot00000000000000"""Backport assertLogs from Python 3.4.""" import collections import logging # mypy: ignore-errors _LoggingWatcher = collections.namedtuple("_LoggingWatcher", ["records", "output"]) class _BaseTestCaseContext: def __init__(self, test_case): self.test_case = test_case def _raiseFailure(self, standardMsg): msg = self.test_case._formatMessage(self.msg, standardMsg) raise self.test_case.failureException(msg) class _CapturingHandler(logging.Handler): """Handler capturing all (raw and formatted) logging output.""" def __init__(self): logging.Handler.__init__(self) self.watcher = _LoggingWatcher([], []) def flush(self): pass def emit(self, record): self.watcher.records.append(record) msg = self.format(record) self.watcher.output.append(msg) class _AssertLogsContext(_BaseTestCaseContext): """A context manager used to implement TestCase.assertLogs().""" LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s" def __init__(self, test_case, logger_name, level): assert isinstance(level, int) _BaseTestCaseContext.__init__(self, test_case) self.logger_name = logger_name self.level = level or logging.INFO self.level_name = logging.getLevelName(level) self.msg = None def __enter__(self): if isinstance(self.logger_name, logging.Logger): logger = self.logger = self.logger_name else: logger = self.logger = logging.getLogger(self.logger_name) formatter = logging.Formatter(self.LOGGING_FORMAT) handler = _CapturingHandler() handler.setFormatter(formatter) self.watcher = handler.watcher self.old_handlers = logger.handlers[:] self.old_level = logger.level self.old_propagate = logger.propagate logger.handlers = [handler] logger.setLevel(self.level) logger.propagate = False return handler.watcher def __exit__(self, exc_type, exc_value, tb): self.logger.handlers = self.old_handlers self.logger.propagate = self.old_propagate self.logger.setLevel(self.old_level) if exc_type is not None: # let unexpected exceptions pass through return False if len(self.watcher.records) == 0: self._raiseFailure( f"no logs of level {self.level_name} or higher triggered on {self.logger.name}" ) class AssertLogsMixin: def assertLogs(self, logger=None, level=None): return _AssertLogsContext(self, logger, level) motor-3.4.0/test/asyncio_tests/000077500000000000000000000000001460060525600165305ustar00rootroot00000000000000motor-3.4.0/test/asyncio_tests/__init__.py000066400000000000000000000211661460060525600206470ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Utilities for testing Motor with asyncio.""" import asyncio import functools import gc import inspect import unittest from asyncio import ensure_future from test.assert_logs_backport import AssertLogsMixin from test.test_environment import CA_PEM, CLIENT_PEM, env from test.utils import get_async_test_timeout from unittest import SkipTest from mockupdb import MockupDB from motor import motor_asyncio # mypy: ignore-errors class _TestMethodWrapper: """Wraps a test method to raise an error if it returns a value. This is mainly used to detect undecorated generators (if a test method yields it must use a decorator to consume the generator), but will also detect other kinds of return values (these are not necessarily errors, but we alert anyway since there is no good reason to return a value from a test). Adapted from Tornado's test framework. """ def __init__(self, orig_method): self.orig_method = orig_method def __call__(self): result = self.orig_method() if inspect.iscoroutine(result): # Cancel the undecorated task to avoid this warning: # RuntimeWarning: coroutine 'test_foo' was never awaited task = ensure_future(result) task.cancel() raise TypeError("Generator test methods should be decorated with @asyncio_test") elif result is not None: raise ValueError("Return value from test method ignored: %r" % result) def __getattr__(self, name): """Proxy all unknown attributes to the original method. This is important for some of the decorators in the `unittest` module, such as `unittest.skipIf`. """ return getattr(self.orig_method, name) class AsyncIOTestCase(AssertLogsMixin, unittest.TestCase): longMessage = True # Used by unittest.TestCase ssl = False # If True, connect with SSL, skip if mongod isn't SSL def __init__(self, methodName="runTest"): super().__init__(methodName) # It's easy to forget the @asyncio_test decorator, but if you do # the test will silently be ignored because nothing will consume # the generator. Replace the test method with a wrapper that will # make sure it's not an undecorated generator. # (Adapted from Tornado's AsyncTestCase.) setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName))) def setUp(self): super().setUp() # Ensure that the event loop is passed explicitly in Motor. asyncio.set_event_loop(None) self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) if self.ssl and not env.mongod_started_with_ssl: raise SkipTest("mongod doesn't support SSL, or is down") self.cx = self.asyncio_client() self.db = self.cx.motor_test self.collection = self.db.test_collection self.loop.run_until_complete(self.collection.drop()) def get_client_kwargs(self, set_loop=True, **kwargs): if set_loop: kwargs.setdefault("io_loop", self.loop) if env.mongod_started_with_ssl: kwargs.setdefault("tlsCAFile", CA_PEM) kwargs.setdefault("tlsCertificateKeyFile", CLIENT_PEM) kwargs.setdefault("tls", env.mongod_started_with_ssl) return kwargs def asyncio_client( self, uri=None, *args, set_loop=True, **kwargs ) -> motor_asyncio.AsyncIOMotorClient: """Get an AsyncIOMotorClient. Ignores self.ssl, you must pass 'ssl' argument. """ return motor_asyncio.AsyncIOMotorClient( kwargs.pop("host", None) or uri or env.uri, *args, **self.get_client_kwargs(**kwargs, set_loop=set_loop), ) def asyncio_rsc(self, uri=None, *args, **kwargs): """Get an open MotorClient for replica set. Ignores self.ssl, you must pass 'ssl' argument. """ return motor_asyncio.AsyncIOMotorClient( uri or env.rs_uri, *args, **self.get_client_kwargs(**kwargs) ) async def make_test_data(self): await self.collection.delete_many({}) await self.collection.insert_many([{"_id": i} for i in range(200)]) make_test_data.__test__ = False def tearDown(self): self.cx.close() self.loop.stop() self.loop.run_forever() self.loop.close() asyncio.set_event_loop(asyncio.new_event_loop()) gc.collect() class AsyncIOMockServerTestCase(AsyncIOTestCase): def server(self, *args, **kwargs): server = MockupDB(*args, **kwargs) server.run() self.addCleanup(server.stop) return server def client_server(self, *args, **kwargs): server = self.server(*args, **kwargs) client = motor_asyncio.AsyncIOMotorClient(server.uri, io_loop=self.loop) self.addCleanup(client.close) return client, server def run_thread(self, fn, *args, **kwargs): return self.loop.run_in_executor(None, functools.partial(fn, *args, **kwargs)) def fetch_next(self, cursor): async def fetch_next(): return await cursor.fetch_next return ensure_future(fetch_next()) # TODO: Spin off to a PyPI package. def asyncio_test(func=None, timeout=None): """Decorator for coroutine methods of AsyncIOTestCase:: class MyTestCase(AsyncIOTestCase): @asyncio_test def test(self): # Your test code here.... pass Default timeout is 5 seconds. Override like:: class MyTestCase(AsyncIOTestCase): @asyncio_test(timeout=10) def test(self): # Your test code here.... pass You can also set the ASYNC_TEST_TIMEOUT environment variable to a number of seconds. The final timeout is the ASYNC_TEST_TIMEOUT or the timeout in the test (5 seconds or the passed-in timeout), whichever is longest. """ def wrap(f): @functools.wraps(f) def wrapped(self, *args, **kwargs): if timeout is None: actual_timeout = get_async_test_timeout() else: actual_timeout = get_async_test_timeout(timeout) coro_exc = None def exc_handler(loop, context): nonlocal coro_exc # Exception is optional. coro_exc = context.get("exception", Exception(context)) # Raise CancelledError from run_until_complete below. task.cancel() self.loop.set_exception_handler(exc_handler) coro = asyncio.wait_for(f(self, *args, **kwargs), actual_timeout) task = ensure_future(coro, loop=self.loop) try: self.loop.run_until_complete(task) except BaseException: if coro_exc: # Raise the error thrown in on_timeout, with only the # traceback from the coroutine itself, not from # run_until_complete. raise coro_exc from None raise return wrapped if func is not None: # Used like: # @gen_test # def f(self): # pass if not inspect.isfunction(func): msg = ( "%r is not a test method. Pass a timeout as" " a keyword argument, like @asyncio_test(timeout=7)" ) raise TypeError(msg % func) return wrap(func) else: # Used like @gen_test(timeout=10) return wrap async def get_command_line(client): command_line = await client.admin.command("getCmdLineOpts") assert command_line["ok"] == 1, "getCmdLineOpts() failed" return command_line async def server_is_mongos(client): ismaster_response = await client.admin.command("ismaster") return ismaster_response.get("msg") == "isdbgrid" async def skip_if_mongos(client): is_mongos = await server_is_mongos(client) if is_mongos: raise unittest.SkipTest("connected to mongos") async def remove_all_users(db): await db.command({"dropAllUsersFromDatabase": 1}) motor-3.4.0/test/asyncio_tests/test_aiohttp_gridfs.py000066400000000000000000000247551460060525600231640ustar00rootroot00000000000000# Copyright 2016 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. """Test Motor's AIOHTTPGridFSHandler.""" import asyncio import datetime import email import logging import test import time from test.asyncio_tests import AsyncIOTestCase, asyncio_test import aiohttp import aiohttp.web import gridfs from motor.aiohttp import AIOHTTPGridFS from motor.motor_gridfs import _hash_gridout def format_date(d): return time.strftime("%a, %d %b %Y %H:%M:%S GMT", d.utctimetuple()) def parse_date(d): date_tuple = email.utils.parsedate(d) return datetime.datetime.fromtimestamp(time.mktime(date_tuple)) def expires(response): return parse_date(response.headers["Expires"]) class AIOHTTPGridFSHandlerTestBase(AsyncIOTestCase): fs = None file_id = None def tearDown(self): self.loop.run_until_complete(self.stop()) super().tearDown() @classmethod def setUpClass(cls): super().setUpClass() logging.getLogger("aiohttp.web").setLevel(logging.CRITICAL) cls.fs = gridfs.GridFS(test.env.sync_cx.motor_test) # Make a 500k file in GridFS with filename 'foo' cls.contents = b"Jesse" * 100 * 1024 # Record when we created the file, to check the Last-Modified header cls.put_start = datetime.datetime.now(datetime.timezone.utc).replace( microsecond=0, tzinfo=None ) file_id = "id" cls.file_id = file_id cls.fs.delete(cls.file_id) cls.fs.put(cls.contents, _id=file_id, filename="foo", content_type="my type") item = cls.fs.get(file_id) cls.contents_hash = _hash_gridout(item) cls.put_end = datetime.datetime.now(datetime.timezone.utc).replace( microsecond=0, tzinfo=None ) cls.app = cls.srv = cls.app_handler = None @classmethod def tearDownClass(cls): cls.fs.delete(cls.file_id) super().tearDownClass() async def start_app(self, http_gridfs=None, extra_routes=None): self.app = aiohttp.web.Application() resource = self.app.router.add_resource("/fs/{filename}") handler = http_gridfs or AIOHTTPGridFS(self.db) resource.add_route("GET", handler) resource.add_route("HEAD", handler) if extra_routes: for route, handler in extra_routes.items(): resource = self.app.router.add_resource(route) resource.add_route("GET", handler) self.app_handler = self.app.make_handler() server = self.loop.create_server(self.app_handler, host="localhost", port=8088) self.srv, _ = await asyncio.gather(server, self.app.startup()) async def request(self, method, path, if_modified_since=None, headers=None): headers = headers or {} if if_modified_since: headers["If-Modified-Since"] = format_date(if_modified_since) session = aiohttp.ClientSession() try: method = getattr(session, method) resp = await method("http://localhost:8088%s" % path, headers=headers) await resp.read() return resp finally: await session.close() def get(self, path, **kwargs): return self.request("get", path, **kwargs) def head(self, path, **kwargs): return self.request("head", path, **kwargs) async def stop(self): # aiohttp.rtfd.io/en/stable/web.html#aiohttp-web-graceful-shutdown self.srv.close() await self.srv.wait_closed() await self.app.shutdown() await self.app_handler.shutdown(timeout=1) await self.app.cleanup() class AIOHTTPGridFSHandlerTest(AIOHTTPGridFSHandlerTestBase): @asyncio_test async def test_basic(self): await self.start_app() # First request response = await self.get("/fs/foo") self.assertEqual(200, response.status) self.assertEqual(self.contents, (await response.read())) self.assertEqual(len(self.contents), int(response.headers["Content-Length"])) self.assertEqual("my type", response.headers["Content-Type"]) self.assertEqual("public", response.headers["Cache-Control"]) self.assertTrue("Expires" not in response.headers) etag = response.headers["Etag"] last_mod_dt = parse_date(response.headers["Last-Modified"]) self.assertEqual(self.contents_hash, etag.strip('"')) self.assertTrue(self.put_start <= last_mod_dt <= self.put_end) # Now check we get 304 NOT MODIFIED responses as appropriate for ims_value in (last_mod_dt, last_mod_dt + datetime.timedelta(seconds=1)): response = await self.get("/fs/foo", if_modified_since=ims_value) self.assertEqual(304, response.status) self.assertEqual(b"", (await response.read())) # If-Modified-Since in the past, get whole response back response = await self.get( "/fs/foo", if_modified_since=last_mod_dt - datetime.timedelta(seconds=1) ) self.assertEqual(200, response.status) self.assertEqual(self.contents, (await response.read())) # Matching Etag response = await self.get("/fs/foo", headers={"If-None-Match": etag}) self.assertEqual(304, response.status) self.assertEqual(b"", (await response.read())) # Mismatched Etag response = await self.get("/fs/foo", headers={"If-None-Match": etag + "a"}) self.assertEqual(200, response.status) self.assertEqual(self.contents, (await response.read())) @asyncio_test async def test_404(self): await self.start_app() response = await self.get("/fs/bar") self.assertEqual(404, response.status) @asyncio_test async def test_head(self): await self.start_app() response = await self.head("/fs/foo") etag = response.headers["Etag"] last_mod_dt = parse_date(response.headers["Last-Modified"]) self.assertEqual(200, response.status) # Empty body for HEAD request. self.assertEqual(b"", (await response.read())) self.assertEqual(len(self.contents), int(response.headers["Content-Length"])) self.assertEqual("my type", response.headers["Content-Type"]) self.assertEqual(self.contents_hash, etag.strip('"')) self.assertTrue(self.put_start <= last_mod_dt <= self.put_end) self.assertEqual("public", response.headers["Cache-Control"]) @asyncio_test async def test_bad_route(self): handler = AIOHTTPGridFS(self.db) await self.start_app(extra_routes={"/x/{wrongname}": handler}) response = await self.get("/x/foo") self.assertEqual(500, response.status) msg = 'Bad AIOHTTPGridFS route "/x/{wrongname}"' self.assertIn(msg, (await response.text())) @asyncio_test async def test_content_type(self): await self.start_app() # Check that GridFSHandler uses file extension to guess Content-Type # if not provided for filename, expected_type in [ ("bar", "octet-stream"), ("bar.png", "png"), ("ht.html", "html"), ]: # 'fs' is PyMongo's blocking GridFS _id = self.fs.put(b"", filename=filename) self.addCleanup(self.fs.delete, _id) for method in self.get, self.head: response = await method("/fs/" + filename) self.assertEqual(200, response.status) # mimetypes are platform-defined, be fuzzy self.assertIn(expected_type, response.content_type) @asyncio_test async def test_post(self): # Only allow GET and HEAD, even if a POST route is added. await self.start_app() result = await self.request("post", "/fs/foo") self.assertEqual(405, result.status) class AIOHTTPTZAwareGridFSHandlerTest(AIOHTTPGridFSHandlerTestBase): @asyncio_test async def test_tz_aware(self): client = self.asyncio_client(tz_aware=True) await self.start_app(AIOHTTPGridFS(client.motor_test)) now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) ago = now - datetime.timedelta(minutes=10) hence = now + datetime.timedelta(minutes=10) response = await self.get("/fs/foo", if_modified_since=ago) self.assertEqual(200, response.status) response = await self.get("/fs/foo", if_modified_since=hence) self.assertEqual(304, response.status) class AIOHTTPCustomHTTPGridFSTest(AIOHTTPGridFSHandlerTestBase): @asyncio_test async def test_get_gridfs_file(self): def getter(bucket, filename, request): # Test overriding the get_gridfs_file() method, path is # interpreted as file_id instead of filename. return bucket.open_download_stream(file_id=filename) def cache_time(path, modified, mime_type): return 10 def extras(response, gridout): response.headers["quux"] = "fizzledy" await self.start_app( AIOHTTPGridFS( self.db, get_gridfs_file=getter, get_cache_time=cache_time, set_extra_headers=extras ) ) # We overrode get_gridfs_file so we expect getting by filename *not* to # work now; we'll get a 404. We have to get by file_id now. response = await self.get("/fs/foo") self.assertEqual(404, response.status) response = await self.get("/fs/" + str(self.file_id)) self.assertEqual(200, response.status) self.assertEqual(self.contents, (await response.read())) cache_control = response.headers["Cache-Control"] self.assertRegex(cache_control, r"max-age=\d+") self.assertEqual(10, int(cache_control.split("=")[1])) expiration = parse_date(response.headers["Expires"]) now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) # It should expire about 10 seconds from now self.assertTrue( datetime.timedelta(seconds=8) < expiration - now < datetime.timedelta(seconds=12) ) self.assertEqual("fizzledy", response.headers["quux"]) motor-3.4.0/test/asyncio_tests/test_asyncio_await.py000066400000000000000000000166501460060525600230030ustar00rootroot00000000000000# Copyright 2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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 test import warnings from test import env from test.asyncio_tests import AsyncIOTestCase, asyncio_test import bson from motor.motor_asyncio import AsyncIOMotorClientSession, AsyncIOMotorGridFSBucket class TestAsyncIOAwait(AsyncIOTestCase): @asyncio_test async def test_to_list(self): collection = self.collection await collection.delete_many({}) results = await collection.find().sort("_id").to_list(length=None) self.assertEqual([], results) docs = [{"_id": 1}, {"_id": 2}] await collection.insert_many(docs) cursor = collection.find().sort("_id") results = await cursor.to_list(length=None) self.assertEqual(docs, results) results = await cursor.to_list(length=None) self.assertEqual([], results) @asyncio_test async def test_iter_cursor(self): collection = self.collection await collection.delete_many({}) for n_docs in 0, 1, 2, 10: if n_docs: docs = [{"_id": i} for i in range(n_docs)] await collection.insert_many(docs) # Force extra batches to test iteration. j = 0 async for doc in collection.find().sort("_id").batch_size(3): self.assertEqual(j, doc["_id"]) j += 1 self.assertEqual(j, n_docs) j = 0 raw_cursor = collection.find_raw_batches().sort("_id").batch_size(3) async for batch in raw_cursor: j += len(bson.decode_all(batch)) await collection.delete_many({}) @asyncio_test async def test_iter_aggregate(self): collection = self.collection await collection.delete_many({}) pipeline = [{"$sort": {"_id": 1}}] # Empty iterator. async for _ in collection.aggregate(pipeline): self.fail() for n_docs in 1, 2, 10: if n_docs: docs = [{"_id": i} for i in range(n_docs)] await collection.insert_many(docs) # Force extra batches to test iteration. j = 0 cursor = collection.aggregate(pipeline).batch_size(3) async for doc in cursor: self.assertEqual(j, doc["_id"]) j += 1 self.assertEqual(j, n_docs) j = 0 raw = collection.aggregate_raw_batches(pipeline).batch_size(3) async for batch in raw: j += len(bson.decode_all(batch)) self.assertEqual(j, n_docs) await collection.delete_many({}) @asyncio_test async def test_iter_gridfs(self): gfs = AsyncIOMotorGridFSBucket(self.db) async def cleanup(): await self.db.fs.files.delete_many({}) await self.db.fs.chunks.delete_many({}) await cleanup() # Empty iterator. async for _ in gfs.find({"_id": 1}): self.fail() data = b"data" for n_files in 1, 2, 10: for _ in range(n_files): async with gfs.open_upload_stream(filename="filename") as f: await f.write(data) # Force extra batches to test iteration. j = 0 async for _ in gfs.find({"filename": "filename"}).batch_size(3): j += 1 self.assertEqual(j, n_files) await cleanup() await gfs.upload_from_stream_with_id(1, "filename", source=data, chunk_size_bytes=1) cursor = gfs.find({"_id": 1}) await cursor.fetch_next gout = cursor.next_object() chunks = [] async for chunk in gout: chunks.append(chunk) self.assertEqual(len(chunks), len(data)) self.assertEqual(b"".join(chunks), data) @asyncio_test async def test_stream_to_handler(self): # Sort of Tornado-specific, but it does work with asyncio. fs = AsyncIOMotorGridFSBucket(self.db) content_length = 1000 await fs.delete(1) await fs.upload_from_stream_with_id(1, "filename", source=b"a" * content_length) gridout = await fs.open_download_stream(1) handler = test.MockRequestHandler() await gridout.stream_to_handler(handler) self.assertEqual(content_length, handler.n_written) await fs.delete(1) @asyncio_test async def test_cursor_iter(self): # Have we handled the async iterator change in Python 3.5.2?: # python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions with warnings.catch_warnings(record=True) as w: async for _ in self.collection.find(): pass if w: self.fail(w[0].message) @asyncio_test async def test_list_indexes(self): await self.collection.drop() await self.collection.create_index([("x", 1)]) await self.collection.create_index([("y", -1)]) keys = set() async for info in self.collection.list_indexes(): keys.add(info["name"]) self.assertEqual(keys, {"_id_", "x_1", "y_-1"}) @env.require_version_min(3, 6) @env.require_replica_set @asyncio_test async def test_session(self): s = await self.cx.start_session() self.assertIsInstance(s, AsyncIOMotorClientSession) self.assertIs(s.client, self.cx) self.assertFalse(s.has_ended) await s.end_session() self.assertTrue(s.has_ended) # Raises a helpful error if used in a regular with-statement. with self.assertRaises(AttributeError) as ctx: with await self.cx.start_session(): pass self.assertIn("async with await", str(ctx.exception)) async with await self.cx.start_session() as s: self.assertIsInstance(s, AsyncIOMotorClientSession) self.assertFalse(s.has_ended) await s.end_session() self.assertTrue(s.has_ended) self.assertTrue(s.has_ended) @env.require_version_min(3, 7) @env.require_replica_set @asyncio_test async def test_transaction(self): async with await self.cx.start_session() as s: s.start_transaction() self.assertTrue(s.in_transaction) self.assertFalse(s.has_ended) await s.end_session() self.assertFalse(s.in_transaction) self.assertTrue(s.has_ended) async with await self.cx.start_session() as s: # Use start_transaction in "async with", not "async with await". with self.assertRaises(TypeError): async with await s.start_transaction(): pass await s.abort_transaction() async with s.start_transaction(): self.assertTrue(s.in_transaction) self.assertFalse(s.has_ended) self.assertFalse(s.in_transaction) self.assertFalse(s.has_ended) self.assertTrue(s.has_ended) motor-3.4.0/test/asyncio_tests/test_asyncio_basic.py000066400000000000000000000137321460060525600227550ustar00rootroot00000000000000# Copyright 2013-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import test import unittest from abc import ABC from asyncio import new_event_loop, set_event_loop from multiprocessing import Pipe from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.utils import ignore_deprecations import pymongo from pymongo import WriteConcern from pymongo.read_preferences import Nearest, ReadPreference, Secondary from motor import motor_asyncio class AIOMotorTestBasic(AsyncIOTestCase): def test_repr(self): self.assertTrue(repr(self.cx).startswith("AsyncIOMotorClient")) self.assertTrue(repr(self.db).startswith("AsyncIOMotorDatabase")) self.assertTrue(repr(self.collection).startswith("AsyncIOMotorCollection")) cursor = self.collection.find() self.assertTrue(repr(cursor).startswith("AsyncIOMotorCursor")) @asyncio_test async def test_write_concern(self): # Default empty dict means "w=1" self.assertEqual(WriteConcern(), self.cx.write_concern) await self.collection.delete_many({}) await self.collection.insert_one({"_id": 0}) for wc_opts in [ {}, {"w": 0}, {"w": 1}, {"wTimeoutMS": 1000}, ]: cx = self.asyncio_client(test.env.uri, **wc_opts) wtimeout = wc_opts.pop("wTimeoutMS", None) if wtimeout: wc_opts["wtimeout"] = wtimeout wc = WriteConcern(**wc_opts) self.assertEqual(wc, cx.write_concern) db = cx.motor_test self.assertEqual(wc, db.write_concern) collection = db.test_collection self.assertEqual(wc, collection.write_concern) if wc.acknowledged: with self.assertRaises(pymongo.errors.DuplicateKeyError): await collection.insert_one({"_id": 0}) else: await collection.insert_one({"_id": 0}) # No error # No error c = collection.with_options(write_concern=WriteConcern(w=0)) await c.insert_one({"_id": 0}) cx.close() @ignore_deprecations @asyncio_test async def test_read_preference(self): # Check the default cx = motor_asyncio.AsyncIOMotorClient(test.env.uri, io_loop=self.loop) self.assertEqual(ReadPreference.PRIMARY, cx.read_preference) # We can set mode, tags, and latency. cx = self.asyncio_client( read_preference=Secondary(tag_sets=[{"foo": "bar"}]), localThresholdMS=42 ) self.assertEqual(ReadPreference.SECONDARY.mode, cx.read_preference.mode) self.assertEqual([{"foo": "bar"}], cx.read_preference.tag_sets) self.assertEqual(42, cx.options.local_threshold_ms) # Make a MotorCursor and get its PyMongo Cursor collection = cx.motor_test.test_collection.with_options( read_preference=Nearest(tag_sets=[{"yay": "jesse"}]) ) motor_cursor = collection.find() cursor = motor_cursor.delegate self.assertEqual(Nearest(tag_sets=[{"yay": "jesse"}]), cursor._read_preference()) cx.close() def test_underscore(self): self.assertIsInstance(self.cx["_db"], motor_asyncio.AsyncIOMotorDatabase) self.assertIsInstance(self.db["_collection"], motor_asyncio.AsyncIOMotorCollection) self.assertIsInstance(self.collection["_collection"], motor_asyncio.AsyncIOMotorCollection) with self.assertRaises(AttributeError): self.cx._db with self.assertRaises(AttributeError): self.db._collection with self.assertRaises(AttributeError): self.collection._collection def test_abc(self): class C(ABC): db = self.db collection = self.collection subcollection = self.collection.subcollection # MOTOR-104, TypeError: Can't instantiate abstract class C with abstract # methods collection, db, subcollection. C() @asyncio_test async def test_inheritance(self): class CollectionSubclass(motor_asyncio.AsyncIOMotorCollection): pass class DatabaseSubclass(motor_asyncio.AsyncIOMotorDatabase): def __getitem__(self, name): return CollectionSubclass(self, name) class ClientSubclass(motor_asyncio.AsyncIOMotorClient): def __getitem__(self, name): return DatabaseSubclass(self, name) cx = ClientSubclass(test.env.uri, **self.get_client_kwargs()) self.assertIsInstance(cx, ClientSubclass) db = cx["testdb"] self.assertIsInstance(db, DatabaseSubclass) coll = db["testcoll"] self.assertIsInstance(coll, CollectionSubclass) self.assertIsNotNone(await coll.insert_one({})) class ExecutorForkTest(AsyncIOTestCase): @unittest.skipUnless(hasattr(os, "fork"), "This test requires fork") @asyncio_test() async def test_executor_reset(self): parent_conn, child_conn = Pipe() lock_pid = os.fork() if lock_pid == 0: # Child set_event_loop(None) self.loop = new_event_loop() client = self.asyncio_client() try: self.loop.run_until_complete(client.db.command("ping")) except Exception: child_conn.send(False) child_conn.send(True) os._exit(0) else: # Parent self.assertTrue(parent_conn.recv(), "Child process did not complete.") motor-3.4.0/test/asyncio_tests/test_asyncio_change_stream.py000066400000000000000000000243111460060525600244670ustar00rootroot00000000000000# Copyright 2017-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. """Test AsyncIOMotorChangeStream.""" import asyncio import copy import threading import time from test import SkipTest, env from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.utils import get_async_test_timeout, wait_until from pymongo.errors import InvalidOperation, OperationFailure from motor.frameworks.asyncio import max_workers class TestAsyncIOChangeStream(AsyncIOTestCase): @classmethod @env.require_version_min(3, 6) def setUpClass(cls): super().setUpClass() if env.is_standalone: raise SkipTest("Standalone") # Ensure the collection exists. env.sync_cx.motor_test.test_collection.delete_many({}) env.sync_cx.motor_test.test_collection.insert_one({"_id": 1}) def wait_and_insert(self, change_stream, n=1): # The start time of the change stream is nondeterministic. Wait # to ensure this insert comes after the change stream starts. def target(): start = time.time() timeout = get_async_test_timeout() while not change_stream.delegate: if time.time() - start > timeout: print("MotorChangeStream never created ChangeStream") return time.sleep(0.1) doclist = [{} for _ in range(n)] if isinstance(n, int) else n self.loop.call_soon_threadsafe(self.collection.insert_many, doclist) t = threading.Thread(target=target) t.daemon = True t.start() @asyncio_test async def test_async_for(self): change_stream = self.collection.watch() self.wait_and_insert(change_stream, 2) i = 0 async for _ in change_stream: i += 1 if i == 2: break self.assertEqual(i, 2) @asyncio_test async def test_async_for(self): change_stream = self.collection.watch() self.wait_and_insert(change_stream, 2) i = 0 async for _ in change_stream: i += 1 if i == 2: break self.assertEqual(i, 2) @asyncio_test async def test_async_try_next(self): change_stream = self.collection.watch() # No changes. doc = await change_stream.try_next() self.assertIsNone(doc) # Insert a change and ensure we see it via try_next. idoc = {"_id": 1, "data": "abc"} self.wait_and_insert(change_stream, [idoc]) while change_stream.alive: change_doc = await change_stream.try_next() if change_doc is not None: break self.assertEqual(change_doc["fullDocument"], idoc) @env.require_version_min(4, 0, 7) @asyncio_test async def test_async_try_next_updates_resume_token(self): change_stream = self.collection.watch([{"$match": {"fullDocument.a": 10}}]) # Get empty change, check non-empty resume token. _ = await change_stream.try_next() self.assertIsNotNone(change_stream.resume_token) # Insert some record that don't match the change stream filter. self.wait_and_insert(change_stream, [{"a": 19}, {"a": 20}]) # Ensure we see a new resume token even though we see no changes. initial_resume_token = copy.copy(change_stream.resume_token) async def token_change(): _ = await change_stream.try_next() return change_stream.resume_token != initial_resume_token await wait_until(token_change, "see a new resume token", timeout=get_async_test_timeout()) @asyncio_test async def test_watch(self): coll = self.collection with self.assertRaises(TypeError): # pipeline must be a list. async for _ in coll.watch(pipeline={}): pass change_stream = coll.watch() self.wait_and_insert(change_stream, 1) change = await change_stream.next() # New change stream with resume token. await coll.insert_one({"_id": 23}) change = await coll.watch(resume_after=change["_id"]).next() self.assertEqual(change["fullDocument"], {"_id": 23}) @env.require_version_min(4, 2) @asyncio_test async def test_watch_with_start_after(self): # Ensure collection exists before starting. await self.collection.insert_one({}) # Create change stream before invalidate event. change_stream = self.collection.watch([{"$match": {"operationType": "invalidate"}}]) _ = await change_stream.try_next() # Generate invalidate event and store corresponding resume token. await self.collection.drop() _ = await change_stream.next() # v5.1 requires an extra getMore after an invalidate event to exhaust # the cursor. self.assertIsNone(await change_stream.try_next()) self.assertFalse(change_stream.alive) resume_token = change_stream.resume_token # Recreate change stream and observe from invalidate event. doc = {"_id": "startAfterTest"} await self.collection.insert_one(doc) change_stream = self.collection.watch(start_after=resume_token) change = await change_stream.next() self.assertEqual(doc, change["fullDocument"]) @asyncio_test async def test_close(self): coll = self.collection change_stream = coll.watch() future = change_stream.next() self.wait_and_insert(change_stream, 1) await future await change_stream.close() with self.assertRaises(StopAsyncIteration): await change_stream.next() async for _ in change_stream: pass @asyncio_test async def test_missing_id(self): coll = self.collection change_stream = coll.watch([{"$project": {"_id": 0}}]) future = change_stream.next() self.wait_and_insert(change_stream) with self.assertRaises((InvalidOperation, OperationFailure)): await future # The cursor should now be closed. with self.assertRaises(StopAsyncIteration): await change_stream.next() @asyncio_test async def test_unknown_full_document(self): coll = self.collection change_stream = coll.watch(full_document="unknownFullDocOption") future = change_stream.next() self.wait_and_insert(change_stream, 1) with self.assertRaises(OperationFailure): await future @asyncio_test async def test_async_with(self): async with self.collection.watch() as change_stream: self.wait_and_insert(change_stream, 1) async for _ in change_stream: self.assertTrue(change_stream.delegate._cursor.alive) break self.assertFalse(change_stream.delegate._cursor.alive) @asyncio_test async def test_async_with_creates_cursor(self): coll = self.collection await coll.insert_one({"_id": 1}) async with coll.watch() as stream: self.assertEqual([{"_id": 1}], await coll.find().to_list(None)) await coll.insert_one({"_id": 2}) doc = await stream.next() self.assertEqual({"_id": 2}, doc["fullDocument"]) @asyncio_test async def test_with_statement(self): with self.assertRaises(RuntimeError): with self.collection.watch(): pass @env.require_version_min(4, 0) @asyncio_test async def test_client(self): change_stream = self.cx.watch() self.wait_and_insert(change_stream, 2) i = 0 async for _ in change_stream: i += 1 if i == 2: break await self.cx.other_db.other_collection.insert_one({}) async for _ in change_stream: i += 1 if i == 3: break @env.require_version_min(4, 0) @asyncio_test async def test_database(self): change_stream = self.db.watch() self.wait_and_insert(change_stream, 2) i = 0 async for _ in change_stream: i += 1 if i == 2: break await self.db.other_collection.insert_one({}) async for _ in change_stream: i += 1 if i == 3: break @asyncio_test async def test_watch_with_session(self): async with await self.cx.start_session() as session: # Pass MotorSession. async with self.collection.watch(session=session) as cs: self.wait_and_insert(cs, 1) _ = await cs.next() # Pass PyMongo session directly. async with self.collection.watch(session=session.delegate) as cs: self.wait_and_insert(cs, 1) _ = await cs.next() @asyncio_test(timeout=10) async def test_iterate_more_streams_than_workers(self): # Create more tasks running ChangeStream.next than there are worker # threads, and then ensure that other tasks can still run. streams = [self.collection.watch() for _ in range(max_workers)] tasks = [stream.next() for stream in streams] try: async def find_insert(): # Wait for all change streams to be created while not all(stream.delegate for stream in streams): await asyncio.sleep(0.1) await self.collection.find_one() await self.collection.insert_one({}) tasks.extend([find_insert() for _ in range(10)]) await asyncio.gather(*tasks) finally: # Ensure that the .next() tasks always unblock. self.collection.delegate.insert_one({}) for stream in streams: await stream.close() motor-3.4.0/test/asyncio_tests/test_asyncio_client.py000066400000000000000000000245671460060525600231620ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test AsyncIOMotorClient.""" import asyncio import os import unittest from unittest import SkipTest try: import contextvars except ImportError: contextvars = False import test from test.asyncio_tests import ( AsyncIOMockServerTestCase, AsyncIOTestCase, asyncio_test, remove_all_users, ) from test.test_environment import db_password, db_user, env from test.utils import get_primary_pool import pymongo from bson import CodecOptions from pymongo import ReadPreference, WriteConcern, monitoring from pymongo.errors import ConnectionFailure, OperationFailure import motor from motor import motor_asyncio class TestAsyncIOClient(AsyncIOTestCase): @asyncio_test async def test_client_lazy_connect(self): await self.db.test_client_lazy_connect.delete_many({}) # Create client without connecting; connect on demand. cx = self.asyncio_client() collection = cx.motor_test.test_client_lazy_connect future0 = collection.insert_one({"foo": "bar"}) future1 = collection.insert_one({"foo": "bar"}) await asyncio.gather(future0, future1) resp = await collection.count_documents({"foo": "bar"}) self.assertEqual(2, resp) cx.close() @asyncio_test async def test_close(self): cx = self.asyncio_client() cx.close() self.assertEqual(None, get_primary_pool(cx)) @asyncio_test async def test_unix_socket(self): if env.mongod_started_with_ssl: raise SkipTest("Server started with SSL") mongodb_socket = "/tmp/mongodb-%d.sock" % env.port if not os.access(mongodb_socket, os.R_OK): raise SkipTest("Socket file is not accessible") encoded_socket = "%2Ftmp%2Fmongodb-" + str(env.port) + ".sock" if test.env.auth: uri = "mongodb://%s:%s@%s" % (db_user, db_password, encoded_socket) else: uri = "mongodb://%s" % (encoded_socket,) client = self.asyncio_client(uri) collection = client.motor_test.test await collection.insert_one({"dummy": "object"}) # Confirm it fails with a missing socket. client = motor_asyncio.AsyncIOMotorClient( "mongodb://%2Ftmp%2Fnon-existent.sock", io_loop=self.loop, serverSelectionTimeoutMS=100 ) with self.assertRaises(ConnectionFailure): await client.admin.command("ismaster") client.close() def test_database_named_delegate(self): self.assertTrue(isinstance(self.cx.delegate, pymongo.mongo_client.MongoClient)) self.assertTrue(isinstance(self.cx["delegate"], motor_asyncio.AsyncIOMotorDatabase)) @asyncio_test async def test_reconnect_in_case_connection_closed_by_mongo(self): cx = self.asyncio_client(maxPoolSize=1, retryReads=False) await cx.admin.command("ping") # close motor_socket, we imitate that connection to mongo server # lost, as result we should have AutoReconnect instead of # IncompleteReadError pool = get_primary_pool(cx) conn = pool.conns.pop() conn.conn.close() pool.conns.appendleft(conn) with self.assertRaises(pymongo.errors.AutoReconnect): await cx.motor_test.test_collection.find_one() @asyncio_test async def test_connection_failure(self): # Assuming there isn't anything actually running on this port client = motor_asyncio.AsyncIOMotorClient( "localhost", 8765, serverSelectionTimeoutMS=10, io_loop=self.loop ) with self.assertRaises(ConnectionFailure): await client.admin.command("ismaster") @asyncio_test(timeout=30) async def test_connection_timeout(self): # Motor merely tries to time out a connection attempt within the # specified duration; DNS lookup in particular isn't charged against # the timeout. So don't measure how long this takes. client = motor_asyncio.AsyncIOMotorClient( "example.com", port=12345, serverSelectionTimeoutMS=1, io_loop=self.loop ) with self.assertRaises(ConnectionFailure): await client.admin.command("ismaster") @asyncio_test async def test_max_pool_size_validation(self): with self.assertRaises(ValueError): motor_asyncio.AsyncIOMotorClient(maxPoolSize=-1, io_loop=self.loop) with self.assertRaises(ValueError): motor_asyncio.AsyncIOMotorClient(maxPoolSize="foo", io_loop=self.loop) cx = self.asyncio_client(maxPoolSize=100) self.assertEqual(cx.options.pool_options.max_pool_size, 100) cx.close() @asyncio_test(timeout=30) async def test_drop_database(self): # Make sure we can pass an AsyncIOMotorDatabase instance # to drop_database db = self.cx.test_drop_database await db.test_collection.insert_one({}) names = await self.cx.list_database_names() self.assertTrue("test_drop_database" in names) await self.cx.drop_database(db) names = await self.cx.list_database_names() self.assertFalse("test_drop_database" in names) @asyncio_test async def test_auth_from_uri(self): if not test.env.auth: raise SkipTest("Authentication is not enabled on server") # self.db is logged in as root. await remove_all_users(self.db) db = self.db try: test.env.create_user(db.name, "mike", "password", roles=["userAdmin", "readWrite"]) client = self.asyncio_client("mongodb://u:pass@%s:%d" % (env.host, env.port)) with self.assertRaises(OperationFailure): await client.db.collection.find_one() client = self.asyncio_client( "mongodb://mike:password@%s:%d/%s" % (env.host, env.port, db.name) ) await client[db.name].collection.find_one() finally: test.env.drop_user(db.name, "mike") def test_get_database(self): codec_options = CodecOptions(tz_aware=True) write_concern = WriteConcern(w=2, j=True) db = self.cx.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) assert isinstance(db, motor_asyncio.AsyncIOMotorDatabase) self.assertEqual("foo", db.name) self.assertEqual(codec_options, db.codec_options) self.assertEqual(ReadPreference.SECONDARY, db.read_preference) self.assertEqual(write_concern, db.write_concern) @asyncio_test async def test_list_databases(self): await self.collection.insert_one({}) cursor = await self.cx.list_databases() self.assertIsInstance(cursor, motor_asyncio.AsyncIOMotorCommandCursor) while await cursor.fetch_next: info = cursor.next_object() if info["name"] == self.collection.database.name: break else: self.fail("'%s' database not found" % self.collection.database.name) @asyncio_test async def test_list_database_names(self): await self.collection.insert_one({}) names = await self.cx.list_database_names() self.assertIsInstance(names, list) self.assertIn(self.collection.database.name, names) @unittest.skipIf(not contextvars, "this test requires contextvars") @asyncio_test async def test_contextvars_support(self): var = contextvars.ContextVar("variable", default="default") class Listener(monitoring.CommandListener): def __init__(self): self.values = [] def save_contextvar_value(self): self.values.append(var.get()) def started(self, event): self.save_contextvar_value() def succeeded(self, event): self.save_contextvar_value() def failed(self, event): pass listener = Listener() client = self.asyncio_client(event_listeners=[listener]) coll = client[self.db.name].test await coll.insert_one({}) self.assertTrue(listener.values) for val in listener.values: self.assertEqual(val, "default") var.set("ContextVar value") listener.values.clear() await coll.insert_one({}) self.assertTrue(listener.values) for val in listener.values: self.assertEqual(val, "ContextVar value") class TestAsyncIOClientTimeout(AsyncIOMockServerTestCase): @asyncio_test async def test_timeout(self): server = self.server(auto_ismaster=True) client = motor_asyncio.AsyncIOMotorClient( server.uri, socketTimeoutMS=100, io_loop=self.loop ) with self.assertRaises(pymongo.errors.AutoReconnect) as context: await client.motor_test.test_collection.find_one() self.assertIn("timed out", str(context.exception)) client.close() class TestAsyncIOClientHandshake(AsyncIOMockServerTestCase): @asyncio_test async def test_handshake(self): server = self.server() client = motor_asyncio.AsyncIOMotorClient( server.uri, connectTimeoutMS=100, serverSelectionTimeoutMS=100 ) # Trigger connection. future = client.db.command("ping") ismaster = await self.run_thread(server.receives, "ismaster") meta = ismaster.doc["client"] self.assertEqual("PyMongo|Motor", meta["driver"]["name"]) # AsyncIOMotorClient adds nothing to platform. self.assertNotIn("Tornado", meta["platform"]) self.assertTrue( meta["driver"]["version"].endswith(motor.version), "Version in handshake [%s] doesn't end with Motor version [%s]" % (meta["driver"]["version"], motor.version), ) ismaster.hangs_up() server.stop() client.close() try: await future except Exception: pass if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_collection.py000066400000000000000000000314621460060525600240270ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test AsyncIOMotorCollection.""" import asyncio import sys import traceback import unittest from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.test_environment import env from test.utils import ignore_deprecations import pymongo from bson import CodecOptions from bson.binary import JAVA_LEGACY from pymongo import ReadPreference, WriteConcern from pymongo.encryption import Algorithm, QueryType from pymongo.errors import BulkWriteError, DuplicateKeyError, OperationFailure from pymongo.read_concern import ReadConcern from pymongo.read_preferences import Secondary from motor.motor_asyncio import AsyncIOMotorClientEncryption, AsyncIOMotorCollection if pymongo.version_tuple >= (4, 4, 0): from pymongo.encryption_options import RangeOpts class TestAsyncIOCollection(AsyncIOTestCase): @asyncio_test async def test_collection(self): # Test that we can create a collection directly, not just from # database accessors. collection = AsyncIOMotorCollection(self.db, "test_collection") # Make sure we got the right collection and it can do an operation self.assertEqual("test_collection", collection.name) await collection.delete_many({}) await collection.insert_one({"_id": 1}) doc = await collection.find_one({"_id": 1}) self.assertEqual(1, doc["_id"]) # If you pass kwargs to PyMongo's Collection(), it calls # db.create_collection(). Motor can't do I/O in a constructor # so this is prohibited. self.assertRaises( TypeError, AsyncIOMotorCollection, self.db, "test_collection", capped=True ) @asyncio_test async def test_dotted_collection_name(self): # Ensure that remove, insert, and find work on collections with dots # in their names. for coll in (self.db.foo.bar, self.db.foo.bar.baz): await coll.delete_many({}) result = await coll.insert_one({"_id": "xyzzy"}) self.assertEqual("xyzzy", result.inserted_id) result = await coll.find_one({"_id": "xyzzy"}) self.assertEqual(result["_id"], "xyzzy") await coll.delete_many({}) resp = await coll.find_one({"_id": "xyzzy"}) self.assertEqual(None, resp) def test_call(self): # Prevents user error with nice message. try: self.db.foo() except TypeError as e: self.assertTrue("no such method exists" in str(e)) else: self.fail("Expected TypeError") @ignore_deprecations @asyncio_test async def test_update(self): await self.collection.insert_one({"_id": 1}) result = await self.collection.update_one({"_id": 1}, {"$set": {"foo": "bar"}}) self.assertIsNone(result.upserted_id) self.assertEqual(1, result.modified_count) @ignore_deprecations @asyncio_test async def test_update_bad(self): # Violate a unique index, make sure we handle error well coll = self.db.unique_collection await coll.create_index("s", unique=True) try: await coll.insert_many([{"s": 1}, {"s": 2}]) with self.assertRaises(DuplicateKeyError): await coll.update_one({"s": 2}, {"$set": {"s": 1}}) finally: await coll.drop() @asyncio_test async def test_insert_one(self): collection = self.collection result = await collection.insert_one({"_id": 201}) self.assertEqual(201, result.inserted_id) @ignore_deprecations @asyncio_test async def test_insert_many_one_bad(self): collection = self.collection await collection.insert_one({"_id": 2}) # Violate a unique index in one of many updates, handle error. with self.assertRaises(BulkWriteError): await collection.insert_many([{"_id": 1}, {"_id": 2}, {"_id": 3}]) # Already exists # First insert should have succeeded, but not second or third. self.assertEqual(set([1, 2]), set(await collection.distinct("_id"))) @asyncio_test async def test_delete_one(self): # Remove a document twice, check that we get a success responses # and n = 0 for the second time. await self.collection.insert_one({"_id": 1}) result = await self.collection.delete_one({"_id": 1}) # First time we remove, n = 1 self.assertEqual(1, result.raw_result["n"]) self.assertEqual(1, result.raw_result["ok"]) self.assertEqual(None, result.raw_result.get("err")) result = await self.collection.delete_one({"_id": 1}) # Second time, document is already gone, n = 0 self.assertEqual(0, result.raw_result["n"]) self.assertEqual(1, result.raw_result["ok"]) self.assertEqual(None, result.raw_result.get("err")) @ignore_deprecations @asyncio_test async def test_unacknowledged_insert(self): coll = self.db.test_unacknowledged_insert await coll.with_options(write_concern=WriteConcern(0)).insert_one({"_id": 1}) # The insert is eventually executed. while not (await coll.count_documents({})): await asyncio.sleep(0.1) @ignore_deprecations @asyncio_test async def test_unacknowledged_update(self): coll = self.collection await coll.insert_one({"_id": 1}) await coll.with_options(write_concern=WriteConcern(0)).update_one( {"_id": 1}, {"$set": {"a": 1}} ) while not (await coll.find_one({"a": 1})): await asyncio.sleep(0.1) @ignore_deprecations @asyncio_test async def test_indexes(self): test_collection = self.collection # Create an index idx_name = await test_collection.create_index([("foo", 1)]) index_info = await test_collection.index_information() self.assertEqual([("foo", 1)], index_info[idx_name]["key"]) # Don't test drop_index or drop_indexes -- Synchro tests them async def _make_test_data(self, n): await self.db.drop_collection("test") await self.db.test.insert_many([{"_id": i} for i in range(n)]) expected_sum = sum(range(n)) return expected_sum pipeline = [{"$project": {"_id": "$_id"}}] @asyncio_test(timeout=30) async def test_aggregation_cursor(self): db = self.db # A small collection which returns only an initial batch, # and a larger one that requires a getMore. for collection_size in (10, 1000): expected_sum = await self._make_test_data(collection_size) cursor = db.test.aggregate(self.pipeline) docs = await cursor.to_list(collection_size) self.assertEqual(expected_sum, sum(doc["_id"] for doc in docs)) @asyncio_test async def test_aggregation_cursor_exc_info(self): await self._make_test_data(200) cursor = self.db.test.aggregate(self.pipeline) await cursor.to_list(length=10) await self.db.test.drop() try: await cursor.to_list(length=None) except OperationFailure: _, _, tb = sys.exc_info() # The call tree should include PyMongo code we ran on a thread. formatted = "\n".join(traceback.format_tb(tb)) self.assertTrue( "_unpack_response" in formatted or "_check_command_response" in formatted ) @asyncio_test async def test_aggregate_cursor_del(self): cursor = self.db.test.aggregate(self.pipeline) del cursor cursor = self.db.test.aggregate(self.pipeline) await cursor.close() del cursor def test_with_options(self): coll = self.db.test codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) write_concern = WriteConcern(w=2, j=True) coll2 = coll.with_options(codec_options, ReadPreference.SECONDARY, write_concern) self.assertTrue(isinstance(coll2, AsyncIOMotorCollection)) self.assertEqual(codec_options, coll2.codec_options) self.assertEqual(Secondary(), coll2.read_preference) self.assertEqual(write_concern, coll2.write_concern) pref = Secondary([{"dc": "sf"}]) coll2 = coll.with_options(read_preference=pref) self.assertEqual(pref, coll2.read_preference) self.assertEqual(coll.codec_options, coll2.codec_options) self.assertEqual(coll.write_concern, coll2.write_concern) def test_sub_collection(self): # Verify that a collection with a dotted name inherits options from its # parent collection. write_concern = WriteConcern(w=2, j=True) read_concern = ReadConcern("majority") read_preference = Secondary([{"dc": "sf"}]) codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) coll1 = self.db.get_collection( "test", write_concern=write_concern, read_concern=read_concern, read_preference=read_preference, codec_options=codec_options, ) coll2 = coll1.subcollection coll3 = coll1["subcollection"] for c in [coll1, coll2, coll3]: self.assertEqual(write_concern, c.write_concern) self.assertEqual(read_concern, c.read_concern) self.assertEqual(read_preference, c.read_preference) self.assertEqual(codec_options, c.codec_options) @env.require_version_min(7, 0, -1, -1) @env.require_no_standalone @asyncio_test async def test_async_create_encrypted_collection(self): c = self.collection KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}} self.cx.drop_database("db") async with AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", c, CodecOptions() ) as client_encryption: coll, ef = await client_encryption.create_encrypted_collection( database=self.db, name="testing1", encrypted_fields={"fields": [{"path": "ssn", "bsonType": "string", "keyId": None}]}, kms_provider="local", ) with self.assertRaises(pymongo.errors.WriteError) as exc: await coll.insert_one({"ssn": "123-45-6789"}) self.assertEqual(exc.exception.code, 121) await self.db.drop_collection("testing1", encrypted_fields=ef) @asyncio_test async def test_async_encrypt_expression(self): c = self.collection KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}} self.cx.drop_database("db") async with AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", c, CodecOptions() ) as client_encryption: data_key = await client_encryption.create_data_key( "local", key_alt_names=["pymongo_encryption_example_1"] ) name = "DoubleNoPrecision" range_opts = RangeOpts(sparsity=1) for i in [6.0, 30.0, 200.0]: insert_payload = await client_encryption.encrypt( float(i), key_id=data_key, algorithm=Algorithm.RANGEPREVIEW, contention_factor=0, range_opts=range_opts, ) self.collection.insert_one( { f"encrypted{name}": insert_payload, } ) self.assertEqual(await client_encryption.decrypt(insert_payload), i) find_payload = await client_encryption.encrypt_expression( expression={ "$and": [ {f"encrypted{name}": {"$gte": 6.0}}, {f"encrypted{name}": {"$lte": 200.0}}, ] }, key_id=data_key, algorithm=Algorithm.RANGEPREVIEW, query_type=QueryType.RANGEPREVIEW, contention_factor=0, range_opts=range_opts, ) sorted_find = sorted( await self.collection.explicit_encryption.find(find_payload).to_list(3), key=lambda x: x["_id"], ) for elem, expected in zip(sorted_find, [6.0, 30.0, 200.0]): self.assertEqual(elem[f"encrypted{name}"], expected) if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_cursor.py000066400000000000000000000625151460060525600232140ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test AsyncIOMotorCursor.""" import asyncio import sys import traceback import unittest import warnings from functools import partial from test.asyncio_tests import ( AsyncIOMockServerTestCase, AsyncIOTestCase, asyncio_test, get_command_line, server_is_mongos, ) from test.test_environment import env from test.utils import ( FailPoint, TestListener, get_async_test_timeout, get_primary_pool, one, safe_get, wait_until, ) from unittest import SkipTest import bson from pymongo import CursorType from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure from motor import motor_asyncio class TestAsyncIOCursor(AsyncIOMockServerTestCase): def test_cursor(self): cursor = self.collection.find() self.assertTrue(isinstance(cursor, motor_asyncio.AsyncIOMotorCursor)) self.assertFalse(cursor.started, "Cursor shouldn't start immediately") @asyncio_test async def test_count(self): await self.make_test_data() coll = self.collection self.assertEqual(100, (await coll.count_documents({"_id": {"$gt": 99}}))) @asyncio_test async def test_fetch_next(self): await self.make_test_data() coll = self.collection # 200 results, only including _id field, sorted by _id. cursor = coll.find({}, {"_id": 1}).sort("_id").batch_size(75) self.assertEqual(None, cursor.cursor_id) self.assertEqual(None, cursor.next_object()) # Haven't fetched yet. i = 0 while await cursor.fetch_next: self.assertEqual({"_id": i}, cursor.next_object()) i += 1 # With batch_size 75 and 200 results, cursor should be exhausted on # the server by third fetch. if i <= 150: self.assertNotEqual(0, cursor.cursor_id) else: self.assertEqual(0, cursor.cursor_id) self.assertEqual(False, (await cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) self.assertEqual(0, cursor.cursor_id) self.assertEqual(200, i) @unittest.skipIf("PyPy" in sys.version, "PyPy") @asyncio_test async def test_fetch_next_delete(self): client, server = self.client_server(auto_ismaster=True) cursor = client.test.coll.find() self.fetch_next(cursor) request = await self.run_thread(server.receives, "find", "coll") request.replies({"cursor": {"id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) # Decref the cursor and clear from the event loop. del cursor request = await self.run_thread(server.receives, "killCursors", "coll") request.ok() @asyncio_test async def test_fetch_next_without_results(self): coll = self.collection # Nothing matches this query. cursor = coll.find({"foo": "bar"}) self.assertEqual(None, cursor.next_object()) self.assertEqual(False, (await cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) # Now cursor knows it's exhausted. self.assertEqual(0, cursor.cursor_id) @asyncio_test async def test_fetch_next_is_idempotent(self): # Subsequent calls to fetch_next don't do anything await self.make_test_data() coll = self.collection cursor = coll.find() self.assertEqual(None, cursor.cursor_id) await cursor.fetch_next self.assertTrue(cursor.cursor_id) self.assertEqual(101, cursor._buffer_size()) await cursor.fetch_next # Does nothing self.assertEqual(101, cursor._buffer_size()) await cursor.close() @asyncio_test async def test_fetch_next_exception(self): coll = self.collection await coll.insert_many([{} for _ in range(10)]) cursor = coll.find(batch_size=2) await cursor.fetch_next self.assertTrue(cursor.next_object()) # Not valid on server, causes CursorNotFound. cursor.delegate._Cursor__id = bson.int64.Int64(1234) with self.assertRaises(OperationFailure): await cursor.fetch_next self.assertTrue(cursor.next_object()) await cursor.fetch_next self.assertTrue(cursor.next_object()) @asyncio_test(timeout=30) async def test_each(self): await self.make_test_data() cursor = self.collection.find({}, {"_id": 1}).sort("_id") future = self.loop.create_future() results = [] def callback(result, error): if error: raise error if result is not None: results.append(result) else: # Done iterating. future.set_result(True) cursor.each(callback) await future expected = [{"_id": i} for i in range(200)] self.assertEqual(expected, results) @asyncio_test async def test_to_list_argument_checking(self): # We need more than 10 documents so the cursor stays alive. await self.make_test_data() coll = self.collection cursor = coll.find() with self.assertRaises(ValueError): await cursor.to_list(-1) with self.assertRaises(TypeError): await cursor.to_list("foo") @asyncio_test async def test_to_list_with_length(self): await self.make_test_data() coll = self.collection cursor = coll.find().sort("_id") def expected(start, stop): return [{"_id": i} for i in range(start, stop)] self.assertEqual(expected(0, 10), (await cursor.to_list(10))) self.assertEqual(expected(10, 100), (await cursor.to_list(90))) # Test particularly rigorously around the 101-doc mark, since this is # where the first batch ends self.assertEqual(expected(100, 101), (await cursor.to_list(1))) self.assertEqual(expected(101, 102), (await cursor.to_list(1))) self.assertEqual(expected(102, 103), (await cursor.to_list(1))) self.assertEqual([], (await cursor.to_list(0))) self.assertEqual(expected(103, 105), (await cursor.to_list(2))) # Only 95 docs left, make sure length=100 doesn't error or hang self.assertEqual(expected(105, 200), (await cursor.to_list(100))) self.assertEqual(0, cursor.cursor_id) # Nothing left. self.assertEqual([], (await cursor.to_list(100))) await cursor.close() @asyncio_test async def test_to_list_multiple_getMores(self): await self.make_test_data() coll = self.collection cursor = coll.find(batch_size=5).sort("_id") def expected(start, stop): return [{"_id": i} for i in range(start, stop)] # 2 batches (find+getMore): self.assertEqual(expected(0, 10), (await cursor.to_list(10))) # 5 batches, stop in the middle of a batch: self.assertEqual(expected(10, 33), (await cursor.to_list(23))) # 33 batches: self.assertEqual(expected(33, 200), (await cursor.to_list(167))) # Nothing left. self.assertEqual([], (await cursor.to_list(100))) await cursor.close() @asyncio_test async def test_to_list_exc_info(self): await self.make_test_data() coll = self.collection cursor = coll.find() await cursor.to_list(length=10) await self.collection.drop() try: await cursor.to_list(length=None) except OperationFailure: _, _, tb = sys.exc_info() # The call tree should include PyMongo code we ran on a thread. formatted = "\n".join(traceback.format_tb(tb)) self.assertTrue( "_unpack_response" in formatted or "_check_command_response" in formatted ) async def _test_cancelled_error(self, coro): await self.make_test_data() # Cause an error on a getMore after the cursor.to_list task is # cancelled. fp = { "configureFailPoint": "failCommand", "data": {"failCommands": ["getMore"], "errorCode": 96}, "mode": {"times": 1}, } async with FailPoint(self.cx, fp): cleanup, task = coro(self.collection) task.cancel() with self.assertRaises(asyncio.CancelledError): await task await cleanup() # Yield for some time to allow pending Cursor callbacks to run. await asyncio.sleep(0.5) @env.require_version_min(4, 2) # failCommand @asyncio_test async def test_cancelled_error_to_list(self): # Note: We intentionally don't use "async def" here to avoid wrapping # the returned to_list Future in a coroutine. def to_list(collection): cursor = collection.find(batch_size=2) return cursor.close, cursor.to_list(None) await self._test_cancelled_error(to_list) @env.require_version_min(4, 2) # failCommand @asyncio_test async def test_cancelled_error_fetch_next(self): def fetch_next(collection): cursor = collection.find(batch_size=2) return cursor.close, cursor.fetch_next await self._test_cancelled_error(fetch_next) @env.require_version_min(4, 2) # failCommand @asyncio_test async def test_cancelled_error_fetch_next_aggregate(self): def fetch_next(collection): cursor = collection.aggregate([], batchSize=2) return cursor.close, cursor.fetch_next await self._test_cancelled_error(fetch_next) @asyncio_test async def test_to_list_with_length_of_none(self): await self.make_test_data() collection = self.collection cursor = collection.find() docs = await cursor.to_list(None) # Unlimited. count = await collection.count_documents({}) self.assertEqual(count, len(docs)) @asyncio_test async def test_to_list_tailable(self): coll = self.collection cursor = coll.find(cursor_type=CursorType.TAILABLE) # Can't call to_list on tailable cursor. with self.assertRaises(InvalidOperation): await cursor.to_list(10) @asyncio_test async def test_cursor_explicit_close(self): client, server = self.client_server(auto_ismaster=True) collection = client.test.coll cursor = collection.find() future = self.fetch_next(cursor) self.assertTrue(cursor.alive) request = await self.run_thread(server.receives, "find", "coll") request.replies({"cursor": {"id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) self.assertTrue(await future) self.assertEqual(123, cursor.cursor_id) future = asyncio.ensure_future(cursor.close()) # No reply to OP_KILLCURSORS. request = await self.run_thread(server.receives, "killCursors", "coll") request.ok() await future # Cursor reports it's alive because it has buffered data, even though # it's killed on the server. self.assertTrue(cursor.alive) self.assertEqual({"_id": 1}, cursor.next_object()) self.assertFalse(await cursor.fetch_next) self.assertFalse(cursor.alive) @asyncio_test async def test_each_cancel(self): await self.make_test_data() loop = self.loop collection = self.collection results = [] future = self.loop.create_future() def cancel(result, error): if error: future.set_exception(error) else: results.append(result) loop.call_soon(canceled) return False # Cancel iteration. def canceled(): try: self.assertFalse(cursor.delegate._Cursor__killed) self.assertTrue(cursor.alive) # Resume iteration cursor.each(each) except Exception as e: future.set_exception(e) def each(result, error): if error: future.set_exception(error) elif result: results.append(result) else: # Complete future.set_result(None) cursor = collection.find() cursor.each(cancel) await future self.assertEqual((await collection.count_documents({})), len(results)) @asyncio_test async def test_rewind(self): await self.collection.insert_many([{}, {}, {}]) cursor = self.collection.find().limit(2) count = 0 while await cursor.fetch_next: cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while await cursor.fetch_next: cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while await cursor.fetch_next: cursor.next_object() break cursor.rewind() while await cursor.fetch_next: cursor.next_object() count += 1 self.assertEqual(2, count) self.assertEqual(cursor, cursor.rewind()) @unittest.skipIf("PyPy" in sys.version, "PyPy") @asyncio_test async def test_cursor_del(self): client, server = self.client_server(auto_ismaster=True) cursor = client.test.coll.find() future = self.fetch_next(cursor) request = await self.run_thread(server.receives, "find", "coll") request.replies({"cursor": {"id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) await future # Complete the first fetch. # Dereference the cursor. del cursor # Let the event loop iterate once more to clear its references to # callbacks, allowing the cursor to be freed. await asyncio.sleep(0) request = await self.run_thread(server.receives, "killCursors", "coll") request.ok() @asyncio_test async def test_exhaust(self): if await server_is_mongos(self.cx): self.assertRaises(InvalidOperation, self.db.test.find, cursor_type=CursorType.EXHAUST) return self.assertRaises(ValueError, self.db.test.find, cursor_type=5) cur = self.db.test.find(cursor_type=CursorType.EXHAUST) self.assertRaises(InvalidOperation, cur.limit, 5) cur = self.db.test.find(limit=5) self.assertRaises(InvalidOperation, cur.add_option, 64) cur = self.db.test.find() cur.add_option(64) self.assertRaises(InvalidOperation, cur.limit, 5) await self.db.drop_collection("test") # Insert enough documents to require more than one batch. await self.db.test.insert_many([{} for _ in range(150)]) client = self.asyncio_client(maxPoolSize=1) # Ensure a pool. await client.db.collection.find_one() pool = get_primary_pool(client) conns = pool.conns # Make sure the socket is returned after exhaustion. cur = client[self.db.name].test.find(cursor_type=CursorType.EXHAUST) has_next = await cur.fetch_next self.assertTrue(has_next) self.assertEqual(0, len(conns)) while await cur.fetch_next: cur.next_object() self.assertEqual(1, len(conns)) # Same as previous but with to_list instead of next_object. docs = await client[self.db.name].test.find(cursor_type=CursorType.EXHAUST).to_list(None) self.assertEqual(1, len(conns)) self.assertEqual((await self.db.test.count_documents({})), len(docs)) # If the Cursor instance is discarded before being # completely iterated we have to close and # discard the socket. conn = one(conns) cur = client[self.db.name].test.find(cursor_type=CursorType.EXHAUST).batch_size(1) await cur.fetch_next self.assertTrue(cur.next_object()) # Run at least one getMore to initiate the OP_MSG exhaust protocol. if env.version.at_least(4, 2): await cur.fetch_next self.assertTrue(cur.next_object()) self.assertEqual(0, len(conns)) if "PyPy" in sys.version: # Don't wait for GC or use gc.collect(), it's unreliable. await cur.close() del cur async def conn_closed(): return conn not in conns and conn.closed await wait_until( conn_closed, "close exhaust cursor socket", timeout=get_async_test_timeout() ) # The exhaust cursor's socket was discarded, although another may # already have been opened to send OP_KILLCURSORS. self.assertNotIn(conn, conns) self.assertTrue(conn.closed) @asyncio_test async def test_close_with_docs_in_batch(self): # MOTOR-67 Killed cursor with docs batched is "alive", don't kill again. await self.make_test_data() # Ensure multiple batches. cursor = self.collection.find() await cursor.fetch_next await cursor.close() # Killed but still "alive": has a batch. self.cx.close() with warnings.catch_warnings(record=True) as w: del cursor # No-op, no error. self.assertEqual(0, len(w)) @asyncio_test async def test_aggregate_batch_size(self): listener = TestListener() cx = self.asyncio_client(event_listeners=[listener]) c = cx.motor_test.collection await c.delete_many({}) await c.insert_many({"_id": i} for i in range(3)) # Two ways of setting batchSize. cursor0 = c.aggregate([{"$sort": {"_id": 1}}]).batch_size(2) cursor1 = c.aggregate([{"$sort": {"_id": 1}}], batchSize=2) for cursor in cursor0, cursor1: lst = [] while await cursor.fetch_next: lst.append(cursor.next_object()) self.assertEqual(lst, [{"_id": 0}, {"_id": 1}, {"_id": 2}]) aggregate = listener.first_command_started("aggregate") self.assertEqual(aggregate.command["cursor"]["batchSize"], 2) getMore = listener.first_command_started("getMore") self.assertEqual(getMore.command["batchSize"], 2) @asyncio_test async def test_raw_batches(self): c = self.collection await c.delete_many({}) await c.insert_many({"_id": i} for i in range(4)) find = partial(c.find_raw_batches, {}) agg = partial(c.aggregate_raw_batches, [{"$sort": {"_id": 1}}]) for method in find, agg: cursor = method().batch_size(2) await cursor.fetch_next batch = cursor.next_object() self.assertEqual([{"_id": 0}, {"_id": 1}], bson.decode_all(batch)) lst = await method().batch_size(2).to_list(length=1) self.assertEqual([{"_id": 0}, {"_id": 1}], bson.decode_all(lst[0])) @asyncio_test async def test_context_manager(self): coll = self.collection await coll.insert_many({"_id": i} for i in range(10)) find = partial(coll.find, {}) agg = partial(coll.aggregate, [{"$sort": {"_id": 1}}]) find_raw_batches = partial(coll.find_raw_batches, {}) agg_raw_batches = partial(coll.aggregate_raw_batches, [{"$sort": {"_id": 1}}]) for method in find, agg, find_raw_batches, agg_raw_batches: contrast_cursor = method().batch_size(2) async with method().batch_size(2) as cursor: self.assertFalse(cursor.started, "Cursor shouldn't start immediately") with self.assertWarns(DeprecationWarning): await cursor.fetch_next record = cursor.next_object() self.assertEqual( {"_id": 0}, bson.decode_all(record)[0] if type(record) is bytes else record ) self.assertTrue(cursor.started) self.assertFalse(cursor.closed) self.assertFalse(contrast_cursor.closed) self.assertTrue(cursor.closed) await contrast_cursor.close() self.assertTrue(contrast_cursor.closed) @asyncio_test async def test_generate_keys(self): c = self.collection KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}} async with motor_asyncio.AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", c, bson.codec_options.CodecOptions() ) as client_encryption: self.assertIsInstance( await client_encryption.get_keys(), motor_asyncio.AsyncIOMotorCursor ) class TestAsyncIOCursorMaxTimeMS(AsyncIOTestCase): def setUp(self): super().setUp() self.loop.run_until_complete(self.maybe_skip()) def tearDown(self): self.loop.run_until_complete(self.disable_timeout()) super().tearDown() async def maybe_skip(self): if await server_is_mongos(self.cx): raise SkipTest("mongos has no maxTimeAlwaysTimeOut fail point") cmdline = await get_command_line(self.cx) if "1" != safe_get(cmdline, "parsed.setParameter.enableTestCommands"): if "enableTestCommands=1" not in cmdline["argv"]: raise SkipTest("testing maxTimeMS requires failpoints") async def enable_timeout(self): await self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="alwaysOn") async def disable_timeout(self): await self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") @asyncio_test async def test_max_time_ms_query(self): # Cursor parses server timeout error in response to initial query. await self.enable_timeout() cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): await cursor.fetch_next cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): await cursor.to_list(10) with self.assertRaises(ExecutionTimeout): await self.collection.find_one(max_time_ms=100000) @asyncio_test(timeout=60) async def test_max_time_ms_getmore(self): # Cursor handles server timeout during getmore, also. await self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) await cursor.fetch_next cursor.next_object() # Test getmore timeout. await self.enable_timeout() with self.assertRaises(ExecutionTimeout): while await cursor.fetch_next: cursor.next_object() await cursor.close() # Send another initial query. await self.disable_timeout() cursor = self.collection.find().max_time_ms(100000) await cursor.fetch_next cursor.next_object() # Test getmore timeout. await self.enable_timeout() with self.assertRaises(ExecutionTimeout): await cursor.to_list(None) # Avoid 'IOLoop is closing' warning. await cursor.close() finally: # Cleanup. await self.disable_timeout() await self.collection.delete_many({}) @asyncio_test async def test_max_time_ms_each_query(self): # Cursor.each() handles server timeout during initial query. await self.enable_timeout() cursor = self.collection.find().max_time_ms(100000) future = self.loop.create_future() def callback(result, error): if error: future.set_exception(error) elif not result: # Done. future.set_result(None) with self.assertRaises(ExecutionTimeout): cursor.each(callback) await future @asyncio_test(timeout=30) async def test_max_time_ms_each_getmore(self): # Cursor.each() handles server timeout during getmore. await self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) await cursor.fetch_next cursor.next_object() future = self.loop.create_future() def callback(result, error): if error: future.set_exception(error) elif not result: # Done. future.set_result(None) await self.enable_timeout() with self.assertRaises(ExecutionTimeout): cursor.each(callback) await future await cursor.close() finally: # Cleanup. await self.disable_timeout() await self.collection.delete_many({}) def test_iter(self): # Iteration should be prohibited. with self.assertRaises(TypeError): for _ in self.db.test.find(): pass if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_database.py000066400000000000000000000150431460060525600234350ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test AsyncIOMotorDatabase.""" import unittest from test import env from test.asyncio_tests import AsyncIOTestCase, asyncio_test import pymongo.database from bson import CodecOptions from bson.binary import JAVA_LEGACY from pymongo import ReadPreference, WriteConcern from pymongo.errors import CollectionInvalid, OperationFailure from pymongo.read_preferences import Secondary from motor.motor_asyncio import AsyncIOMotorCollection, AsyncIOMotorDatabase class TestAsyncIODatabase(AsyncIOTestCase): @asyncio_test async def test_database(self): # Test that we can create a db directly, not just get on from # AsyncIOMotorClient. db = AsyncIOMotorDatabase(self.cx, "motor_test") # Make sure we got the right DB and it can do an operation. self.assertEqual("motor_test", db.name) await db.test_collection.delete_many({}) await db.test_collection.insert_one({"_id": 1}) doc = await db.test_collection.find_one({"_id": 1}) self.assertEqual(1, doc["_id"]) def test_collection_named_delegate(self): db = self.db self.assertTrue(isinstance(db.delegate, pymongo.database.Database)) self.assertTrue(isinstance(db["delegate"], AsyncIOMotorCollection)) db.client.close() def test_call(self): # Prevents user error with nice message. try: self.cx.foo() except TypeError as e: self.assertTrue("no such method exists" in str(e)) else: self.fail("Expected TypeError") @env.require_version_min(3, 6) @asyncio_test async def test_aggregate(self): pipeline = [ {"$listLocalSessions": {}}, {"$limit": 1}, {"$addFields": {"dummy": "dummy field"}}, {"$project": {"_id": 0, "dummy": 1}}, ] expected = [{"dummy": "dummy field"}] cursor = self.cx.admin.aggregate(pipeline) docs = await cursor.to_list(10) self.assertEqual(expected, docs) @asyncio_test async def test_command(self): result = await self.cx.admin.command("buildinfo") # Make sure we got some sane result or other. self.assertEqual(1, result["ok"]) @asyncio_test async def test_create_collection(self): # Test creating collection, return val is wrapped in # AsyncIOMotorCollection, creating it again raises CollectionInvalid. db = self.db await db.drop_collection("test_collection2") collection = await db.create_collection("test_collection2") self.assertTrue(isinstance(collection, AsyncIOMotorCollection)) self.assertTrue("test_collection2" in (await db.list_collection_names())) with self.assertRaises(CollectionInvalid): await db.create_collection("test_collection2") @asyncio_test async def test_drop_collection(self): # Make sure we can pass an AsyncIOMotorCollection instance to # drop_collection. db = self.db collection = db.test_drop_collection await collection.insert_one({}) names = await db.list_collection_names() self.assertTrue("test_drop_collection" in names) await db.drop_collection(collection) names = await db.list_collection_names() self.assertFalse("test_drop_collection" in names) @asyncio_test async def test_validate_collection(self): db = self.db with self.assertRaises(TypeError): await db.validate_collection(5) with self.assertRaises(TypeError): await db.validate_collection(None) with self.assertRaises(OperationFailure): await db.validate_collection("test.doesnotexist") with self.assertRaises(OperationFailure): await db.validate_collection(db.test.doesnotexist) await db.test.insert_one({"dummy": "object"}) self.assertTrue(await db.validate_collection("test")) self.assertTrue(await db.validate_collection(db.test)) def test_get_collection(self): codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) write_concern = WriteConcern(w=2, j=True) coll = self.db.get_collection("foo", codec_options, ReadPreference.SECONDARY, write_concern) self.assertTrue(isinstance(coll, AsyncIOMotorCollection)) self.assertEqual("foo", coll.name) self.assertEqual(codec_options, coll.codec_options) self.assertEqual(ReadPreference.SECONDARY, coll.read_preference) self.assertEqual(write_concern, coll.write_concern) pref = Secondary([{"dc": "sf"}]) coll = self.db.get_collection("foo", read_preference=pref) self.assertEqual(pref, coll.read_preference) self.assertEqual(self.db.codec_options, coll.codec_options) self.assertEqual(self.db.write_concern, coll.write_concern) def test_with_options(self): db = self.db codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) write_concern = WriteConcern(w=2, j=True) db2 = db.with_options(codec_options, ReadPreference.SECONDARY, write_concern) self.assertTrue(isinstance(db2, AsyncIOMotorDatabase)) self.assertEqual(codec_options, db2.codec_options) self.assertEqual(Secondary(), db2.read_preference) self.assertEqual(write_concern, db2.write_concern) pref = Secondary([{"dc": "sf"}]) db2 = db.with_options(read_preference=pref) self.assertEqual(pref, db2.read_preference) self.assertEqual(db.codec_options, db2.codec_options) self.assertEqual(db.write_concern, db2.write_concern) @asyncio_test async def test_cursor_command(self): db = self.db await db.test.drop() docs = [{"_id": i, "doc": i} for i in range(3)] await db.test.insert_many(docs) cursor = await db.cursor_command("find", "test") for i in range(3): item = await cursor.try_next() self.assertEqual(item, docs[i]) if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_encryption.py000066400000000000000000000175471460060525600240760ustar00rootroot00000000000000# Copyright 2021-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. """Test Explicit Encryption with AsyncIOMotorClient.""" import unittest import uuid from test import env from test.asyncio_tests import AsyncIOTestCase, asyncio_test from bson.binary import JAVA_LEGACY, STANDARD, UUID_SUBTYPE, Binary from bson.codec_options import CodecOptions from bson.errors import BSONError from pymongo.encryption import Algorithm from pymongo.errors import InvalidOperation from motor.motor_asyncio import AsyncIOMotorClientEncryption KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}} OPTS = CodecOptions(uuid_representation=STANDARD) class TestExplicitSimple(AsyncIOTestCase): @env.require_csfle def setUp(self): super().setUp() def assertEncrypted(self, val): self.assertIsInstance(val, Binary) self.assertEqual(val.subtype, 6) def assertBinaryUUID(self, val): self.assertIsInstance(val, Binary) self.assertEqual(val.subtype, UUID_SUBTYPE) @asyncio_test async def test_encrypt_decrypt(self): client = self.asyncio_client() client_encryption = AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", client, OPTS ) # Use standard UUID representation. key_vault = client.keyvault.get_collection("datakeys", codec_options=OPTS) # Create the encrypted field's data key. key_id = await client_encryption.create_data_key("local", key_alt_names=["name"]) self.assertBinaryUUID(key_id) self.assertTrue(await key_vault.find_one({"_id": key_id})) # Create an unused data key to make sure filtering works. unused_key_id = await client_encryption.create_data_key("local", key_alt_names=["unused"]) self.assertBinaryUUID(unused_key_id) self.assertTrue(await key_vault.find_one({"_id": unused_key_id})) doc = {"_id": 0, "ssn": "000"} encrypted_ssn = await client_encryption.encrypt( doc["ssn"], Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_id=key_id ) # Ensure encryption via key_alt_name for the same key produces the # same output. encrypted_ssn2 = await client_encryption.encrypt( doc["ssn"], Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_alt_name="name" ) self.assertEqual(encrypted_ssn, encrypted_ssn2) # Test decryption. decrypted_ssn = await client_encryption.decrypt(encrypted_ssn) self.assertEqual(decrypted_ssn, doc["ssn"]) await key_vault.drop() await client_encryption.close() @asyncio_test async def test_validation(self): client = self.asyncio_client() client_encryption = AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", client, OPTS ) msg = "value to decrypt must be a bson.binary.Binary with subtype 6" with self.assertRaisesRegex(TypeError, msg): await client_encryption.decrypt("str") with self.assertRaisesRegex(TypeError, msg): await client_encryption.decrypt(Binary(b"123")) msg = "key_id must be a bson.binary.Binary with subtype 4" algo = Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic with self.assertRaisesRegex(TypeError, msg): await client_encryption.encrypt("str", algo, key_id="str") with self.assertRaisesRegex(TypeError, msg): await client_encryption.encrypt("str", algo, key_id=Binary(b"123")) await client_encryption.close() @asyncio_test async def test_bson_errors(self): client = self.asyncio_client() client_encryption = AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", client, OPTS ) # Attempt to encrypt an unencodable object. unencodable_value = object() with self.assertRaises(BSONError): await client_encryption.encrypt( unencodable_value, Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_id=Binary(uuid.uuid4().bytes, UUID_SUBTYPE), ) await client_encryption.close() @asyncio_test async def test_codec_options(self): client = self.asyncio_client() with self.assertRaisesRegex(TypeError, "codec_options must be"): AsyncIOMotorClientEncryption(KMS_PROVIDERS, "keyvault.datakeys", client, None) opts = CodecOptions(uuid_representation=JAVA_LEGACY) client_encryption_legacy = AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", client, opts ) # Create the encrypted field's data key. key_id = await client_encryption_legacy.create_data_key("local") # Encrypt a UUID with JAVA_LEGACY codec options. value = uuid.uuid4() encrypted_legacy = await client_encryption_legacy.encrypt( value, Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_id=key_id ) decrypted_value_legacy = await client_encryption_legacy.decrypt(encrypted_legacy) self.assertEqual(decrypted_value_legacy, value) # Encrypt the same UUID with STANDARD codec options. client_encryption = AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", client, OPTS ) encrypted_standard = await client_encryption.encrypt( value, Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_id=key_id ) decrypted_standard = await client_encryption.decrypt(encrypted_standard) self.assertEqual(decrypted_standard, value) # Test that codec_options is applied during encryption. self.assertNotEqual(encrypted_standard, encrypted_legacy) # Test that codec_options is applied during decryption. self.assertEqual( await client_encryption_legacy.decrypt(encrypted_standard), Binary.from_uuid(value, uuid_representation=STANDARD), ) self.assertNotEqual(await client_encryption.decrypt(encrypted_legacy), value) await client_encryption_legacy.close() await client_encryption.close() @asyncio_test async def test_close(self): client = self.asyncio_client() client_encryption = AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", client, OPTS ) await client_encryption.close() # Close can be called multiple times. await client_encryption.close() algo = Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic msg = "Cannot use closed ClientEncryption" with self.assertRaisesRegex(InvalidOperation, msg): await client_encryption.create_data_key("local") with self.assertRaisesRegex(InvalidOperation, msg): await client_encryption.encrypt("val", algo, key_alt_name="name") with self.assertRaisesRegex(InvalidOperation, msg): await client_encryption.decrypt(Binary(b"", 6)) @asyncio_test async def test_with_statement(self): client = self.asyncio_client() async with AsyncIOMotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", client, OPTS ) as client_encryption: pass with self.assertRaisesRegex(InvalidOperation, "Cannot use closed ClientEncryption"): await client_encryption.create_data_key("local") if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_grid_file.py000066400000000000000000000312711460060525600236160ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test GridFS with Motor, an asynchronous driver for MongoDB and Tornado.""" import datetime import sys import traceback import unittest from test import MockRequestHandler from test.asyncio_tests import AsyncIOTestCase, asyncio_test from bson.objectid import ObjectId from gridfs.errors import NoFile from pymongo.errors import InvalidOperation from motor import motor_asyncio class MotorGridFileTest(AsyncIOTestCase): async def _reset(self): await self.db.drop_collection("fs.files") await self.db.drop_collection("fs.chunks") await self.db.drop_collection("alt.files") await self.db.drop_collection("alt.chunks") def tearDown(self): self.loop.run_until_complete(self._reset()) super().tearDown() @asyncio_test async def test_attributes(self): f = motor_asyncio.AsyncIOMotorGridIn( self.db.fs, filename="test", foo="bar", content_type="text" ) await f.close() g = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, f._id) attr_names = ( "_id", "filename", "name", "name", "content_type", "length", "chunk_size", "upload_date", "aliases", "metadata", ) for attr_name in attr_names: self.assertRaises(InvalidOperation, getattr, g, attr_name) await g.open() for attr_name in attr_names: getattr(g, attr_name) @asyncio_test async def test_iteration(self): fs = motor_asyncio.AsyncIOMotorGridFSBucket(self.db) _id = await fs.upload_from_stream("filename", b"foo") g = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, _id) # Iteration is prohibited. self.assertRaises(TypeError, iter, g) @asyncio_test async def test_basic(self): f = motor_asyncio.AsyncIOMotorGridIn(self.db.fs, filename="test") await f.write(b"hello world") await f.close() self.assertEqual(1, (await self.db.fs.files.count_documents({}))) self.assertEqual(1, (await self.db.fs.chunks.count_documents({}))) g = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, f._id) self.assertEqual(b"hello world", (await g.read())) f = motor_asyncio.AsyncIOMotorGridIn(self.db.fs, filename="test") await f.close() self.assertEqual(2, (await self.db.fs.files.count_documents({}))) self.assertEqual(1, (await self.db.fs.chunks.count_documents({}))) g = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, f._id) self.assertEqual(b"", (await g.read())) @asyncio_test async def test_readchunk(self): in_data = b"a" * 10 f = motor_asyncio.AsyncIOMotorGridIn(self.db.fs, chunkSize=3) await f.write(in_data) await f.close() g = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, f._id) # This is starting to look like Lisp. self.assertEqual(3, len(await g.readchunk())) self.assertEqual(2, len(await g.read(2))) self.assertEqual(1, len(await g.readchunk())) self.assertEqual(3, len(await g.read(3))) self.assertEqual(1, len(await g.readchunk())) self.assertEqual(0, len(await g.readchunk())) @asyncio_test async def test_gridout_open_exc_info(self): g = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, "_id that doesn't exist") try: await g.open() except NoFile: _, _, tb = sys.exc_info() # The call tree should include PyMongo code we ran on a thread. formatted = "\n".join(traceback.format_tb(tb)) self.assertTrue("_ensure_file" in formatted) @asyncio_test async def test_alternate_collection(self): await self.db.alt.files.delete_many({}) await self.db.alt.chunks.delete_many({}) f = motor_asyncio.AsyncIOMotorGridIn(self.db.alt) await f.write(b"hello world") await f.close() self.assertEqual(1, (await self.db.alt.files.count_documents({}))) self.assertEqual(1, (await self.db.alt.chunks.count_documents({}))) g = motor_asyncio.AsyncIOMotorGridOut(self.db.alt, f._id) self.assertEqual(b"hello world", (await g.read())) @asyncio_test async def test_grid_in_default_opts(self): self.assertRaises(TypeError, motor_asyncio.AsyncIOMotorGridIn, "foo") a = motor_asyncio.AsyncIOMotorGridIn(self.db.fs) self.assertTrue(isinstance(a._id, ObjectId)) self.assertRaises(AttributeError, setattr, a, "_id", 5) self.assertEqual(None, a.filename) # This raises AttributeError because you can't directly set properties # in Motor, have to use set() def setter(): a.filename = "my_file" self.assertRaises(AttributeError, setter) # This method of setting attributes works in Motor await a.set("filename", "my_file") self.assertEqual("my_file", a.filename) self.assertEqual(None, a.content_type) await a.set("content_type", "text/html") self.assertEqual("text/html", a.content_type) self.assertRaises(AttributeError, getattr, a, "length") self.assertRaises(AttributeError, setattr, a, "length", 5) self.assertEqual(255 * 1024, a.chunk_size) self.assertRaises(AttributeError, setattr, a, "chunk_size", 5) self.assertRaises(AttributeError, getattr, a, "upload_date") self.assertRaises(AttributeError, setattr, a, "upload_date", 5) self.assertRaises(AttributeError, getattr, a, "aliases") await a.set("aliases", ["foo"]) self.assertEqual(["foo"], a.aliases) self.assertRaises(AttributeError, getattr, a, "metadata") await a.set("metadata", {"foo": 1}) self.assertEqual({"foo": 1}, a.metadata) await a.close() self.assertTrue(isinstance(a._id, ObjectId)) self.assertRaises(AttributeError, setattr, a, "_id", 5) self.assertEqual("my_file", a.filename) self.assertEqual("text/html", a.content_type) self.assertEqual(0, a.length) self.assertRaises(AttributeError, setattr, a, "length", 5) self.assertEqual(255 * 1024, a.chunk_size) self.assertRaises(AttributeError, setattr, a, "chunk_size", 5) self.assertTrue(isinstance(a.upload_date, datetime.datetime)) self.assertRaises(AttributeError, setattr, a, "upload_date", 5) self.assertEqual(["foo"], a.aliases) self.assertEqual({"foo": 1}, a.metadata) @asyncio_test async def test_grid_in_custom_opts(self): self.assertRaises(TypeError, motor_asyncio.AsyncIOMotorGridIn, "foo") a = motor_asyncio.AsyncIOMotorGridIn( self.db.fs, _id=5, filename="my_file", contentType="text/html", chunkSize=1000, aliases=["foo"], metadata={"foo": 1, "bar": 2}, bar=3, baz="hello", ) self.assertEqual(5, a._id) self.assertEqual("my_file", a.filename) self.assertEqual("text/html", a.content_type) self.assertEqual(1000, a.chunk_size) self.assertEqual(["foo"], a.aliases) self.assertEqual({"foo": 1, "bar": 2}, a.metadata) self.assertEqual(3, a.bar) self.assertEqual("hello", a.baz) self.assertRaises(AttributeError, getattr, a, "mike") b = motor_asyncio.AsyncIOMotorGridIn( self.db.fs, content_type="text/html", chunk_size=1000, baz=100 ) self.assertEqual("text/html", b.content_type) self.assertEqual(1000, b.chunk_size) self.assertEqual(100, b.baz) @asyncio_test async def test_grid_out_default_opts(self): self.assertRaises(TypeError, motor_asyncio.AsyncIOMotorGridOut, "foo") gout = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, 5) with self.assertRaises(NoFile): await gout.open() a = motor_asyncio.AsyncIOMotorGridIn(self.db.fs) await a.close() b = await motor_asyncio.AsyncIOMotorGridOut(self.db.fs, a._id).open() self.assertEqual(a._id, b._id) self.assertEqual(0, b.length) self.assertEqual(None, b.content_type) self.assertEqual(255 * 1024, b.chunk_size) self.assertTrue(isinstance(b.upload_date, datetime.datetime)) self.assertEqual(None, b.aliases) self.assertEqual(None, b.metadata) @asyncio_test async def test_grid_out_custom_opts(self): one = motor_asyncio.AsyncIOMotorGridIn( self.db.fs, _id=5, filename="my_file", contentType="text/html", chunkSize=1000, aliases=["foo"], metadata={"foo": 1, "bar": 2}, bar=3, baz="hello", ) await one.write(b"hello world") await one.close() two = await motor_asyncio.AsyncIOMotorGridOut(self.db.fs, 5).open() self.assertEqual(5, two._id) self.assertEqual(11, two.length) self.assertEqual("text/html", two.content_type) self.assertEqual(1000, two.chunk_size) self.assertTrue(isinstance(two.upload_date, datetime.datetime)) self.assertEqual(["foo"], two.aliases) self.assertEqual({"foo": 1, "bar": 2}, two.metadata) self.assertEqual(3, two.bar) @asyncio_test async def test_grid_out_file_document(self): one = motor_asyncio.AsyncIOMotorGridIn(self.db.fs) await one.write(b"foo bar") await one.close() file_document = await self.db.fs.files.find_one() two = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, file_document=file_document) self.assertEqual(b"foo bar", (await two.read())) file_document = await self.db.fs.files.find_one() three = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, 5, file_document) self.assertEqual(b"foo bar", (await three.read())) gridout = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, file_document={}) with self.assertRaises(NoFile): await gridout.open() @asyncio_test async def test_write_file_like(self): one = motor_asyncio.AsyncIOMotorGridIn(self.db.fs) await one.write(b"hello world") await one.close() two = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, one._id) three = motor_asyncio.AsyncIOMotorGridIn(self.db.fs) await three.write(two) await three.close() four = motor_asyncio.AsyncIOMotorGridOut(self.db.fs, three._id) self.assertEqual(b"hello world", (await four.read())) @asyncio_test async def test_set_after_close(self): f = motor_asyncio.AsyncIOMotorGridIn(self.db.fs, _id="foo", bar="baz") self.assertEqual("foo", f._id) self.assertEqual("baz", f.bar) self.assertRaises(AttributeError, getattr, f, "baz") self.assertRaises(AttributeError, getattr, f, "uploadDate") self.assertRaises(AttributeError, setattr, f, "_id", 5) f.bar = "foo" f.baz = 5 self.assertEqual("foo", f.bar) self.assertEqual(5, f.baz) self.assertRaises(AttributeError, getattr, f, "uploadDate") await f.close() self.assertEqual("foo", f._id) self.assertEqual("foo", f.bar) self.assertEqual(5, f.baz) self.assertTrue(f.uploadDate) self.assertRaises(AttributeError, setattr, f, "_id", 5) await f.set("bar", "a") await f.set("baz", "b") self.assertRaises(AttributeError, setattr, f, "upload_date", 5) g = await motor_asyncio.AsyncIOMotorGridOut(self.db.fs, f._id).open() self.assertEqual("a", g.bar) self.assertEqual("b", g.baz) @asyncio_test async def test_stream_to_handler(self): fs = motor_asyncio.AsyncIOMotorGridFSBucket(self.db) for content_length in (0, 1, 100, 100 * 1000): _id = await fs.upload_from_stream("filename", b"a" * content_length) gridout = await fs.open_download_stream(_id) handler = MockRequestHandler() await gridout.stream_to_handler(handler) self.assertEqual(content_length, handler.n_written) await fs.delete(_id) if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_gridfsbucket.py000066400000000000000000000061641460060525600243510ustar00rootroot00000000000000# Copyright 2016 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. """Test AsyncIOMotorGridFSBucket.""" from io import BytesIO from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.utils import ignore_deprecations from gridfs.errors import NoFile from pymongo.read_preferences import ReadPreference from pymongo.write_concern import WriteConcern from motor.motor_asyncio import AsyncIOMotorGridFSBucket class TestAsyncIOGridFSBucket(AsyncIOTestCase): async def _reset(self): await self.db.drop_collection("fs.files") await self.db.drop_collection("fs.chunks") await self.db.drop_collection("alt.files") await self.db.drop_collection("alt.chunks") def setUp(self): super().setUp() self.loop.run_until_complete(self._reset()) self.bucket = AsyncIOMotorGridFSBucket(self.db) def tearDown(self): self.loop.run_until_complete(self._reset()) super().tearDown() @asyncio_test async def test_basic(self): oid = await self.bucket.upload_from_stream("test_filename", b"hello world") gout = await self.bucket.open_download_stream(oid) self.assertEqual(b"hello world", (await gout.read())) self.assertEqual(1, (await self.db.fs.files.count_documents({}))) self.assertEqual(1, (await self.db.fs.chunks.count_documents({}))) dst = BytesIO() await self.bucket.download_to_stream(gout._id, dst) self.assertEqual(b"hello world", dst.getvalue()) await self.bucket.delete(oid) with self.assertRaises(NoFile): await self.bucket.open_download_stream(oid) self.assertEqual(0, (await self.db.fs.files.count_documents({}))) self.assertEqual(0, (await self.db.fs.chunks.count_documents({}))) def test_init(self): name = "bucket" wc = WriteConcern(w="majority", wtimeout=1000) rp = ReadPreference.SECONDARY size = 8 bucket = AsyncIOMotorGridFSBucket( self.db, name, chunk_size_bytes=size, write_concern=wc, read_preference=rp ) self.assertEqual(name, bucket.collection.name) self.assertEqual(wc, bucket.collection.write_concern) self.assertEqual(rp, bucket.collection.read_preference) self.assertEqual(wc, bucket.delegate._chunks.write_concern) self.assertEqual(rp, bucket.delegate._chunks.read_preference) self.assertEqual(size, bucket.delegate._chunk_size_bytes) @ignore_deprecations def test_collection_param(self): bucket = AsyncIOMotorGridFSBucket(self.db, collection="collection") self.assertEqual("collection", bucket.collection.name) motor-3.4.0/test/asyncio_tests/test_asyncio_ipv6.py000066400000000000000000000036621460060525600225610ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test AsyncIOMotorClient with IPv6.""" import test import unittest from test import SkipTest from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.test_environment import connected, db_password, db_user, env from pymongo import MongoClient from pymongo.errors import ConnectionFailure class MotorIPv6Test(AsyncIOTestCase): @asyncio_test async def test_ipv6(self): assert env.host in ( "localhost", "127.0.0.1", ), "This unittest isn't written to test IPv6 with host %s" % repr(env.host) try: connected( MongoClient( "[::1]", username=db_user, password=db_password, serverSelectionTimeoutMS=100 ) ) except ConnectionFailure: # Either mongod was started without --ipv6 # or the OS doesn't support it (or both). raise SkipTest("No IPV6") if test.env.auth: cx_string = "mongodb://%s:%s@[::1]:%d" % (db_user, db_password, env.port) else: cx_string = "mongodb://[::1]:%d" % env.port cx = self.asyncio_client(uri=cx_string) collection = cx.motor_test.test_collection await collection.insert_one({"dummy": "object"}) self.assertTrue(await collection.find_one({"dummy": "object"})) if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_replica_set.py000066400000000000000000000043421460060525600241630ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test replica set AsyncIOClient.""" import test import unittest from test import SkipTest from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.test_environment import env import pymongo import pymongo.errors from motor import motor_asyncio class TestAsyncIOReplicaSet(AsyncIOTestCase): def setUp(self): if not test.env.is_replica_set: raise SkipTest("Not connected to a replica set") super().setUp() @asyncio_test async def test_connection_failure(self): # Assuming there isn't anything actually running on this port. client = motor_asyncio.AsyncIOMotorClient( "localhost:8765", replicaSet="rs", io_loop=self.loop, serverSelectionTimeoutMS=10 ) with self.assertRaises(pymongo.errors.ConnectionFailure): await client.admin.command("ismaster") class TestReplicaSetClientAgainstStandalone(AsyncIOTestCase): """This is a funny beast -- we want to run tests for a replica set AsyncIOMotorClient but only if the database at DB_IP and DB_PORT is a standalone. """ def setUp(self): super().setUp() if test.env.is_replica_set: raise SkipTest("Connected to a replica set, not a standalone mongod") @asyncio_test async def test_connect(self): client = motor_asyncio.AsyncIOMotorClient( "%s:%s" % (env.host, env.port), replicaSet="anything", serverSelectionTimeoutMS=10, io_loop=self.loop, ) with self.assertRaises(pymongo.errors.ServerSelectionTimeoutError): await client.test.test.find_one() if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_session.py000066400000000000000000000165501460060525600233600ustar00rootroot00000000000000# Copyright 2017-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. """Test ClientSession support with asyncio.""" import copy import sys import unittest from test import SkipTest from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.test_environment import env from test.utils import TestListener, session_ids from pymongo import IndexModel, InsertOne from pymongo.errors import InvalidOperation class TestAsyncIOSession(AsyncIOTestCase): @classmethod def setUpClass(cls): super().setUpClass() if not env.sessions_enabled: raise SkipTest("Sessions not supported") async def _test_ops(self, client, *ops): listener = client.options.event_listeners[0] for f, args, kw in ops: s = await client.start_session() # Simulate "async with" on Python 3.4. try: listener.results.clear() # In case "f" modifies its inputs. args2 = copy.copy(args) kw2 = copy.copy(kw) kw2["session"] = s await f(*args2, **kw2) for event in listener.results["started"]: self.assertTrue( "lsid" in event.command, "%s sent no lsid with %s" % (f.__name__, event.command_name), ) self.assertEqual( s.session_id, event.command["lsid"], "%s sent wrong lsid with %s" % (f.__name__, event.command_name), ) self.assertFalse(s.has_ended) finally: await s.end_session() with self.assertRaisesRegex(InvalidOperation, "ended session"): await f(*args2, **kw2) # No explicit session. for f, args, kw in ops: listener.results.clear() await f(*args, **kw) self.assertGreaterEqual(len(listener.results["started"]), 1) lsids = [] for event in listener.results["started"]: self.assertTrue( "lsid" in event.command, "%s sent no lsid with %s" % (f.__name__, event.command_name), ) lsids.append(event.command["lsid"]) if "PyPy" not in sys.version: # Server session was returned to pool. Ignore interpreters with # non-deterministic GC. for lsid in lsids: self.assertIn( lsid, session_ids(client), "%s did not return implicit session to pool" % (f.__name__,), ) @asyncio_test async def test_database(self): listener = TestListener() client = self.asyncio_client(event_listeners=[listener]) db = client.pymongo_test ops = [ (db.command, ["ping"], {}), (db.drop_collection, ["collection"], {}), (db.create_collection, ["collection"], {}), (db.list_collection_names, [], {}), ] await self._test_ops(client, *ops) @asyncio_test(timeout=60) async def test_collection(self): listener = TestListener() client = self.asyncio_client(event_listeners=[listener]) await client.drop_database("motor_test") coll = client.motor_test.test_collection async def list_indexes(session=None): await coll.list_indexes(session=session).to_list(length=None) async def aggregate(session=None): await coll.aggregate([], session=session).to_list(length=None) # Test some collection methods - the rest are in test_cursor. await self._test_ops( client, (coll.drop, [], {}), (coll.bulk_write, [[InsertOne({})]], {}), (coll.insert_one, [{}], {}), (coll.insert_many, [[{}, {}]], {}), (coll.replace_one, [{}, {}], {}), (coll.update_one, [{}, {"$set": {"a": 1}}], {}), (coll.update_many, [{}, {"$set": {"a": 1}}], {}), (coll.delete_one, [{}], {}), (coll.delete_many, [{}], {}), (coll.find_one_and_replace, [{}, {}], {}), (coll.find_one_and_update, [{}, {"$set": {"a": 1}}], {}), (coll.find_one_and_delete, [{}, {}], {}), (coll.rename, ["collection2"], {}), # Drop collection2 between tests of "rename", above. (client.motor_test.drop_collection, ["collection2"], {}), (coll.distinct, ["a"], {}), (coll.find_one, [], {}), (coll.count_documents, [{}], {}), (coll.create_indexes, [[IndexModel("a")]], {}), (coll.create_index, ["a"], {}), (coll.drop_index, ["a_1"], {}), (coll.drop_indexes, [], {}), (list_indexes, [], {}), (coll.index_information, [], {}), (coll.options, [], {}), (aggregate, [], {}), ) @asyncio_test async def test_cursor(self): listener = TestListener() client = self.asyncio_client(event_listeners=[listener]) await self.make_test_data() coll = client.motor_test.test_collection s = await client.start_session() # Simulate "async with" on Python 3.4. try: listener.results.clear() cursor = coll.find(session=s) await cursor.to_list(length=None) self.assertEqual(len(listener.results["started"]), 2) for event in listener.results["started"]: self.assertTrue( "lsid" in event.command, "find sent no lsid with %s" % (event.command_name,) ) self.assertEqual( s.session_id, event.command["lsid"], "find sent wrong lsid with %s" % (event.command_name,), ) finally: await s.end_session() with self.assertRaisesRegex(InvalidOperation, "ended session"): await coll.find(session=s).to_list(length=None) # No explicit session. listener.results.clear() cursor = coll.find() await cursor.to_list(length=None) self.assertEqual(len(listener.results["started"]), 2) event0 = listener.first_command_started() self.assertTrue( "lsid" in event0.command, "find sent no lsid with %s" % (event0.command_name,) ) lsid = event0.command["lsid"] for event in listener.results["started"][1:]: self.assertTrue( "lsid" in event.command, "find sent no lsid with %s" % (event.command_name,) ) self.assertEqual( lsid, event.command["lsid"], "find sent wrong lsid with %s" % (event.command_name,) ) if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_asyncio_ssl.py000066400000000000000000000142141460060525600224710ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test AsyncIOMotorClient with SSL.""" import asyncio import gc import test import unittest from test.asyncio_tests import asyncio_test from test.test_environment import CA_PEM, CLIENT_PEM, env from unittest import SkipTest from pymongo.errors import ConfigurationError, ConnectionFailure from motor.motor_asyncio import AsyncIOMotorClient # Start a mongod instance like: # # mongod \ # --sslOnNormalPorts \ # --sslPEMKeyFile test/certificates/server.pem \ # --sslCAFile test/certificates/ca.pem # # Also, make sure you have 'server' as an alias for localhost in /etc/hosts class TestAsyncIOSSL(unittest.TestCase): def setUp(self): if not test.env.server_is_resolvable: raise SkipTest( "No hosts entry for 'server'. Cannot validate hostname in the certificate" ) asyncio.set_event_loop(None) self.loop = asyncio.new_event_loop() def tearDown(self): if not hasattr(self, "loop"): return self.loop.stop() self.loop.run_forever() self.loop.close() gc.collect() def test_config_ssl(self): # This test doesn't require a running mongod. self.assertRaises(ValueError, AsyncIOMotorClient, io_loop=self.loop, tls="foo") self.assertRaises( ConfigurationError, AsyncIOMotorClient, io_loop=self.loop, tls=False, tlsCertificateKeyFile=CLIENT_PEM, ) self.assertRaises( IOError, AsyncIOMotorClient, io_loop=self.loop, tlsCertificateKeyFile="NoFile" ) self.assertRaises( TypeError, AsyncIOMotorClient, io_loop=self.loop, tlsCertificateKeyFile=True ) @asyncio_test async def test_cert_ssl(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("Can't test with auth") client = AsyncIOMotorClient( env.host, env.port, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.loop, ) await client.db.collection.find_one() response = await client.admin.command("ismaster") if "setName" in response: client = AsyncIOMotorClient( env.host, env.port, tls=True, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, replicaSet=response["setName"], io_loop=self.loop, ) await client.db.collection.find_one() @asyncio_test async def test_cert_ssl_validation(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("Can't test with auth") client = AsyncIOMotorClient( env.host, env.port, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.loop, ) await client.db.collection.find_one() response = await client.admin.command("ismaster") if "setName" in response: client = AsyncIOMotorClient( env.host, env.port, replicaSet=response["setName"], tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.loop, ) await client.db.collection.find_one() @asyncio_test async def test_cert_ssl_validation_none(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("Can't test with auth") client = AsyncIOMotorClient( test.env.fake_hostname_uri, tlsCertificateKeyFile=CLIENT_PEM, tlsAllowInvalidCertificates=True, tlsCAFile=CA_PEM, io_loop=self.loop, ) await client.admin.command("ismaster") @asyncio_test async def test_cert_ssl_validation_hostname_fail(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("Can't test with auth") client = AsyncIOMotorClient( env.host, env.port, tls=True, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.loop, ) response = await client.admin.command("ismaster") with self.assertRaises(ConnectionFailure): # Create client with hostname 'server', not 'localhost', # which is what the server cert presents. client = AsyncIOMotorClient( test.env.fake_hostname_uri, serverSelectionTimeoutMS=1000, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.loop, ) await client.db.collection.find_one() if "setName" in response: with self.assertRaises(ConnectionFailure): client = AsyncIOMotorClient( test.env.fake_hostname_uri, serverSelectionTimeoutMS=1000, replicaSet=response["setName"], tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.loop, ) await client.db.collection.find_one() motor-3.4.0/test/asyncio_tests/test_asyncio_tests.py000066400000000000000000000142701460060525600230340ustar00rootroot00000000000000# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor's asyncio test utilities.""" import asyncio import concurrent.futures import contextlib import io import os import unittest from test.asyncio_tests import AsyncIOTestCase, asyncio_test def run_test_case(case, suppress_output=True): suite = unittest.defaultTestLoader.loadTestsFromTestCase(case) stream = io.StringIO() if suppress_output else None runner = unittest.TextTestRunner(stream=stream) return runner.run(suite) @contextlib.contextmanager def set_environ(name, value): old_value = os.environ.get(name) os.environ[name] = value try: yield finally: if old_value is None: del os.environ[name] else: os.environ[name] = old_value class TestAsyncIOTests(unittest.TestCase): def test_basic(self): class Test(AsyncIOTestCase): @asyncio_test async def test(self): pass result = run_test_case(Test) self.assertEqual(1, result.testsRun) self.assertEqual(0, len(result.errors)) def test_decorator_with_no_args(self): class TestPasses(AsyncIOTestCase): @asyncio_test async def test_decorated_with_no_args(self): pass result = run_test_case(TestPasses) self.assertEqual(0, len(result.errors)) class TestFails(AsyncIOTestCase): @asyncio_test() async def test_decorated_with_no_args(self): self.fail() result = run_test_case(TestFails) self.assertEqual(1, len(result.failures)) def test_timeout_passed_as_positional(self): with self.assertRaises(TypeError): class _(AsyncIOTestCase): # Should be "timeout=10". @asyncio_test(10) def test_decorated_with_no_args(self): pass def test_timeout(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) self.addCleanup(self.loop.close) self.addCleanup(setattr, self, "loop", None) class Test(AsyncIOTestCase): @asyncio_test(timeout=0.01) async def test_that_is_too_slow(self): await self.middle() async def middle(self): await self.inner() async def inner(self): await asyncio.sleep(1) with set_environ("ASYNC_TEST_TIMEOUT", "0"): result = run_test_case(Test) self.assertEqual(1, len(result.errors)) case, text = result.errors[0] self.assertTrue("TimeoutError" in text) def test_timeout_environment_variable(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) self.addCleanup(self.loop.close) self.addCleanup(setattr, self, "loop", None) @asyncio_test async def default_timeout(self): await asyncio.sleep(0.1) with set_environ("ASYNC_TEST_TIMEOUT", "0.2"): # No error, sleeps for 0.1 seconds and the timeout is 0.2 seconds. default_timeout(self) @asyncio_test(timeout=0.1) async def custom_timeout(self): await asyncio.sleep(0.2) with set_environ("ASYNC_TEST_TIMEOUT", "0"): # No error, default timeout of 5 seconds overrides '0'. default_timeout(self) with set_environ("ASYNC_TEST_TIMEOUT", "0"): if hasattr(asyncio, "exceptions"): with self.assertRaises(asyncio.exceptions.TimeoutError): custom_timeout(self) else: with self.assertRaises(concurrent.futures.TimeoutError): custom_timeout(self) with set_environ("ASYNC_TEST_TIMEOUT", "1"): # No error, 1-second timeout from environment overrides custom # timeout of 0.1 seconds. custom_timeout(self) def test_failure(self): class Test(AsyncIOTestCase): @asyncio_test async def test_that_fails(self): await self.middle() async def middle(self): await self.inner() async def inner(self): assert False, "expected error" result = run_test_case(Test) self.assertEqual(1, len(result.failures)) case, text = result.failures[0] self.assertFalse("CancelledError" in text) self.assertTrue("AssertionError" in text) self.assertTrue("expected error" in text) # The traceback shows where the coroutine raised. self.assertTrue("test_that_fails" in text) self.assertTrue("middle" in text) self.assertTrue("inner" in text) def test_undecorated(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) self.addCleanup(self.loop.close) self.addCleanup(setattr, self, "loop", None) class Test(AsyncIOTestCase): async def test_that_should_be_decorated(self): await asyncio.sleep(0.01) result = run_test_case(Test) self.assertEqual(1, len(result.errors)) case, text = result.errors[0] self.assertFalse("CancelledError" in text) self.assertTrue("TypeError" in text) self.assertTrue("should be decorated with @asyncio_test" in text) def test_other_return(self): class Test(AsyncIOTestCase): def test_other_return(self): return 42 result = run_test_case(Test) self.assertEqual(len(result.errors), 1) case, text = result.errors[0] self.assertIn("Return value from test method ignored", text) if __name__ == "__main__": unittest.main() motor-3.4.0/test/asyncio_tests/test_examples.py000066400000000000000000001530521460060525600217650ustar00rootroot00000000000000# Copyright 2018-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. """MongoDB documentation examples with Motor and asyncio.""" import asyncio import base64 import datetime import unittest from io import StringIO from os import environ from test import env from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.utils import wait_until from threading import Thread from unittest.mock import patch import pymongo from bson import Binary from bson.codec_options import CodecOptions from pymongo import WriteConcern from pymongo.encryption_options import AutoEncryptionOpts from pymongo.errors import ConnectionFailure, OperationFailure from pymongo.read_concern import ReadConcern from pymongo.read_preferences import ReadPreference from pymongo.server_api import ServerApi from motor.motor_asyncio import AsyncIOMotorClientEncryption async def count(cursor): i = 0 async for _ in cursor: i += 1 return i class TestExamples(AsyncIOTestCase): @classmethod def setUpClass(cls): env.sync_cx.motor_test.inventory.drop() def tearDown(self): env.sync_cx.motor_test.inventory.drop() @asyncio_test async def test_first_three_examples(self): db = self.db # Start Example 1 await db.inventory.insert_one( { "item": "canvas", "qty": 100, "tags": ["cotton"], "size": {"h": 28, "w": 35.5, "uom": "cm"}, } ) # End Example 1 self.assertEqual(await db.inventory.count_documents({}), 1) # Start Example 2 cursor = db.inventory.find({"item": "canvas"}) # End Example 2 self.assertEqual(await count(cursor), 1) # Start Example 3 await db.inventory.insert_many( [ { "item": "journal", "qty": 25, "tags": ["blank", "red"], "size": {"h": 14, "w": 21, "uom": "cm"}, }, { "item": "mat", "qty": 85, "tags": ["gray"], "size": {"h": 27.9, "w": 35.5, "uom": "cm"}, }, { "item": "mousepad", "qty": 25, "tags": ["gel", "blue"], "size": {"h": 19, "w": 22.85, "uom": "cm"}, }, ] ) # End Example 3 self.assertEqual(await db.inventory.count_documents({}), 4) @asyncio_test async def test_query_top_level_fields(self): db = self.db # Start Example 6 await db.inventory.insert_many( [ { "item": "journal", "qty": 25, "size": {"h": 14, "w": 21, "uom": "cm"}, "status": "A", }, { "item": "notebook", "qty": 50, "size": {"h": 8.5, "w": 11, "uom": "in"}, "status": "A", }, { "item": "paper", "qty": 100, "size": {"h": 8.5, "w": 11, "uom": "in"}, "status": "D", }, { "item": "planner", "qty": 75, "size": {"h": 22.85, "w": 30, "uom": "cm"}, "status": "D", }, { "item": "postcard", "qty": 45, "size": {"h": 10, "w": 15.25, "uom": "cm"}, "status": "A", }, ] ) # End Example 6 self.assertEqual(await db.inventory.count_documents({}), 5) # Start Example 7 cursor = db.inventory.find({}) # End Example 7 self.assertEqual(await count(cursor), 5) # Start Example 9 cursor = db.inventory.find({"status": "D"}) # End Example 9 self.assertEqual(await count(cursor), 2) # Start Example 10 cursor = db.inventory.find({"status": {"$in": ["A", "D"]}}) # End Example 10 self.assertEqual(await count(cursor), 5) # Start Example 11 cursor = db.inventory.find({"status": "A", "qty": {"$lt": 30}}) # End Example 11 self.assertEqual(await count(cursor), 1) # Start Example 12 cursor = db.inventory.find({"$or": [{"status": "A"}, {"qty": {"$lt": 30}}]}) # End Example 12 self.assertEqual(await count(cursor), 3) # Start Example 13 cursor = db.inventory.find( {"status": "A", "$or": [{"qty": {"$lt": 30}}, {"item": {"$regex": "^p"}}]} ) # End Example 13 self.assertEqual(await count(cursor), 2) @asyncio_test async def test_query_embedded_documents(self): db = self.db # Start Example 14 # Subdocument key order matters in a few of these examples so we have # to use bson.son.SON instead of a Python dict. from bson.son import SON await db.inventory.insert_many( [ { "item": "journal", "qty": 25, "size": SON([("h", 14), ("w", 21), ("uom", "cm")]), "status": "A", }, { "item": "notebook", "qty": 50, "size": SON([("h", 8.5), ("w", 11), ("uom", "in")]), "status": "A", }, { "item": "paper", "qty": 100, "size": SON([("h", 8.5), ("w", 11), ("uom", "in")]), "status": "D", }, { "item": "planner", "qty": 75, "size": SON([("h", 22.85), ("w", 30), ("uom", "cm")]), "status": "D", }, { "item": "postcard", "qty": 45, "size": SON([("h", 10), ("w", 15.25), ("uom", "cm")]), "status": "A", }, ] ) # End Example 14 # Start Example 15 cursor = db.inventory.find({"size": SON([("h", 14), ("w", 21), ("uom", "cm")])}) # End Example 15 self.assertEqual(await count(cursor), 1) # Start Example 16 cursor = db.inventory.find({"size": SON([("w", 21), ("h", 14), ("uom", "cm")])}) # End Example 16 self.assertEqual(await count(cursor), 0) # Start Example 17 cursor = db.inventory.find({"size.uom": "in"}) # End Example 17 self.assertEqual(await count(cursor), 2) # Start Example 18 cursor = db.inventory.find({"size.h": {"$lt": 15}}) # End Example 18 self.assertEqual(await count(cursor), 4) # Start Example 19 cursor = db.inventory.find({"size.h": {"$lt": 15}, "size.uom": "in", "status": "D"}) # End Example 19 self.assertEqual(await count(cursor), 1) @asyncio_test async def test_query_arrays(self): db = self.db # Start Example 20 await db.inventory.insert_many( [ {"item": "journal", "qty": 25, "tags": ["blank", "red"], "dim_cm": [14, 21]}, {"item": "notebook", "qty": 50, "tags": ["red", "blank"], "dim_cm": [14, 21]}, { "item": "paper", "qty": 100, "tags": ["red", "blank", "plain"], "dim_cm": [14, 21], }, {"item": "planner", "qty": 75, "tags": ["blank", "red"], "dim_cm": [22.85, 30]}, {"item": "postcard", "qty": 45, "tags": ["blue"], "dim_cm": [10, 15.25]}, ] ) # End Example 20 # Start Example 21 cursor = db.inventory.find({"tags": ["red", "blank"]}) # End Example 21 self.assertEqual(await count(cursor), 1) # Start Example 22 cursor = db.inventory.find({"tags": {"$all": ["red", "blank"]}}) # End Example 22 self.assertEqual(await count(cursor), 4) # Start Example 23 cursor = db.inventory.find({"tags": "red"}) # End Example 23 self.assertEqual(await count(cursor), 4) # Start Example 24 cursor = db.inventory.find({"dim_cm": {"$gt": 25}}) # End Example 24 self.assertEqual(await count(cursor), 1) # Start Example 25 cursor = db.inventory.find({"dim_cm": {"$gt": 15, "$lt": 20}}) # End Example 25 self.assertEqual(await count(cursor), 4) # Start Example 26 cursor = db.inventory.find({"dim_cm": {"$elemMatch": {"$gt": 22, "$lt": 30}}}) # End Example 26 self.assertEqual(await count(cursor), 1) # Start Example 27 cursor = db.inventory.find({"dim_cm.1": {"$gt": 25}}) # End Example 27 self.assertEqual(await count(cursor), 1) # Start Example 28 cursor = db.inventory.find({"tags": {"$size": 3}}) # End Example 28 self.assertEqual(await count(cursor), 1) @asyncio_test async def test_query_array_of_documents(self): db = self.db # Start Example 29 # Subdocument key order matters in a few of these examples so we have # to use bson.son.SON instead of a Python dict. from bson.son import SON await db.inventory.insert_many( [ { "item": "journal", "instock": [ SON([("warehouse", "A"), ("qty", 5)]), SON([("warehouse", "C"), ("qty", 15)]), ], }, {"item": "notebook", "instock": [SON([("warehouse", "C"), ("qty", 5)])]}, { "item": "paper", "instock": [ SON([("warehouse", "A"), ("qty", 60)]), SON([("warehouse", "B"), ("qty", 15)]), ], }, { "item": "planner", "instock": [ SON([("warehouse", "A"), ("qty", 40)]), SON([("warehouse", "B"), ("qty", 5)]), ], }, { "item": "postcard", "instock": [ SON([("warehouse", "B"), ("qty", 15)]), SON([("warehouse", "C"), ("qty", 35)]), ], }, ] ) # End Example 29 # Start Example 30 cursor = db.inventory.find({"instock": SON([("warehouse", "A"), ("qty", 5)])}) # End Example 30 self.assertEqual(await count(cursor), 1) # Start Example 31 cursor = db.inventory.find({"instock": SON([("qty", 5), ("warehouse", "A")])}) # End Example 31 self.assertEqual(await count(cursor), 0) # Start Example 32 cursor = db.inventory.find({"instock.0.qty": {"$lte": 20}}) # End Example 32 self.assertEqual(await count(cursor), 3) # Start Example 33 cursor = db.inventory.find({"instock.qty": {"$lte": 20}}) # End Example 33 self.assertEqual(await count(cursor), 5) # Start Example 34 cursor = db.inventory.find({"instock": {"$elemMatch": {"qty": 5, "warehouse": "A"}}}) # End Example 34 self.assertEqual(await count(cursor), 1) # Start Example 35 cursor = db.inventory.find({"instock": {"$elemMatch": {"qty": {"$gt": 10, "$lte": 20}}}}) # End Example 35 self.assertEqual(await count(cursor), 3) # Start Example 36 cursor = db.inventory.find({"instock.qty": {"$gt": 10, "$lte": 20}}) # End Example 36 self.assertEqual(await count(cursor), 4) # Start Example 37 cursor = db.inventory.find({"instock.qty": 5, "instock.warehouse": "A"}) # End Example 37 self.assertEqual(await count(cursor), 2) @asyncio_test async def test_query_null(self): db = self.db # Start Example 38 await db.inventory.insert_many([{"_id": 1, "item": None}, {"_id": 2}]) # End Example 38 # Start Example 39 cursor = db.inventory.find({"item": None}) # End Example 39 self.assertEqual(await count(cursor), 2) # Start Example 40 cursor = db.inventory.find({"item": {"$type": 10}}) # End Example 40 self.assertEqual(await count(cursor), 1) # Start Example 41 cursor = db.inventory.find({"item": {"$exists": False}}) # End Example 41 self.assertEqual(await count(cursor), 1) @asyncio_test async def test_projection(self): db = self.db # Start Example 42 await db.inventory.insert_many( [ { "item": "journal", "status": "A", "size": {"h": 14, "w": 21, "uom": "cm"}, "instock": [{"warehouse": "A", "qty": 5}], }, { "item": "notebook", "status": "A", "size": {"h": 8.5, "w": 11, "uom": "in"}, "instock": [{"warehouse": "C", "qty": 5}], }, { "item": "paper", "status": "D", "size": {"h": 8.5, "w": 11, "uom": "in"}, "instock": [{"warehouse": "A", "qty": 60}], }, { "item": "planner", "status": "D", "size": {"h": 22.85, "w": 30, "uom": "cm"}, "instock": [{"warehouse": "A", "qty": 40}], }, { "item": "postcard", "status": "A", "size": {"h": 10, "w": 15.25, "uom": "cm"}, "instock": [{"warehouse": "B", "qty": 15}, {"warehouse": "C", "qty": 35}], }, ] ) # End Example 42 # Start Example 43 cursor = db.inventory.find({"status": "A"}) # End Example 43 self.assertEqual(await count(cursor), 3) # Start Example 44 cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1}) # End Example 44 async for doc in cursor: self.assertTrue("_id" in doc) self.assertTrue("item" in doc) self.assertTrue("status" in doc) self.assertFalse("size" in doc) self.assertFalse("instock" in doc) # Start Example 45 cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1, "_id": 0}) # End Example 45 async for doc in cursor: self.assertFalse("_id" in doc) self.assertTrue("item" in doc) self.assertTrue("status" in doc) self.assertFalse("size" in doc) self.assertFalse("instock" in doc) # Start Example 46 cursor = db.inventory.find({"status": "A"}, {"status": 0, "instock": 0}) # End Example 46 async for doc in cursor: self.assertTrue("_id" in doc) self.assertTrue("item" in doc) self.assertFalse("status" in doc) self.assertTrue("size" in doc) self.assertFalse("instock" in doc) # Start Example 47 cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1, "size.uom": 1}) # End Example 47 async for doc in cursor: self.assertTrue("_id" in doc) self.assertTrue("item" in doc) self.assertTrue("status" in doc) self.assertTrue("size" in doc) self.assertFalse("instock" in doc) size = doc["size"] self.assertTrue("uom" in size) self.assertFalse("h" in size) self.assertFalse("w" in size) # Start Example 48 cursor = db.inventory.find({"status": "A"}, {"size.uom": 0}) # End Example 48 async for doc in cursor: self.assertTrue("_id" in doc) self.assertTrue("item" in doc) self.assertTrue("status" in doc) self.assertTrue("size" in doc) self.assertTrue("instock" in doc) size = doc["size"] self.assertFalse("uom" in size) self.assertTrue("h" in size) self.assertTrue("w" in size) # Start Example 49 cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1, "instock.qty": 1}) # End Example 49 async for doc in cursor: self.assertTrue("_id" in doc) self.assertTrue("item" in doc) self.assertTrue("status" in doc) self.assertFalse("size" in doc) self.assertTrue("instock" in doc) for subdoc in doc["instock"]: self.assertFalse("warehouse" in subdoc) self.assertTrue("qty" in subdoc) # Start Example 50 cursor = db.inventory.find( {"status": "A"}, {"item": 1, "status": 1, "instock": {"$slice": -1}} ) # End Example 50 async for doc in cursor: self.assertTrue("_id" in doc) self.assertTrue("item" in doc) self.assertTrue("status" in doc) self.assertFalse("size" in doc) self.assertTrue("instock" in doc) self.assertEqual(len(doc["instock"]), 1) @asyncio_test async def test_update_and_replace(self): db = self.db # Start Example 51 await db.inventory.insert_many( [ { "item": "canvas", "qty": 100, "size": {"h": 28, "w": 35.5, "uom": "cm"}, "status": "A", }, { "item": "journal", "qty": 25, "size": {"h": 14, "w": 21, "uom": "cm"}, "status": "A", }, { "item": "mat", "qty": 85, "size": {"h": 27.9, "w": 35.5, "uom": "cm"}, "status": "A", }, { "item": "mousepad", "qty": 25, "size": {"h": 19, "w": 22.85, "uom": "cm"}, "status": "P", }, { "item": "notebook", "qty": 50, "size": {"h": 8.5, "w": 11, "uom": "in"}, "status": "P", }, { "item": "paper", "qty": 100, "size": {"h": 8.5, "w": 11, "uom": "in"}, "status": "D", }, { "item": "planner", "qty": 75, "size": {"h": 22.85, "w": 30, "uom": "cm"}, "status": "D", }, { "item": "postcard", "qty": 45, "size": {"h": 10, "w": 15.25, "uom": "cm"}, "status": "A", }, { "item": "sketchbook", "qty": 80, "size": {"h": 14, "w": 21, "uom": "cm"}, "status": "A", }, { "item": "sketch pad", "qty": 95, "size": {"h": 22.85, "w": 30.5, "uom": "cm"}, "status": "A", }, ] ) # End Example 51 # Start Example 52 await db.inventory.update_one( {"item": "paper"}, {"$set": {"size.uom": "cm", "status": "P"}, "$currentDate": {"lastModified": True}}, ) # End Example 52 async for doc in db.inventory.find({"item": "paper"}): self.assertEqual(doc["size"]["uom"], "cm") self.assertEqual(doc["status"], "P") self.assertTrue("lastModified" in doc) # Start Example 53 await db.inventory.update_many( {"qty": {"$lt": 50}}, {"$set": {"size.uom": "in", "status": "P"}, "$currentDate": {"lastModified": True}}, ) # End Example 53 async for doc in db.inventory.find({"qty": {"$lt": 50}}): self.assertEqual(doc["size"]["uom"], "in") self.assertEqual(doc["status"], "P") self.assertTrue("lastModified" in doc) # Start Example 54 await db.inventory.replace_one( {"item": "paper"}, { "item": "paper", "instock": [{"warehouse": "A", "qty": 60}, {"warehouse": "B", "qty": 40}], }, ) # End Example 54 async for doc in db.inventory.find({"item": "paper"}, {"_id": 0}): self.assertEqual(len(doc.keys()), 2) self.assertTrue("item" in doc) self.assertTrue("instock" in doc) self.assertEqual(len(doc["instock"]), 2) @asyncio_test async def test_delete(self): db = self.db # Start Example 55 await db.inventory.insert_many( [ { "item": "journal", "qty": 25, "size": {"h": 14, "w": 21, "uom": "cm"}, "status": "A", }, { "item": "notebook", "qty": 50, "size": {"h": 8.5, "w": 11, "uom": "in"}, "status": "P", }, { "item": "paper", "qty": 100, "size": {"h": 8.5, "w": 11, "uom": "in"}, "status": "D", }, { "item": "planner", "qty": 75, "size": {"h": 22.85, "w": 30, "uom": "cm"}, "status": "D", }, { "item": "postcard", "qty": 45, "size": {"h": 10, "w": 15.25, "uom": "cm"}, "status": "A", }, ] ) # End Example 55 self.assertEqual(await db.inventory.count_documents({}), 5) # Start Example 57 await db.inventory.delete_many({"status": "A"}) # End Example 57 self.assertEqual(await db.inventory.count_documents({}), 3) # Start Example 58 await db.inventory.delete_one({"status": "D"}) # End Example 58 self.assertEqual(await db.inventory.count_documents({}), 2) # Start Example 56 await db.inventory.delete_many({}) # End Example 56 self.assertEqual(await db.inventory.count_documents({}), 0) @env.require_version_min(3, 6) @env.require_replica_set @asyncio_test async def test_change_streams(self): db = self.db done = False async def insert_docs(): while not done: await db.inventory.insert_one({"username": "alice"}) await db.inventory.delete_one({"username": "alice"}) task = asyncio.ensure_future(insert_docs()) try: # Start Changestream Example 1 cursor = db.inventory.watch() document = await cursor.next() # End Changestream Example 1 # Start Changestream Example 2 cursor = db.inventory.watch(full_document="updateLookup") document = await cursor.next() # End Changestream Example 2 # Start Changestream Example 3 resume_token = cursor.resume_token cursor = db.inventory.watch(resume_after=resume_token) document = await cursor.next() # End Changestream Example 3 # Start Changestream Example 4 pipeline = [ {"$match": {"fullDocument.username": "alice"}}, {"$addFields": {"newField": "this is an added field!"}}, ] cursor = db.inventory.watch(pipeline=pipeline) document = await cursor.next() # End Changestream Example 4 finally: done = True await task # $lookup was new in 3.2. The let and pipeline options were added in 3.6. @env.require_version_min(3, 6) @asyncio_test async def test_aggregate_examples(self): db = self.db # Start Aggregation Example 1 cursor = db.sales.aggregate([{"$match": {"items.fruit": "banana"}}, {"$sort": {"date": 1}}]) async for doc in cursor: print(doc) # End Aggregation Example 1 # Start Aggregation Example 2 cursor = db.sales.aggregate( [ {"$unwind": "$items"}, {"$match": {"items.fruit": "banana"}}, { "$group": { "_id": {"day": {"$dayOfWeek": "$date"}}, "count": {"$sum": "$items.quantity"}, } }, {"$project": {"dayOfWeek": "$_id.day", "numberSold": "$count", "_id": 0}}, {"$sort": {"numberSold": 1}}, ] ) async for doc in cursor: print(doc) # End Aggregation Example 2 # Start Aggregation Example 3 cursor = db.sales.aggregate( [ {"$unwind": "$items"}, { "$group": { "_id": {"day": {"$dayOfWeek": "$date"}}, "items_sold": {"$sum": "$items.quantity"}, "revenue": {"$sum": {"$multiply": ["$items.quantity", "$items.price"]}}, } }, { "$project": { "day": "$_id.day", "revenue": 1, "items_sold": 1, "discount": { "$cond": {"if": {"$lte": ["$revenue", 250]}, "then": 25, "else": 0} }, } }, ] ) async for doc in cursor: print(doc) # End Aggregation Example 3 # Start Aggregation Example 4 cursor = db.air_alliances.aggregate( [ { "$lookup": { "from": "air_airlines", "let": {"constituents": "$airlines"}, "pipeline": [{"$match": {"$expr": {"$in": ["$name", "$$constituents"]}}}], "as": "airlines", } }, { "$project": { "_id": 0, "name": 1, "airlines": { "$filter": { "input": "$airlines", "as": "airline", "cond": {"$eq": ["$$airline.country", "Canada"]}, } }, } }, ] ) async for doc in cursor: print(doc) # End Aggregation Example 4 @asyncio_test async def test_commands(self): db = self.db await db.restaurants.insert_one({}) # Start runCommand Example 1 info = await db.command("buildInfo") # End runCommand Example 1 # Start runCommand Example 2 count = await db.command("count", "restaurants") # End runCommand Example 2 @asyncio_test async def test_index_management(self): db = self.db # Start Index Example 1 await db.records.create_index("score") # End Index Example 1 # Start Index Example 1 await db.restaurants.create_index( [("cuisine", pymongo.ASCENDING), ("name", pymongo.ASCENDING)], partialFilterExpression={"rating": {"$gt": 5}}, ) # End Index Example 1 @env.require_version_min(3, 7) @env.require_replica_set @asyncio_test async def test_transactions(self): # Transaction examples self.addCleanup(env.sync_cx.drop_database, "hr") self.addCleanup(env.sync_cx.drop_database, "reporting") client = self.cx employees = self.cx.hr.employees events = self.cx.reporting.events await employees.insert_one({"employee": 3, "status": "Active"}) await events.insert_one({"employee": 3, "status": {"new": "Active", "old": None}}) # Start Transactions Intro Example 1 async def update_employee_info(session): employees_coll = session.client.hr.employees events_coll = session.client.reporting.events async with session.start_transaction( read_concern=ReadConcern("snapshot"), write_concern=WriteConcern(w="majority") ): await employees_coll.update_one( {"employee": 3}, {"$set": {"status": "Inactive"}}, session=session ) await events_coll.insert_one( {"employee": 3, "status": {"new": "Inactive", "old": "Active"}}, session=session ) while True: try: # Commit uses write concern set at transaction start. await session.commit_transaction() print("Transaction committed.") break except (ConnectionFailure, OperationFailure) as exc: # Can retry commit if exc.has_error_label("UnknownTransactionCommitResult"): print("UnknownTransactionCommitResult, retrying commit operation ...") continue else: print("Error during commit ...") raise # End Transactions Intro Example 1 # Test the example. with patch("sys.stdout", new_callable=StringIO) as mock_stdout: async with await client.start_session() as s: await update_employee_info(s) employee = await employees.find_one({"employee": 3}) self.assertIsNotNone(employee) self.assertEqual(employee["status"], "Inactive") self.assertIn("Transaction committed", mock_stdout.getvalue()) # Start Transactions Retry Example 1 async def run_transaction_with_retry(txn_coro, session): while True: try: await txn_coro(session) # performs transaction break except (ConnectionFailure, OperationFailure) as exc: print("Transaction aborted. Caught exception during transaction.") # If transient error, retry the whole transaction if exc.has_error_label("TransientTransactionError"): print("TransientTransactionError, retryin transaction ...") continue else: raise # End Transactions Retry Example 1 # Test the example. with patch("sys.stdout", new_callable=StringIO) as mock_stdout: async with await client.start_session() as s: await run_transaction_with_retry(update_employee_info, s) employee = await employees.find_one({"employee": 3}) self.assertIsNotNone(employee) self.assertEqual(employee["status"], "Inactive") self.assertIn("Transaction committed", mock_stdout.getvalue()) # Start Transactions Retry Example 2 async def commit_with_retry(session): while True: try: # Commit uses write concern set at transaction start. await session.commit_transaction() print("Transaction committed.") break except (ConnectionFailure, OperationFailure) as exc: # Can retry commit if exc.has_error_label("UnknownTransactionCommitResult"): print("UnknownTransactionCommitResult, retrying commit operation ...") continue else: print("Error during commit ...") raise # End Transactions Retry Example 2 # Test commit_with_retry from the previous examples async def _insert_employee_retry_commit(session): async with session.start_transaction(): await employees.insert_one({"employee": 4, "status": "Active"}, session=session) await events.insert_one( {"employee": 4, "status": {"new": "Active", "old": None}}, session=session ) await commit_with_retry(session) with patch("sys.stdout", new_callable=StringIO) as mock_stdout: async with await client.start_session() as s: await run_transaction_with_retry(_insert_employee_retry_commit, s) employee = await employees.find_one({"employee": 4}) self.assertIsNotNone(employee) self.assertEqual(employee["status"], "Active") self.assertIn("Transaction committed", mock_stdout.getvalue()) # Start Transactions Retry Example 3 async def run_transaction_with_retry(txn_coro, session): while True: try: await txn_coro(session) # performs transaction break except (ConnectionFailure, OperationFailure) as exc: # If transient error, retry the whole transaction if exc.has_error_label("TransientTransactionError"): print("TransientTransactionError, retrying transaction ...") continue else: raise async def commit_with_retry(session): while True: try: # Commit uses write concern set at transaction start. await session.commit_transaction() print("Transaction committed.") break except (ConnectionFailure, OperationFailure) as exc: # Can retry commit if exc.has_error_label("UnknownTransactionCommitResult"): print("UnknownTransactionCommitResult, retrying commit operation ...") continue else: print("Error during commit ...") raise # Updates two collections in a transactions async def update_employee_info(session): employees_coll = session.client.hr.employees events_coll = session.client.reporting.events async with session.start_transaction( read_concern=ReadConcern("snapshot"), write_concern=WriteConcern(w="majority"), read_preference=ReadPreference.PRIMARY, ): await employees_coll.update_one( {"employee": 3}, {"$set": {"status": "Inactive"}}, session=session ) await events_coll.insert_one( {"employee": 3, "status": {"new": "Inactive", "old": "Active"}}, session=session ) await commit_with_retry(session) # Start a session. async with await client.start_session() as session: try: await run_transaction_with_retry(update_employee_info, session) except Exception as exc: # Do something with error. raise # End Transactions Retry Example 3 employee = await employees.find_one({"employee": 3}) self.assertIsNotNone(employee) self.assertEqual(employee["status"], "Inactive") AsyncIOMotorClient = lambda _: self.cx uriString = None # Start Transactions withTxn API Example 1 # For a replica set, include the replica set name and a seedlist of the members in the URI string; e.g. # uriString = 'mongodb://mongodb0.example.com:27017,mongodb1.example.com:27017/?replicaSet=myRepl' # For a sharded cluster, connect to the mongos instances; e.g. # uriString = 'mongodb://mongos0.example.com:27017,mongos1.example.com:27017/' client = AsyncIOMotorClient(uriString) wc_majority = WriteConcern("majority", wtimeout=1000) # Prereq: Create collections. await client.get_database("mydb1", write_concern=wc_majority).foo.insert_one({"abc": 0}) await client.get_database("mydb2", write_concern=wc_majority).bar.insert_one({"xyz": 0}) # Step 1: Define the callback that specifies the sequence of operations to perform inside the transactions. async def callback(my_session): collection_one = my_session.client.mydb1.foo collection_two = my_session.client.mydb2.bar # Important:: You must pass the session to the operations. await collection_one.insert_one({"abc": 1}, session=my_session) await collection_two.insert_one({"xyz": 999}, session=my_session) # Step 2: Start a client session. async with await client.start_session() as session: # Step 3: Use with_transaction to start a transaction, execute the callback, and commit (or abort on error). await session.with_transaction( callback, read_concern=ReadConcern("local"), write_concern=wc_majority, read_preference=ReadPreference.PRIMARY, ) # End Transactions withTxn API Example 1 @env.require_version_min(3, 6) @env.require_replica_set @asyncio_test async def test_causal_consistency(self): # Causal consistency examples client = self.cx self.addCleanup(env.sync_cx.drop_database, "test") await client.test.drop_collection("items") await client.test.items.insert_one( {"sku": "111", "name": "Peanuts", "start": datetime.datetime.today()} ) # Start Causal Consistency Example 1 async with await client.start_session(causal_consistency=True) as s1: current_date = datetime.datetime.today() items = client.get_database( "test", read_concern=ReadConcern("majority"), write_concern=WriteConcern("majority", wtimeout=1000), ).items await items.update_one( {"sku": "111", "end": None}, {"$set": {"end": current_date}}, session=s1 ) await items.insert_one( {"sku": "nuts-111", "name": "Pecans", "start": current_date}, session=s1 ) # End Causal Consistency Example 1 # Start Causal Consistency Example 2 async with await client.start_session(causal_consistency=True) as s2: s2.advance_cluster_time(s1.cluster_time) s2.advance_operation_time(s1.operation_time) items = client.get_database( "test", read_preference=ReadPreference.SECONDARY, read_concern=ReadConcern("majority"), write_concern=WriteConcern("majority", wtimeout=1000), ).items async for item in items.find({"end": None}, session=s2): print(item) # End Causal Consistency Example 2 @env.require_version_min(4, 7) @asyncio_test async def test_versioned_api(self): # Versioned API examples # Use connect=False to reduce overhead as client is not used to run # any operations. AsyncIOMotorClient = lambda _, server_api: self.asyncio_client( server_api=server_api, connect=False ) uri = None # Start Versioned API Example 1 from pymongo.server_api import ServerApi client = AsyncIOMotorClient(uri, server_api=ServerApi("1")) # End Versioned API Example 1 # Start Versioned API Example 2 client = AsyncIOMotorClient(uri, server_api=ServerApi("1", strict=True)) # End Versioned API Example 2 # Start Versioned API Example 3 client = AsyncIOMotorClient(uri, server_api=ServerApi("1", strict=False)) # End Versioned API Example 3 # Start Versioned API Example 4 client = AsyncIOMotorClient(uri, server_api=ServerApi("1", deprecation_errors=True)) # End Versioned API Example 4 @unittest.skip("MOTOR-908 count has been added to API version 1") @env.require_version_min(4, 7) # Only run on RS until https://jira.mongodb.org/browse/SERVER-58785 is resolved. @env.require_replica_set @asyncio_test async def test_versioned_api_migration(self): client = self.asyncio_client(server_api=ServerApi("1", strict=True)) await client.db.sales.drop() # Start Versioned API Example 5 def strptime(s): return datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ") await client.db.sales.insert_many( [ { "_id": 1, "item": "abc", "price": 10, "quantity": 2, "date": strptime("2021-01-01T08:00:00Z"), }, { "_id": 2, "item": "jkl", "price": 20, "quantity": 1, "date": strptime("2021-02-03T09:00:00Z"), }, { "_id": 3, "item": "xyz", "price": 5, "quantity": 5, "date": strptime("2021-02-03T09:05:00Z"), }, { "_id": 4, "item": "abc", "price": 10, "quantity": 10, "date": strptime("2021-02-15T08:00:00Z"), }, { "_id": 5, "item": "xyz", "price": 5, "quantity": 10, "date": strptime("2021-02-15T09:05:00Z"), }, { "_id": 6, "item": "xyz", "price": 5, "quantity": 5, "date": strptime("2021-02-15T12:05:10Z"), }, { "_id": 7, "item": "xyz", "price": 5, "quantity": 10, "date": strptime("2021-02-15T14:12:12Z"), }, { "_id": 8, "item": "abc", "price": 10, "quantity": 5, "date": strptime("2021-03-16T20:20:13Z"), }, ] ) # End Versioned API Example 5 with self.assertRaisesRegex( OperationFailure, "Provided apiStrict:true, but the command count is not in API Version 1", ): await client.db.command("count", "sales", query={}) # Start Versioned API Example 6 # pymongo.errors.OperationFailure: Provided apiStrict:true, but the command count is not in API Version 1, full error: {'ok': 0.0, 'errmsg': 'Provided apiStrict:true, but the command count is not in API Version 1', 'code': 323, 'codeName': 'APIStrictError'} # End Versioned API Example 6 # Start Versioned API Example 7 await client.db.sales.count_documents({}) # End Versioned API Example 7 # Start Versioned API Example 8 # 8 # End Versioned API Example 8 @env.require_version_min(5, 0) @asyncio_test async def test_snapshot_query(self): client = self.cx if not env.is_replica_set and not env.is_mongos: self.skipTest("Must be a sharded or replicaset") self.addCleanup(client.drop_database, "pets") db = client.pets await db.drop_collection("cats") await db.drop_collection("dogs") await db.cats.insert_one( {"name": "Whiskers", "color": "white", "age": 10, "adoptable": True} ) await db.dogs.insert_one( {"name": "Pebbles", "color": "Brown", "age": 10, "adoptable": True} ) await wait_until(lambda: self.check_for_snapshot(db.cats), "success") await wait_until(lambda: self.check_for_snapshot(db.dogs), "success") # Start Snapshot Query Example 1 db = client.pets async with await client.start_session(snapshot=True) as s: adoptablePetsCount = 0 docs = await db.cats.aggregate( [{"$match": {"adoptable": True}}, {"$count": "adoptableCatsCount"}], session=s ).to_list(None) adoptablePetsCount = docs[0]["adoptableCatsCount"] docs = await db.dogs.aggregate( [{"$match": {"adoptable": True}}, {"$count": "adoptableDogsCount"}], session=s ).to_list(None) adoptablePetsCount += docs[0]["adoptableDogsCount"] print(adoptablePetsCount) # End Snapshot Query Example 1 db = client.retail self.addCleanup(client.drop_database, "retail") await db.drop_collection("sales") saleDate = datetime.datetime.now() await db.sales.insert_one({"shoeType": "boot", "price": 30, "saleDate": saleDate}) await wait_until(lambda: self.check_for_snapshot(db.sales), "success") # Start Snapshot Query Example 2 db = client.retail async with await client.start_session(snapshot=True) as s: docs = await db.sales.aggregate( [ { "$match": { "$expr": { "$gt": [ "$saleDate", { "$dateSubtract": { "startDate": "$$NOW", "unit": "day", "amount": 1, } }, ] } } }, {"$count": "totalDailySales"}, ], session=s, ).to_list(None) total = docs[0]["totalDailySales"] print(total) # End Snapshot Query Example 2 async def check_for_snapshot(self, collection): """Wait for snapshot reads to become available to prevent this error: [246:SnapshotUnavailable]: Unable to read from a snapshot due to pending collection catalog changes; please retry the operation. Snapshot timestamp is Timestamp(1646666892, 4). Collection minimum is Timestamp(1646666892, 5) (on localhost:27017, modern retry, attempt 1) From https://github.com/mongodb/mongo-ruby-driver/commit/7c4117b58e3d12e237f7536f7521e18fc15f79ac """ client = collection.database.client async with await client.start_session(snapshot=True) as s: try: if await collection.find_one(session=s): return True return False except OperationFailure as e: # Retry them as the server demands... if e.code == 246: # SnapshotUnavailable return False raise LOCAL_MASTER_KEY = base64.b64decode( b"Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ" b"5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" ) class TestQueryableEncryptionDocsExample(AsyncIOTestCase): # Queryable Encryption is not supported on Standalone topology. @env.require_version_min(7, 0, -1) @asyncio_test @env.require_replica_set async def test_queryable_encryption(self): client = self.cx # MongoClient to use in testing that handles auth/tls/etc, # and cleanup. def MongoClient(**kwargs): c = self.asyncio_client(**kwargs) self.addCleanup(c.close) return c # Drop data from prior test runs. await client.keyvault.datakeys.drop() await client.drop_database("docs_examples") kms_providers_map = {"local": {"key": LOCAL_MASTER_KEY}} # Create two data keys. key_vault_client = MongoClient() await key_vault_client.admin.command("ping") client_encryption = AsyncIOMotorClientEncryption( kms_providers_map, "keyvault.datakeys", key_vault_client, CodecOptions() ) key1_id = await client_encryption.create_data_key("local") key2_id = await client_encryption.create_data_key("local") # Create an encryptedFieldsMap. encrypted_fields_map = { "docs_examples.encrypted": { "fields": [ { "path": "encrypted_indexed", "bsonType": "string", "keyId": key1_id, "queries": [ { "queryType": "equality", }, ], }, { "path": "encrypted_unindexed", "bsonType": "string", "keyId": key2_id, }, ], }, } # Create an Queryable Encryption collection. opts = AutoEncryptionOpts( kms_providers_map, "keyvault.datakeys", encrypted_fields_map=encrypted_fields_map ) encrypted_client = MongoClient(auto_encryption_opts=opts) # Create a Queryable Encryption collection "docs_examples.encrypted". # Because docs_examples.encrypted is in encrypted_fields_map, it is # created with Queryable Encryption support. db = encrypted_client.docs_examples encrypted_coll = await db.create_collection("encrypted") # Auto encrypt an insert and find. # Encrypt an insert. await encrypted_coll.insert_one( { "_id": 1, "encrypted_indexed": "indexed_value", "encrypted_unindexed": "unindexed_value", } ) # Encrypt a find. res = await encrypted_coll.find_one({"encrypted_indexed": "indexed_value"}) assert res is not None assert res["encrypted_indexed"] == "indexed_value" assert res["encrypted_unindexed"] == "unindexed_value" # Find documents without decryption. unencrypted_client = MongoClient() unencrypted_coll = unencrypted_client.docs_examples.encrypted res = await unencrypted_coll.find_one({"_id": 1}) assert res is not None assert isinstance(res["encrypted_indexed"], Binary) assert isinstance(res["encrypted_unindexed"], Binary) await client_encryption.close() class MotorAWSLambdaExamples(AsyncIOTestCase): def test_shared_client(self): environ.setdefault("MONGODB_URI", "localhost") AsyncIOMotorClient = lambda *args, **kwargs: self.asyncio_client( *args, **kwargs, set_loop=False ) # Start AWS Lambda Example 1 import asyncio import os event_loop = asyncio.new_event_loop() client = AsyncIOMotorClient(host=os.environ["MONGODB_URI"]) async def async_handler(event, context): return await client.db.command("ping") def lambda_handler(event, context): return event_loop.run_until_complete(async_handler(event, context)) # End AWS Lambda Example 1 lambda_handler("event", {}) lambda_handler("event", {}) lambda_handler("event", {}) t = Thread(target=lambda_handler, args=("event", {})) t.start() t.join() @unittest.skip("This test needs to be run with valid IAM credentials.") def test_IAM_auth(self): environ.setdefault("MONGODB_URI", "localhost") # Start AWS Lambda Example 2 import asyncio import os from motor.motor_asyncio import AsyncIOMotorClient event_loop = asyncio.new_event_loop() asyncio.set_event_loop(event_loop) client = AsyncIOMotorClient( host=os.environ["MONGODB_URI"], authSource="$external", authMechanism="MONGODB-AWS", ) async def async_handler(event, context): return await client.db.command("ping") def lambda_handler(event, context): return event_loop.run_until_complete(async_handler(event, context)) # End AWS Lambda Example 2 motor-3.4.0/test/certificates/000077500000000000000000000000001460060525600163065ustar00rootroot00000000000000motor-3.4.0/test/certificates/ca.pem000066400000000000000000000023711460060525600173770ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDfzCCAmegAwIBAgIDB1MGMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIwMjMxMVoXDTM5MDUyMjIwMjMxMVoweTEb MBkGA1UEAxMSRHJpdmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAw DgYDVQQKEwdNb25nb0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQI EwhOZXcgWW9yazELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQCl7VN+WsQfHlwapcOpTLZVoeMAl1LTbWTFuXSAavIyy0W1Ytky1UP/ bxCSW0mSWwCgqoJ5aXbAvrNRp6ArWu3LsTQIEcD3pEdrFIVQhYzWUs9fXqPyI9k+ QNNQ+MRFKeGteTPYwF2eVEtPzUHU5ws3+OKp1m6MCLkwAG3RBFUAfddUnLvGoZiT pd8/eNabhgHvdrCw+tYFCWvSjz7SluEVievpQehrSEPKe8DxJq/IM3tSl3tdylzT zeiKNO7c7LuQrgjAfrZl7n2SriHIlNmqiDR/kdd8+TxBuxjFlcf2WyHCO3lIcIgH KXTlhUCg50KfHaxHu05Qw0x8869yIzqbAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8w DQYJKoZIhvcNAQELBQADggEBAEHuhTL8KQZcKCTSJbYA9MgZj7U32arMGBbc1hiq VBREwvdVz4+9tIyWMzN9R/YCKmUTnCq8z3wTlC8kBtxYn/l4Tj8nJYcgLJjQ0Fwe gT564CmvkUat8uXPz6olOCdwkMpJ9Sj62i0mpgXJdBfxKQ6TZ9yGz6m3jannjZpN LchB7xSAEWtqUgvNusq0dApJsf4n7jZ+oBZVaQw2+tzaMfaLqHgMwcu1FzA8UKCD sxCgIsZUs8DdxaD418Ot6nPfheOTqe24n+TTa+Z6O0W0QtnofJBx7tmAo1aEc57i 77s89pfwIJetpIlhzNSMKurCAocFCJMJLAASJFuu6dyDvPo= -----END CERTIFICATE----- motor-3.4.0/test/certificates/client.pem000066400000000000000000000056141460060525600202750ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAsNS8UEuin7/K29jXfIOLpIoh1jEyWVqxiie2Onx7uJJKcoKo khA3XeUnVN0k6X5MwYWcN52xcns7LYtyt06nRpTG2/emoV44w9uKTuHsvUbiOwSV m/ToKQQ4FUFZoqorXH+ZmJuIpJNfoW+3CkE1vEDCIecIq6BNg5ySsPtvSuSJHGjp mc7/5ZUDvFE2aJ8QbJU3Ws0HXiEb6ymi048LlzEL2VKX3w6mqqh+7dcZGAy7qYk2 5FZ9ktKvCeQau7mTyU1hsPrKFiKtMN8Q2ZAItX13asw5/IeSTq2LgLFHlbj5Kpq4 GmLdNCshzH5X7Ew3IYM8EHmsX8dmD6mhv7vpVwIDAQABAoIBABOdpb4qhcG+3twA c/cGCKmaASLnljQ/UU6IFTjrsjXJVKTbRaPeVKX/05sgZQXZ0t3s2mV5AsQ2U1w8 Cd+3w+qaemzQThW8hAOGCROzEDX29QWi/o2sX0ydgTMqaq0Wv3SlWv6I0mGfT45y /BURIsrdTCvCmz2erLqa1dL4MWJXRFjT9UTs5twlecIOM2IHKoGGagFhymRK4kDe wTRC9fpfoAgyfus3pCO/wi/F8yKGPDEwY+zgkhrJQ+kSeki7oKdGD1H540vB8gRt EIqssE0Y6rEYf97WssQlxJgvoJBDSftOijS6mwvoasDUwfFqyyPiirawXWWhHXkc DjIi/XECgYEA5xfjilw9YyM2UGQNESbNNunPcj7gDZbN347xJwmYmi9AUdPLt9xN 3XaMqqR22k1DUOxC/5hH0uiXir7mDfqmC+XS/ic/VOsa3CDWejkEnyGLiwSHY502 wD/xWgHwUiGVAG9HY64vnDGm6L3KGXA2oqxanL4V0+0+Ht49pZ16i8sCgYEAw+Ox CHGtpkzjCP/z8xr+1VTSdpc/4CP2HONnYopcn48KfQnf7Nale69/1kZpypJlvQSG eeA3jMGigNJEkb8/kaVoRLCisXcwLc0XIfCTeiK6FS0Ka30D/84Qm8UsHxRdpGkM kYITAa2r64tgRL8as4/ukeXBKE+oOhX43LeEfyUCgYBkf7IX2Ndlhsm3GlvIarxy NipeP9PGdR/hKlPbq0OvQf9R1q7QrcE7H7Q6/b0mYNV2mtjkOQB7S2WkFDMOP0P5 BqDEoKLdNkV/F9TOYH+PCNKbyYNrodJOt0Ap6Y/u1+Xpw3sjcXwJDFrO+sKqX2+T PStG4S+y84jBedsLbDoAEwKBgQCTz7/KC11o2yOFqv09N+WKvBKDgeWlD/2qFr3w UU9K5viXGVhqshz0k5z25vL09Drowf1nAZVpFMO2SPOMtq8VC6b+Dfr1xmYIaXVH Gu1tf77CM9Zk/VSDNc66e7GrUgbHBK2DLo+A+Ld9aRIfTcSsMbNnS+LQtCrQibvb cG7+MQKBgQCY11oMT2dUekoZEyW4no7W5D74lR8ztMjp/fWWTDo/AZGPBY6cZoZF IICrzYtDT/5BzB0Jh1f4O9ZQkm5+OvlFbmoZoSbMzHL3oJCBOY5K0/kdGXL46WWh IRJSYakNU6VIS7SjDpKgm9D8befQqZeoSggSjIIULIiAtYgS80vmGA== -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIIDgzCCAmugAwIBAgIDAxOUMA0GCSqGSIb3DQEBCwUAMHkxGzAZBgNVBAMTEkRy aXZlcnMgVGVzdGluZyBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9u Z29EQjEWMBQGA1UEBxMNTmV3IFlvcmsgQ2l0eTERMA8GA1UECBMITmV3IFlvcmsx CzAJBgNVBAYTAlVTMB4XDTE5MDUyMjIzNTU1NFoXDTM5MDUyMjIzNTU1NFowaTEP MA0GA1UEAxMGY2xpZW50MRAwDgYDVQQLEwdEcml2ZXJzMQwwCgYDVQQKEwNNREIx FjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYD VQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALDUvFBLop+/ ytvY13yDi6SKIdYxMllasYontjp8e7iSSnKCqJIQN13lJ1TdJOl+TMGFnDedsXJ7 Oy2LcrdOp0aUxtv3pqFeOMPbik7h7L1G4jsElZv06CkEOBVBWaKqK1x/mZibiKST X6FvtwpBNbxAwiHnCKugTYOckrD7b0rkiRxo6ZnO/+WVA7xRNmifEGyVN1rNB14h G+spotOPC5cxC9lSl98Opqqofu3XGRgMu6mJNuRWfZLSrwnkGru5k8lNYbD6yhYi rTDfENmQCLV9d2rMOfyHkk6ti4CxR5W4+SqauBpi3TQrIcx+V+xMNyGDPBB5rF/H Zg+pob+76VcCAwEAAaMkMCIwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUF BwMCMA0GCSqGSIb3DQEBCwUAA4IBAQAqRcLAGvYMaGYOV4HJTzNotT2qE0I9THNQ wOV1fBg69x6SrUQTQLjJEptpOA288Wue6Jt3H+p5qAGV5GbXjzN/yjCoItggSKxG Xg7279nz6/C5faoIKRjpS9R+MsJGlttP9nUzdSxrHvvqm62OuSVFjjETxD39DupE YPFQoHOxdFTtBQlc/zIKxVdd20rs1xJeeU2/L7jtRBSPuR/Sk8zot7G2/dQHX49y kHrq8qz12kj1T6XDXf8KZawFywXaz0/Ur+fUYKmkVk1T0JZaNtF4sKqDeNE4zcns p3xLVDSl1Q5Gwj7bgph9o4Hxs9izPwiqjmNaSjPimGYZ399zcurY -----END CERTIFICATE----- motor-3.4.0/test/certificates/server.pem000066400000000000000000000056411460060525600203250ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAhNrB0E6GY/kFSd8/vNpu/t952tbnOsD5drV0XPvmuy7SgKDY a/S+xb/jPnlZKKehdBnH7qP/gYbv34ZykzcDFZscjPLiGc2cRGP+NQCSFK0d2/7d y15zSD3zhj14G8+MkpAejTU+0/qFNZMc5neDvGanTe0+8aWa0DXssM0MuTxIv7j6 CtsMWeqLLofN7a1Kw2UvmieCHfHMuA/08pJwRnV/+5T9WONBPJja2ZQRrG1BjpI4 81zSPUZesIqi8yDlExdvgNaRZIEHi/njREqwVgJOZomUY57zmKypiMzbz48dDTsV gUStxrEqbaP+BEjQYPX5+QQk4GdMjkLf52LR6QIDAQABAoIBAHSs+hHLJNOf2zkp S3y8CUblVMsQeTpsR6otaehPgi9Zy50TpX4KD5D0GMrBH8BIl86y5Zd7h+VlcDzK gs0vPxI2izhuBovKuzaE6rf5rFFkSBjxGDCG3o/PeJOoYFdsS3RcBbjVzju0hFCs xnDQ/Wz0anJRrTnjyraY5SnQqx/xuhLXkj/lwWoWjP2bUqDprnuLOj16soNu60Um JziWbmWx9ty0wohkI/8DPBl9FjSniEEUi9pnZXPElFN6kwPkgdfT5rY/TkMH4lsu ozOUc5xgwlkT6kVjXHcs3fleuT/mOfVXLPgNms85JKLucfd6KiV7jYZkT/bXIjQ+ 7CZEn0ECgYEA5QiKZgsfJjWvZpt21V/i7dPje2xdwHtZ8F9NjX7ZUFA7mUPxUlwe GiXxmy6RGzNdnLOto4SF0/7ebuF3koO77oLup5a2etL+y/AnNAufbu4S5D72sbiz wdLzr3d5JQ12xeaEH6kQNk2SD5/ShctdS6GmTgQPiJIgH0MIdi9F3v0CgYEAlH84 hMWcC+5b4hHUEexeNkT8kCXwHVcUjGRaYFdSHgovvWllApZDHSWZ+vRcMBdlhNPu 09Btxo99cjOZwGYJyt20QQLGc/ZyiOF4ximQzabTeFgLkTH3Ox6Mh2Rx9yIruYoX nE3UfMDkYELanEJUv0zenKpZHw7tTt5yXXSlEF0CgYBSsEOvVcKYO/eoluZPYQAA F2jgzZ4HeUFebDoGpM52lZD+463Dq2hezmYtPaG77U6V3bUJ/TWH9VN/Or290vvN v83ECcC2FWlSXdD5lFyqYx/E8gqE3YdgqfW62uqM+xBvoKsA9zvYLydVpsEN9v8m 6CSvs/2btA4O21e5u5WBTQKBgGtAb6vFpe0gHRDs24SOeYUs0lWycPhf+qFjobrP lqnHpa9iPeheat7UV6BfeW3qmBIVl/s4IPE2ld4z0qqZiB0Tf6ssu/TpXNPsNXS6 dLFz+myC+ufFdNEoQUtQitd5wKbjTCZCOGRaVRgJcSdG6Tq55Fa22mOKPm+mTmed ZdKpAoGAFsTYBAHPxs8nzkCJCl7KLa4/zgbgywO6EcQgA7tfelB8bc8vcAMG5o+8 YqAfwxrzhVSVbJx0fibTARXROmbh2pn010l2wj3+qUajM8NiskCPFbSjGy7HSUze P8Kt1uMDJdj55gATzn44au31QBioZY2zXleorxF21cr+BZCJgfA= -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIIDlTCCAn2gAwIBAgICdxUwDQYJKoZIhvcNAQELBQAweTEbMBkGA1UEAxMSRHJp dmVycyBUZXN0aW5nIENBMRAwDgYDVQQLEwdEcml2ZXJzMRAwDgYDVQQKEwdNb25n b0RCMRYwFAYDVQQHEw1OZXcgWW9yayBDaXR5MREwDwYDVQQIEwhOZXcgWW9yazEL MAkGA1UEBhMCVVMwHhcNMTkwNTIyMjIzMjU2WhcNMzkwNTIyMjIzMjU2WjBwMRIw EAYDVQQDEwlsb2NhbGhvc3QxEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01v bmdvREIxFjAUBgNVBAcTDU5ldyBZb3JrIENpdHkxETAPBgNVBAgTCE5ldyBZb3Jr MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAITa wdBOhmP5BUnfP7zabv7fedrW5zrA+Xa1dFz75rsu0oCg2Gv0vsW/4z55WSinoXQZ x+6j/4GG79+GcpM3AxWbHIzy4hnNnERj/jUAkhStHdv+3ctec0g984Y9eBvPjJKQ Ho01PtP6hTWTHOZ3g7xmp03tPvGlmtA17LDNDLk8SL+4+grbDFnqiy6Hze2tSsNl L5ongh3xzLgP9PKScEZ1f/uU/VjjQTyY2tmUEaxtQY6SOPNc0j1GXrCKovMg5RMX b4DWkWSBB4v540RKsFYCTmaJlGOe85isqYjM28+PHQ07FYFErcaxKm2j/gRI0GD1 +fkEJOBnTI5C3+di0ekCAwEAAaMwMC4wLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/ AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBol8+YH7MA HwnIh7KcJ8h87GkCWsjOJCDJWiYBJArQ0MmgDO0qdx+QEtvLMn3XNtP05ZfK0WyX or4cWllAkMFYaFbyB2hYazlD1UAAG+22Rku0UP6pJMLbWe6pnqzx+RL68FYdbZhN fCW2xiiKsdPoo2VEY7eeZKrNr/0RFE5EKXgzmobpTBQT1Dl3Ve4aWLoTy9INlQ/g z40qS7oq1PjjPLgxINhf4ncJqfmRXugYTOnyFiVXLZTys5Pb9SMKdToGl3NTYWLL 2AZdjr6bKtT+WtXyHqO0cQ8CkAW0M6VOlMluACllcJxfrtdlQS2S4lUIj76QKBdZ khBHXq/b8MFX -----END CERTIFICATE----- motor-3.4.0/test/mypy_fails/000077500000000000000000000000001460060525600160155ustar00rootroot00000000000000motor-3.4.0/test/mypy_fails/insert_many_dict.py000066400000000000000000000005471460060525600217300ustar00rootroot00000000000000import asyncio from motor.core import AgnosticClient async def _main(): client: AgnosticClient = AgnosticClient() await client.test.test.insert_many( {"a": 1} ) # error: Dict entry 0 has incompatible type "str": "int"; expected "Mapping[str, Any]": "int" loop = asyncio.get_event_loop() loop.run_until_complete(_main()) loop.close() motor-3.4.0/test/mypy_fails/insert_one_list.py000066400000000000000000000006111460060525600215650ustar00rootroot00000000000000import asyncio from motor.core import AgnosticClient async def _main(): client: AgnosticClient = AgnosticClient() client.test.test.insert_one( [{}] ) # error: Argument 1 to "insert_one" of "Collection" has incompatible type "List[Dict[, ]]"; expected "Mapping[str, Any]" loop = asyncio.get_event_loop() loop.run_until_complete(_main()) loop.close() motor-3.4.0/test/mypy_fails/raw_bson_document.py000066400000000000000000000014151460060525600221000ustar00rootroot00000000000000import asyncio from bson.raw_bson import RawBSONDocument from motor.core import AgnosticClient async def _main(): client = AgnosticClient(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] client.test.test.insert_one( [{}] ) # error: Argument 1 to "insert_one" of "Collection" has incompatible type "List[Dict[, ]]"; expected "Mapping[str, Any]" loop = asyncio.get_event_loop() loop.run_until_complete(_main()) loop.close() motor-3.4.0/test/test_environment.py000066400000000000000000000303651460060525600176250ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Discover environment and server configuration, initialize PyMongo client.""" import os import socket import sys from functools import wraps from test.utils import create_user from test.version import Version from typing import TYPE_CHECKING from unittest import SkipTest import pymongo.errors # mypy: ignore-errors HAVE_SSL = True try: import ssl except ImportError: HAVE_SSL = False ssl = None HAVE_TORNADO = True try: import tornado except ImportError: HAVE_TORNADO = False tornado = None HAVE_ASYNCIO = True try: import asyncio except ImportError: HAVE_ASYNCIO = False asyncio = None HAVE_AIOHTTP = True try: # Remove when we drop support for 3.7 if TYPE_CHECKING: HAVE_AIOHTTP = False aiohttp = None else: import aiohttp except ImportError: HAVE_AIOHTTP = False aiohttp = None HAVE_PYMONGOCRYPT = True try: import pymongocrypt # noqa: F401 except ImportError: HAVE_PYMONGOCRYPT = False # Copied from PyMongo. def partition_node(node): """Split a host:port string into (host, int(port)) pair.""" host = node port = 27017 idx = node.rfind(":") if idx != -1: host, port = node[:idx], int(node[idx + 1 :]) if host.startswith("["): host = host[1:-1] return host, port def connected(client): """Convenience, wait for a new PyMongo MongoClient to connect.""" client.admin.command("ping") # Force connection. return client # If these are set to the empty string, substitute None. db_user = os.environ.get("DB_USER") or None db_password = os.environ.get("DB_PASSWORD") or None CERT_PATH = os.environ.get( "CERT_DIR", os.path.join(os.path.dirname(os.path.realpath(__file__)), "certificates") ) CLIENT_PEM = os.path.join(CERT_PATH, "client.pem") CA_PEM = os.path.join(CERT_PATH, "ca.pem") MONGODB_X509_USERNAME = "CN=client,OU=kerneluser,O=10Gen,L=New York City,ST=New York,C=US" def is_server_resolvable(): """Returns True if 'server' is resolvable.""" socket_timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(1) try: socket.gethostbyname("server") return True except OSError: return False finally: socket.setdefaulttimeout(socket_timeout) class TestEnvironment: __test__ = False def __init__(self): self.initialized = False self.host = None self.port = None self.mongod_started_with_ssl = False self.mongod_validates_client_cert = False self.server_is_resolvable = is_server_resolvable() self.sync_cx = None self.is_standalone = False self.is_mongos = False self.is_replica_set = False self.rs_name = None self.w = 1 self.hosts = None self.arbiters = None self.primary = None self.secondaries = None self.v8 = False self.auth = False self.uri = None self.rs_uri = None self.version = None self.sessions_enabled = False self.fake_hostname_uri = None self.server_status = None def setup(self): assert not self.initialized self.setup_sync_cx() self.setup_auth_and_uri() self.setup_version() self.setup_v8() self.server_status = self.sync_cx.admin.command("serverStatus") self.initialized = True def setup_sync_cx(self): """Get a synchronous PyMongo MongoClient and determine SSL config.""" host = os.environ.get("DB_IP", "localhost") port = int(os.environ.get("DB_PORT", 27017)) connectTimeoutMS = 100 serverSelectionTimeoutMS = 100 socketTimeoutMS = 10000 try: client = connected( pymongo.MongoClient( host, port, username=db_user, password=db_password, directConnection=True, connectTimeoutMS=connectTimeoutMS, socketTimeoutMS=socketTimeoutMS, serverSelectionTimeoutMS=serverSelectionTimeoutMS, tlsCAFile=CA_PEM, ssl=True, ) ) self.mongod_started_with_ssl = True except pymongo.errors.ServerSelectionTimeoutError: try: client = connected( pymongo.MongoClient( host, port, username=db_user, password=db_password, directConnection=True, connectTimeoutMS=connectTimeoutMS, socketTimeoutMS=socketTimeoutMS, serverSelectionTimeoutMS=serverSelectionTimeoutMS, tlsCAFile=CA_PEM, tlsCertificateKeyFile=CLIENT_PEM, ) ) self.mongod_started_with_ssl = True self.mongod_validates_client_cert = True except pymongo.errors.ServerSelectionTimeoutError: client = connected( pymongo.MongoClient( host, port, username=db_user, password=db_password, directConnection=True, connectTimeoutMS=connectTimeoutMS, socketTimeoutMS=socketTimeoutMS, serverSelectionTimeoutMS=serverSelectionTimeoutMS, ) ) response = client.admin.command("ismaster") self.sessions_enabled = "logicalSessionTimeoutMinutes" in response self.is_mongos = response.get("msg") == "isdbgrid" if "setName" in response: self.is_replica_set = True self.rs_name = str(response["setName"]) self.w = len(response["hosts"]) self.hosts = set([partition_node(h) for h in response["hosts"]]) host, port = self.primary = partition_node(response["primary"]) self.arbiters = set([partition_node(h) for h in response.get("arbiters", [])]) self.secondaries = [ partition_node(m) for m in response["hosts"] if m != self.primary and m not in self.arbiters ] elif not self.is_mongos: self.is_standalone = True # Reconnect to found primary, without short timeouts. if self.mongod_started_with_ssl: client = connected( pymongo.MongoClient( host, port, username=db_user, password=db_password, directConnection=True, tlsCAFile=CA_PEM, tlsCertificateKeyFile=CLIENT_PEM, ) ) else: client = connected( pymongo.MongoClient( host, port, username=db_user, password=db_password, directConnection=True, ssl=False, ) ) self.sync_cx = client self.host = host self.port = port def setup_auth_and_uri(self): """Set self.auth and self.uri.""" if db_user or db_password: if not (db_user and db_password): sys.stderr.write("You must set both DB_USER and DB_PASSWORD, or neither\n") sys.exit(1) self.auth = True uri_template = "mongodb://%s:%s@%s:%s/admin" self.uri = uri_template % (db_user, db_password, self.host, self.port) # If the hostname 'server' is resolvable, this URI lets us use it # to test SSL hostname validation with auth. self.fake_hostname_uri = uri_template % (db_user, db_password, "server", self.port) else: self.uri = "mongodb://%s:%s/admin" % (self.host, self.port) self.fake_hostname_uri = "mongodb://%s:%s/admin" % ("server", self.port) if self.rs_name: self.rs_uri = self.uri + "?replicaSet=" + self.rs_name def setup_version(self): """Set self.version to the server's version.""" self.version = Version.from_client(self.sync_cx) def setup_v8(self): """Determine if server is running SpiderMonkey or V8.""" if self.sync_cx.server_info().get("javascriptEngine") == "V8": self.v8 = True @property def storage_engine(self): try: return self.server_status.get("storageEngine", {}).get("name") except AttributeError: # Raised if self.server_status is None. return None def supports_transactions(self): if self.storage_engine == "mmapv1": return False if self.version.at_least(4, 1, 8): return self.is_mongos or self.is_replica_set if self.version.at_least(4, 0): return self.is_replica_set return False def require(self, condition, msg, func=None): def make_wrapper(f): @wraps(f) def wrap(*args, **kwargs): assert self.initialized if condition(): return f(*args, **kwargs) raise SkipTest(msg) return wrap if func is None: def decorate(f): return make_wrapper(f) return decorate return make_wrapper(func) def require_auth(self, func): """Run a test only if the server is started with auth.""" return self.require(lambda: self.auth, "Server must be start with auth", func=func) def require_version_min(self, *ver): """Run a test only if the server version is at least ``version``.""" other_version = Version(*ver) return self.require( lambda: self.version >= other_version, "Server version must be at least %s" % str(other_version), ) def require_version_max(self, *ver): """Run a test only if the server version is at most ``version``.""" other_version = Version(*ver) return self.require( lambda: self.version <= other_version, "Server version must be at most %s" % str(other_version), ) def require_no_standalone(self, func): """Run a test only if the client is not connected to a standalone.""" return self.require(lambda: not self.is_standalone, "Connected to a standalone", func=func) def require_replica_set(self, func): """Run a test only if the client is connected to a replica set.""" return self.require( lambda: self.is_replica_set, "Not connected to a replica set", func=func ) def require_transactions(self, func): """Run a test only if the deployment might support transactions. *Might* because this does not test the FCV. """ return self.require(self.supports_transactions, "Transactions are not supported", func=func) def require_csfle(self, func): """Run a test only if the deployment supports CSFLE.""" return self.require( lambda: HAVE_PYMONGOCRYPT and self.version >= Version(4, 2), "CSFLE requires pymongocrypt and MongoDB >=4.2", func=func, ) def create_user(self, dbname, user, pwd=None, roles=None, **kwargs): kwargs["writeConcern"] = {"w": self.w} return create_user(self.sync_cx[dbname], user, pwd, roles, **kwargs) def drop_user(self, dbname, user): self.sync_cx[dbname].command("dropUser", user, writeConcern={"w": self.w}) env = TestEnvironment() if "SKIP_ENV_SETUP" not in os.environ: env.setup() motor-3.4.0/test/test_mypy_fails.py000066400000000000000000000015621460060525600174320ustar00rootroot00000000000000import os import sys import unittest from typing import Iterable try: from mypy import api except ImportError: api = None sys.path[0:0] = [""] TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mypy_fails") def get_tests() -> Iterable[str]: for dirpath, _, filenames in os.walk(TEST_PATH): for filename in filenames: yield os.path.join(dirpath, filename) class TestMypyFails(unittest.TestCase): def ensure_mypy_fails(self, filename: str) -> None: if api is None: raise unittest.SkipTest("Mypy is not installed") stdout, stderr, exit_status = api.run([filename]) self.assertTrue(exit_status, msg=stdout) def test_mypy_failures(self) -> None: for filename in get_tests(): with self.subTest(filename=filename): self.ensure_mypy_fails(filename) motor-3.4.0/test/test_typing.py000066400000000000000000000250601460060525600165670ustar00rootroot00000000000000# 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. """Test that each file in mypy_fails/ actually fails mypy, and test some sample client code that uses Motor typings. """ import unittest from test.asyncio_tests import AsyncIOTestCase, asyncio_test from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, TypeVar, Union, cast from bson import CodecOptions from bson.raw_bson import RawBSONDocument from bson.son import SON from pymongo.operations import DeleteOne, InsertOne, ReplaceOne from pymongo.read_preferences import ReadPreference from motor.motor_asyncio import AsyncIOMotorClient try: from bson import ObjectId from typing_extensions import NotRequired, TypedDict class Movie(TypedDict): name: str year: int class MovieWithId(TypedDict): _id: ObjectId name: str year: int class ImplicitMovie(TypedDict): _id: NotRequired[ObjectId] name: str year: int except ImportError: Movie = dict # type:ignore[misc,assignment] ImplicitMovie = dict # type: ignore[assignment,misc] MovieWithId = dict # type: ignore[assignment,misc] TypedDict = None NotRequired = None # type: ignore[assignment] FuncT = TypeVar("FuncT", bound=Callable[..., Any]) def only_type_check(func: FuncT) -> FuncT: def inner(*args: Any, **kwargs: Any) -> Any: if not TYPE_CHECKING: raise unittest.SkipTest("Used for Type Checking Only") func(*args, **kwargs) return cast(FuncT, inner) class TestMotor(AsyncIOTestCase): cx: AsyncIOMotorClient @asyncio_test # type:ignore[misc] async def test_insert_find(self) -> None: doc = {"my": "doc"} coll = self.collection coll2 = self.cx.test.test2 result = await coll.insert_one(doc) self.assertEqual(result.inserted_id, doc["_id"]) retrieved = await coll.find_one({"_id": doc["_id"]}) if retrieved: # Documents returned from find are mutable. retrieved["new_field"] = 1 result2 = await coll2.insert_one(retrieved) self.assertEqual(result2.inserted_id, result.inserted_id) @asyncio_test # type:ignore[misc] async def test_cursor_to_list(self) -> None: await self.collection.insert_one({}) cursor = self.collection.find() docs = await cursor.to_list(None) self.assertTrue(docs) @asyncio_test # type:ignore[misc] async def test_get_collection(self) -> None: coll = self.db.get_collection("test_collection") self.assertEqual(coll.name, "test_collection") @asyncio_test # type:ignore[misc] async def test_get_database(self) -> None: db1 = self.cx.get_database("test_database") db2 = self.cx["test_database"] self.assertEqual(db1.client, db2.client) @asyncio_test # type:ignore[misc] async def test_find_one(self) -> None: c: AsyncIOMotorClient[Movie] = self.asyncio_client() coll = c[self.db.name]["movies"] await coll.insert_one(Movie(name="American Graffiti", year=1973)) result = await coll.find_one({}) assert result is not None self.assertEqual(result["year"], 1973) @only_type_check @asyncio_test # type:ignore[misc] async def test_bulk_write(self) -> None: await self.collection.insert_one({}) coll = self.collection requests: List[InsertOne[Movie]] = [InsertOne(Movie(name="American Graffiti", year=1973))] result_one = await coll.bulk_write(requests) self.assertTrue(result_one.acknowledged) new_requests: List[Union[InsertOne[Movie], ReplaceOne[Movie]]] = [] input_list: List[Union[InsertOne[Movie], ReplaceOne[Movie]]] = [ InsertOne(Movie(name="American Graffiti", year=1973)), ReplaceOne({}, Movie(name="American Graffiti", year=1973)), ] for i in input_list: new_requests.append(i) result_two = await coll.bulk_write(new_requests) self.assertTrue(result_two.acknowledged) # Because ReplaceOne is not generic, type checking is not enforced for ReplaceOne in the first example. @only_type_check @asyncio_test # type:ignore[misc] async def test_bulk_write_heterogeneous(self) -> None: coll = self.collection requests: List[Union[InsertOne[Movie], ReplaceOne, DeleteOne]] = [ InsertOne(Movie(name="American Graffiti", year=1973)), ReplaceOne({}, {"name": "American Graffiti", "year": "WRONG_TYPE"}), DeleteOne({}), ] result_one = await coll.bulk_write(requests) self.assertTrue(result_one.acknowledged) requests_two: List[Union[InsertOne[Movie], ReplaceOne[Movie], DeleteOne]] = [ InsertOne(Movie(name="American Graffiti", year=1973)), ReplaceOne( {}, {"name": "American Graffiti", "year": "WRONG_TYPE"}, # type:ignore[typeddict-item] ), DeleteOne({}), ] result_two = await coll.bulk_write(requests_two) self.assertTrue(result_two.acknowledged) @asyncio_test # type:ignore[misc] async def test_command(self) -> None: result: Dict = await self.cx.admin.command("ping") result.items() @asyncio_test # type:ignore[misc] async def test_list_collections(self) -> None: cursor = await self.cx.test.list_collections() value: Mapping[str, Any] = await cursor.next() value.items() @asyncio_test # type:ignore[misc] async def test_list_databases(self) -> None: cursor = await self.cx.list_databases() value: Mapping[str, Any] = await cursor.next() value.items() @asyncio_test # type:ignore[misc] async def test_default_document_type(self) -> None: client = self.asyncio_client() self.addCleanup(client.close) 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 retrieved["a"] = 1 @asyncio_test # type:ignore[misc] async def test_aggregate_pipeline(self) -> None: coll3 = self.cx.test.test3 await coll3.insert_many( [ {"x": 1, "tags": ["dog", "cat"]}, {"x": 2, "tags": ["cat"]}, {"x": 2, "tags": ["mouse", "cat", "dog"]}, {"x": 3, "tags": []}, ] ) class mydict(Dict[str, Any]): pass result = coll3.aggregate( [ mydict({"$unwind": "$tags"}), {"$group": {"_id": "$tags", "count": {"$sum": 1}}}, {"$sort": SON([("count", -1), ("_id", -1)])}, ] ) self.assertTrue(len([doc async for doc in result])) @asyncio_test # type:ignore[misc] async def test_with_transaction(self) -> None: async def execute_transaction(session: Any) -> None: pass async with await self.cx.start_session() as session: await session.with_transaction( execute_transaction, read_preference=ReadPreference.PRIMARY ) class TestDocumentType(AsyncIOTestCase): @only_type_check def test_typeddict_explicit_document_type(self) -> None: out = MovieWithId(_id=ObjectId(), name="THX-1138", year=1971) assert out is not None # This should fail because the output is a Movie. assert out["foo"] # type:ignore[typeddict-item] assert bool(out["_id"]) # This should work the same as the test above, but this time using NotRequired to allow # automatic insertion of the _id field by insert_one. @only_type_check def test_typeddict_not_required_document_type(self) -> None: out = ImplicitMovie(name="THX-1138", year=1971) assert out is not None # This should fail because the output is a Movie. assert out["foo"] # type:ignore[typeddict-item] # pyright gives reportTypedDictNotRequiredAccess for the following: assert bool(out["_id"]) @only_type_check def test_typeddict_empty_document_type(self) -> None: out = Movie(name="THX-1138", year=1971) assert out is not None # This should fail because the output is a Movie. assert out["foo"] # type:ignore[typeddict-item] # This should fail because _id is not included in our TypedDict definition. assert out["_id"] # type:ignore[typeddict-item] class TestCommandDocumentType(AsyncIOTestCase): @only_type_check async def test_default(self) -> None: client: AsyncIOMotorClient = AsyncIOMotorClient() result: Dict = await client.admin.command("ping") result["a"] = 1 @only_type_check async def test_explicit_document_type(self) -> None: client: AsyncIOMotorClient = AsyncIOMotorClient() codec_options: CodecOptions[Dict[str, Any]] = CodecOptions() result = await client.admin.command("ping", codec_options=codec_options) result["a"] = 1 @only_type_check async def test_typeddict_document_type(self) -> None: client: AsyncIOMotorClient = AsyncIOMotorClient() codec_options: CodecOptions[Movie] = CodecOptions() result = await client.admin.command("ping", codec_options=codec_options) assert result["year"] == 1 assert result["name"] == "a" @only_type_check async def test_raw_bson_document_type(self) -> None: client: AsyncIOMotorClient = AsyncIOMotorClient() codec_options = CodecOptions(RawBSONDocument) result = await client.admin.command( "ping", codec_options=codec_options ) # Fix once @overload for command works assert len(result.raw) > 0 @only_type_check async def test_son_document_type(self) -> None: client: AsyncIOMotorClient[SON[str, Any]] = AsyncIOMotorClient(document_class=SON[str, Any]) codec_options = CodecOptions(SON[str, Any]) result = await client.admin.command("ping", codec_options=codec_options) result["a"] = 1 if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/000077500000000000000000000000001460060525600165315ustar00rootroot00000000000000motor-3.4.0/test/tornado_tests/__init__.py000066400000000000000000000112151460060525600206420ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Utilities for testing Motor with Tornado.""" import concurrent.futures import functools from test.test_environment import CA_PEM, CLIENT_PEM, env from test.version import Version from unittest import SkipTest from bson import SON from mockupdb import MockupDB from tornado import testing import motor async def get_command_line(client): command_line = await client.admin.command("getCmdLineOpts") assert command_line["ok"] == 1, "getCmdLineOpts() failed" return command_line async def server_is_mongos(client): ismaster_response = await client.admin.command("ismaster") return ismaster_response.get("msg") == "isdbgrid" async def skip_if_mongos(client): is_mongos = await server_is_mongos(client) if is_mongos: raise SkipTest("connected to mongos") async def remove_all_users(db): await db.command({"dropAllUsersFromDatabase": 1}) class MotorTest(testing.AsyncTestCase): longMessage = True # Used by unittest.TestCase ssl = False # If True, connect with SSL, skip if mongod isn't SSL def setUp(self): super().setUp() if self.ssl and not env.mongod_started_with_ssl: raise SkipTest("mongod doesn't support SSL, or is down") self.cx = self.motor_client() self.db = self.cx.motor_test self.collection = self.db.test_collection async def make_test_data(self): await self.collection.delete_many({}) await self.collection.insert_many([{"_id": i} for i in range(200)]) make_test_data.__test__ = False async def set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) cmd.update(command_args) await client.admin.command(cmd) def get_client_kwargs(self, **kwargs): if env.mongod_started_with_ssl: kwargs.setdefault("tlsCAFile", CA_PEM) kwargs.setdefault("tlsCertificateKeyFile", CLIENT_PEM) kwargs.setdefault("tls", env.mongod_started_with_ssl) kwargs.setdefault("io_loop", self.io_loop) return kwargs def motor_client(self, uri=None, *args, **kwargs): """Get a MotorClient. Ignores self.ssl, you must pass 'ssl' argument. You'll probably need to close the client to avoid file-descriptor problems after AsyncTestCase calls self.io_loop.close(all_fds=True). """ return motor.MotorClient(uri or env.uri, *args, **self.get_client_kwargs(**kwargs)) def motor_rsc(self, uri=None, *args, **kwargs): """Get an open MotorClient for replica set. Ignores self.ssl, you must pass 'ssl' argument. """ return motor.MotorClient(uri or env.rs_uri, *args, **self.get_client_kwargs(**kwargs)) def tearDown(self): env.sync_cx.motor_test.test_collection.delete_many({}) if hasattr(self, "cx"): self.cx.close() super().tearDown() class MotorReplicaSetTestBase(MotorTest): def setUp(self): super().setUp() if not env.is_replica_set: raise SkipTest("Not connected to a replica set") self.rsc = self.motor_rsc() class MotorMockServerTest(MotorTest): executor = concurrent.futures.ThreadPoolExecutor(1) def server(self, *args, **kwargs): server = MockupDB(*args, **kwargs) server.run() self.addCleanup(server.stop) return server def client_server(self, *args, **kwargs): server = self.server(*args, **kwargs) client = motor.motor_tornado.MotorClient(server.uri, io_loop=self.io_loop) self.addCleanup(client.close) return client, server async def run_thread(self, fn, *args, **kwargs): return await self.io_loop.run_in_executor(None, functools.partial(fn, *args, **kwargs)) class AsyncVersion(Version): """Version class that can be instantiated with an async client from within a coroutine.""" @classmethod async def from_client(cls, client): info = await client.server_info() if "versionArray" in info: return cls.from_version_array(info["versionArray"]) return cls.from_string(info["version"]) motor-3.4.0/test/tornado_tests/test_motor_auth.py000066400000000000000000000061651460060525600223330ustar00rootroot00000000000000# Copyright 2018-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 test.test_environment import env from test.tornado_tests import MotorTest from pymongo.errors import OperationFailure from tornado.testing import gen_test """Test Motor, an asynchronous driver for MongoDB and Tornado.""" class MotorAuthTest(MotorTest): @env.require_auth @env.require_version_min(4, 0) def setUp(self): super().setUp() self._reset() def tearDown(self): self._reset() def _reset(self): env.sync_cx.scramtestdb.command("dropAllUsersFromDatabase") env.sync_cx.drop_database("scramtestdb") @gen_test async def test_scram(self): env.create_user("scramtestdb", "sha1", "pwd", roles=["dbOwner"], mechanisms=["SCRAM-SHA-1"]) env.create_user( "scramtestdb", "sha256", "pwd", roles=["dbOwner"], mechanisms=["SCRAM-SHA-256"] ) env.create_user( "scramtestdb", "both", "pwd", roles=["dbOwner"], mechanisms=["SCRAM-SHA-1", "SCRAM-SHA-256"], ) for user, mechanism, should_work in [ ("sha1", "SCRAM-SHA-1", True), ("sha1", "SCRAM-SHA-256", False), ("sha256", "SCRAM-SHA-256", True), ("sha256", "SCRAM-SHA-1", False), ("both", "SCRAM-SHA-1", True), ("both", "SCRAM-SHA-256", True), ]: client = self.motor_client( username=user, password="pwd", authsource="scramtestdb", authmechanism=mechanism ) if should_work: await client.scramtestdb.collection.insert_one({}) else: with self.assertRaises(OperationFailure): await client.scramtestdb.collection.insert_one({}) # No mechanism specified, always works. for user in ("sha1", "sha256", "both"): client = self.motor_client(username=user, password="pwd", authsource="scramtestdb") await client.scramtestdb.collection.insert_one({}) @gen_test async def test_saslprep(self): # Use Roman numeral for password, normalized by SASLprep to ASCII "IV", # see RFC 4013. MongoDB SASL mech normalizes password only, not user. env.create_user( "scramtestdb", "saslprep-test-user", "\u2163", roles=["dbOwner"], mechanisms=["SCRAM-SHA-256"], ) client = self.motor_client( username="saslprep-test-user", password="IV", authsource="scramtestdb" ) await client.scramtestdb.collection.insert_one({}) motor-3.4.0/test/tornado_tests/test_motor_await.py000066400000000000000000000166221460060525600224760ustar00rootroot00000000000000# Copyright 2013-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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 warnings from test import env import bson """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import test from test.tornado_tests import MotorTest from tornado.testing import gen_test from motor.motor_tornado import MotorClientSession, MotorGridFSBucket class MotorTestAwait(MotorTest): @gen_test async def test_to_list(self): collection = self.collection await collection.delete_many({}) results = await collection.find().sort("_id").to_list(length=None) self.assertEqual([], results) docs = [{"_id": 1}, {"_id": 2}] await collection.insert_many(docs) cursor = collection.find().sort("_id") results = await cursor.to_list(length=None) self.assertEqual(docs, results) results = await cursor.to_list(length=None) self.assertEqual([], results) @gen_test async def test_iter_cursor(self): collection = self.collection await collection.delete_many({}) for n_docs in 0, 1, 2, 10: if n_docs: docs = [{"_id": i} for i in range(n_docs)] await collection.insert_many(docs) # Force extra batches to test iteration. j = 0 async for doc in collection.find().sort("_id").batch_size(3): self.assertEqual(j, doc["_id"]) j += 1 self.assertEqual(j, n_docs) j = 0 raw_cursor = collection.find_raw_batches().sort("_id").batch_size(3) async for batch in raw_cursor: j += len(bson.decode_all(batch)) self.assertEqual(j, n_docs) await collection.delete_many({}) @gen_test async def test_iter_aggregate(self): collection = self.collection await collection.delete_many({}) pipeline = [{"$sort": {"_id": 1}}] # Empty iterator. async for _ in collection.aggregate(pipeline): self.fail() for n_docs in 1, 2, 10: if n_docs: docs = [{"_id": i} for i in range(n_docs)] await collection.insert_many(docs) # Force extra batches to test iteration. j = 0 cursor = collection.aggregate(pipeline).batch_size(3) async for doc in cursor: self.assertEqual(j, doc["_id"]) j += 1 self.assertEqual(j, n_docs) j = 0 raw = collection.aggregate_raw_batches(pipeline).batch_size(3) async for batch in raw: j += len(bson.decode_all(batch)) self.assertEqual(j, n_docs) await collection.delete_many({}) @gen_test async def test_iter_gridfs(self): gfs = MotorGridFSBucket(self.db) async def cleanup(): await self.db.fs.files.delete_many({}) await self.db.fs.chunks.delete_many({}) await cleanup() # Empty iterator. async for _ in gfs.find({"_id": 1}): self.fail() data = b"data" for n_files in 1, 2, 10: for _ in range(n_files): async with gfs.open_upload_stream(filename="filename") as f: await f.write(data) # Force extra batches to test iteration. j = 0 async for _ in gfs.find({"filename": "filename"}).batch_size(3): j += 1 self.assertEqual(j, n_files) await cleanup() await gfs.upload_from_stream_with_id(1, "filename", source=data, chunk_size_bytes=1) cursor = gfs.find({"_id": 1}) await cursor.fetch_next gout = cursor.next_object() chunks = [] async for chunk in gout: chunks.append(chunk) self.assertEqual(len(chunks), len(data)) self.assertEqual(b"".join(chunks), data) @gen_test async def test_stream_to_handler(self): fs = MotorGridFSBucket(self.db) content_length = 1000 await fs.delete(1) await fs.upload_from_stream_with_id(1, "filename", source=b"a" * content_length) gridout = await fs.open_download_stream(1) handler = test.MockRequestHandler() await gridout.stream_to_handler(handler) self.assertEqual(content_length, handler.n_written) await fs.delete(1) @gen_test async def test_cursor_iter(self): # Have we handled the async iterator change in Python 3.5.2?: # python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions with warnings.catch_warnings(record=True) as w: async for _ in self.collection.find(): pass if w: self.fail(w[0].message) @gen_test async def test_list_indexes(self): await self.collection.drop() await self.collection.create_index([("x", 1)]) await self.collection.create_index([("y", -1)]) keys = set() async for info in self.collection.list_indexes(): keys.add(info["name"]) self.assertEqual(keys, {"_id_", "x_1", "y_-1"}) @env.require_version_min(3, 6) @env.require_replica_set @gen_test async def test_session(self): s = await self.cx.start_session() self.assertIsInstance(s, MotorClientSession) self.assertIs(s.client, self.cx) self.assertFalse(s.has_ended) await s.end_session() self.assertTrue(s.has_ended) # Raises a helpful error if used in a regular with-statement. with self.assertRaises(AttributeError) as ctx: with await self.cx.start_session(): pass self.assertIn("async with await", str(ctx.exception)) async with await self.cx.start_session() as s: self.assertIsInstance(s, MotorClientSession) self.assertFalse(s.has_ended) await s.end_session() self.assertTrue(s.has_ended) self.assertTrue(s.has_ended) @env.require_version_min(3, 7) @env.require_replica_set @gen_test async def test_transaction(self): async with await self.cx.start_session() as s: s.start_transaction() self.assertTrue(s.in_transaction) self.assertFalse(s.has_ended) await s.end_session() self.assertFalse(s.in_transaction) self.assertTrue(s.has_ended) async with await self.cx.start_session() as s: # Use start_transaction in "async with", not "async with await". with self.assertRaises(TypeError): async with await s.start_transaction(): pass await s.abort_transaction() async with s.start_transaction(): self.assertTrue(s.in_transaction) self.assertFalse(s.has_ended) self.assertFalse(s.in_transaction) self.assertFalse(s.has_ended) self.assertTrue(s.has_ended) motor-3.4.0/test/tornado_tests/test_motor_basic.py000066400000000000000000000135651460060525600224550ustar00rootroot00000000000000# Copyright 2013-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import asyncio import os import test import unittest from abc import ABC from multiprocessing import Pipe from test.tornado_tests import MotorTest from test.utils import ignore_deprecations import pymongo from pymongo import WriteConcern from pymongo.read_preferences import Nearest, ReadPreference, Secondary from tornado.ioloop import IOLoop from tornado.testing import gen_test import motor class MotorTestBasic(MotorTest): def test_repr(self): self.assertTrue(repr(self.cx).startswith("MotorClient")) self.assertTrue(repr(self.db).startswith("MotorDatabase")) self.assertTrue(repr(self.collection).startswith("MotorCollection")) cursor = self.collection.find() self.assertTrue(repr(cursor).startswith("MotorCursor")) @gen_test async def test_write_concern(self): # Default empty dict means "w=1" self.assertEqual(WriteConcern(), self.cx.write_concern) await self.collection.delete_many({}) await self.collection.insert_one({"_id": 0}) for wc_opts in [ {}, {"w": 0}, {"w": 1}, {"wTimeoutMS": 1000}, ]: cx = self.motor_client(test.env.uri, **wc_opts) wtimeout = wc_opts.pop("wTimeoutMS", None) if wtimeout: wc_opts["wtimeout"] = wtimeout wc = WriteConcern(**wc_opts) self.assertEqual(wc, cx.write_concern) db = cx.motor_test self.assertEqual(wc, db.write_concern) collection = db.test_collection self.assertEqual(wc, collection.write_concern) if wc.acknowledged: with self.assertRaises(pymongo.errors.DuplicateKeyError): await collection.insert_one({"_id": 0}) else: await collection.insert_one({"_id": 0}) # No error # No error c = collection.with_options(write_concern=WriteConcern(w=0)) await c.insert_one({"_id": 0}) cx.close() @ignore_deprecations def test_read_preference(self): # Check the default cx = motor.MotorClient(test.env.uri, io_loop=self.io_loop) self.assertEqual(ReadPreference.PRIMARY, cx.read_preference) # We can set mode, tags, and latency. cx = self.motor_client( read_preference=Secondary(tag_sets=[{"foo": "bar"}]), localThresholdMS=42 ) self.assertEqual(ReadPreference.SECONDARY.mode, cx.read_preference.mode) self.assertEqual([{"foo": "bar"}], cx.read_preference.tag_sets) self.assertEqual(42, cx.options.local_threshold_ms) # Make a MotorCursor and get its PyMongo Cursor collection = cx.motor_test.test_collection.with_options( read_preference=Nearest(tag_sets=[{"yay": "jesse"}]) ) motor_cursor = collection.find() cursor = motor_cursor.delegate self.assertEqual(Nearest(tag_sets=[{"yay": "jesse"}]), cursor._read_preference()) cx.close() def test_underscore(self): self.assertIsInstance(self.cx["_db"], motor.MotorDatabase) self.assertIsInstance(self.db["_collection"], motor.MotorCollection) self.assertIsInstance(self.collection["_collection"], motor.MotorCollection) with self.assertRaises(AttributeError): self.cx._db with self.assertRaises(AttributeError): self.db._collection with self.assertRaises(AttributeError): self.collection._collection def test_abc(self): class C(ABC): db = self.db collection = self.collection subcollection = self.collection.subcollection # MOTOR-104, TypeError: Can't instantiate abstract class C with abstract # methods collection, db, subcollection. C() @gen_test async def test_inheritance(self): class CollectionSubclass(motor.MotorCollection): pass class DatabaseSubclass(motor.MotorDatabase): def __getitem__(self, name): return CollectionSubclass(self, name) class ClientSubclass(motor.MotorClient): def __getitem__(self, name): return DatabaseSubclass(self, name) cx = ClientSubclass(test.env.uri, **self.get_client_kwargs()) self.assertIsInstance(cx, ClientSubclass) db = cx["testdb"] self.assertIsInstance(db, DatabaseSubclass) coll = db["testcoll"] self.assertIsInstance(coll, CollectionSubclass) self.assertIsNotNone(await coll.insert_one({})) class ExecutorForkTest(MotorTest): @unittest.skipUnless(hasattr(os, "fork"), "This test requires fork") @gen_test() async def test_executor_reset(self): parent_conn, child_conn = Pipe() lock_pid = os.fork() if lock_pid == 0: # Child asyncio.set_event_loop(asyncio.new_event_loop()) self.loop = IOLoop.current() client = self.motor_client() try: self.loop.spawn_callback(client.db.command, "ping") except Exception: child_conn.send(False) child_conn.send(True) os._exit(0) else: # Parent self.assertTrue(parent_conn.recv(), "Child process did not complete.") motor-3.4.0/test/tornado_tests/test_motor_change_stream.py000066400000000000000000000207061460060525600241670ustar00rootroot00000000000000# Copyright 2017-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. """Test MotorChangeStream.""" import copy import threading import time from test import SkipTest, env from test.tornado_tests import MotorTest from test.utils import get_async_test_timeout, wait_until from pymongo.errors import InvalidOperation, OperationFailure from tornado.testing import gen_test class MotorChangeStreamTest(MotorTest): @classmethod @env.require_version_min(3, 6) def setUpClass(cls): super().setUpClass() if env.is_standalone: raise SkipTest("Standalone") # Ensure the collection exists. env.sync_cx.motor_test.test_collection.delete_many({}) env.sync_cx.motor_test.test_collection.insert_one({"_id": 1}) def wait_and_insert(self, change_stream, n=1): # The start time of the change stream is nondeterministic. Wait # to ensure this insert comes after the change stream starts. def target(): start = time.time() timeout = get_async_test_timeout() while not change_stream.delegate: if time.time() - start > timeout: print("MotorChangeStream never created ChangeStream") return time.sleep(0.1) doclist = [{} for _ in range(n)] if isinstance(n, int) else n self.io_loop.add_callback(self.collection.insert_many, doclist) t = threading.Thread(target=target) t.daemon = True t.start() @gen_test async def test_async_for(self): change_stream = self.collection.watch() self.wait_and_insert(change_stream, 2) i = 0 async for _ in change_stream: i += 1 if i == 2: break self.assertEqual(i, 2) @gen_test async def test_async_try_next(self): change_stream = self.collection.watch() # No changes. doc = await change_stream.try_next() self.assertIsNone(doc) # Insert a change and ensure we see it via try_next. idoc = {"_id": 1, "data": "abc"} self.wait_and_insert(change_stream, [idoc]) while change_stream.alive: change_doc = await change_stream.try_next() if change_doc is not None: break self.assertEqual(change_doc["fullDocument"], idoc) @env.require_version_min(4, 0, 7) @gen_test async def test_async_try_next_updates_resume_token(self): change_stream = self.collection.watch([{"$match": {"fullDocument.a": 10}}]) # Get empty change, check non-empty resume token. _ = await change_stream.try_next() self.assertIsNotNone(change_stream.resume_token) # Insert some record that don't match the change stream filter. self.wait_and_insert(change_stream, [{"a": 19}, {"a": 20}]) # Ensure we see a new resume token even though we see no changes. initial_resume_token = copy.copy(change_stream.resume_token) async def token_change(): _ = await change_stream.try_next() return change_stream.resume_token != initial_resume_token await wait_until(token_change, "see a new resume token", timeout=get_async_test_timeout()) @gen_test async def test_watch(self): coll = self.collection with self.assertRaises(TypeError): # pipeline must be a list. async for _ in coll.watch(pipeline={}): pass change_stream = coll.watch() future = change_stream.next() self.wait_and_insert(change_stream, 1) change = await future # New change stream with resume token. await coll.insert_one({"_id": 23}) change = await coll.watch(resume_after=change["_id"]).next() self.assertEqual(change["fullDocument"], {"_id": 23}) @env.require_version_min(4, 2) @gen_test async def test_watch_with_start_after(self): # Ensure collection exists before starting. await self.collection.insert_one({}) # Create change stream before invalidate event. change_stream = self.collection.watch([{"$match": {"operationType": "invalidate"}}]) _ = await change_stream.try_next() # Generate invalidate event and store corresponding resume token. await self.collection.drop() _ = await change_stream.next() # v5.1 requires an extra getMore after an invalidate event to exhaust # the cursor. self.assertIsNone(await change_stream.try_next()) self.assertFalse(change_stream.alive) resume_token = change_stream.resume_token # Recreate change stream and observe from invalidate event. doc = {"_id": "startAfterTest"} await self.collection.insert_one(doc) change_stream = self.collection.watch(start_after=resume_token) change = await change_stream.next() self.assertEqual(doc, change["fullDocument"]) @gen_test async def test_close(self): coll = self.collection change_stream = coll.watch() future = change_stream.next() self.wait_and_insert(change_stream, 1) await future await change_stream.close() with self.assertRaises(StopAsyncIteration): await change_stream.next() async for _ in change_stream: pass @gen_test async def test_missing_id(self): coll = self.collection change_stream = coll.watch([{"$project": {"_id": 0}}]) future = change_stream.next() self.wait_and_insert(change_stream) with self.assertRaises((InvalidOperation, OperationFailure)): await future # The cursor should now be closed. with self.assertRaises(StopAsyncIteration): await change_stream.next() @gen_test async def test_unknown_full_document(self): coll = self.collection change_stream = coll.watch(full_document="unknownFullDocOption") future = change_stream.next() self.wait_and_insert(change_stream, 1) with self.assertRaises(OperationFailure): await future @gen_test async def test_async_with(self): async with self.collection.watch() as change_stream: self.wait_and_insert(change_stream, 1) async for _ in change_stream: self.assertTrue(change_stream.delegate._cursor.alive) break self.assertFalse(change_stream.delegate._cursor.alive) @gen_test async def test_with_statement(self): with self.assertRaises(RuntimeError): with self.collection.watch(): pass @env.require_version_min(4, 0) @gen_test async def test_client(self): change_stream = self.cx.watch() self.wait_and_insert(change_stream, 2) i = 0 async for _ in change_stream: i += 1 if i == 2: break await self.cx.other_db.other_collection.insert_one({}) async for _ in change_stream: i += 1 if i == 3: break @env.require_version_min(4, 0) @gen_test async def test_database(self): change_stream = self.db.watch() self.wait_and_insert(change_stream, 2) i = 0 async for _ in change_stream: i += 1 if i == 2: break await self.db.other_collection.insert_one({}) async for _ in change_stream: i += 1 if i == 3: break @gen_test async def test_watch_with_session(self): async with await self.cx.start_session() as session: # Pass MotorSession. async with self.collection.watch(session=session) as cs: self.wait_and_insert(cs, 1) _ = await cs.next() # Pass PyMongo session directly. async with self.collection.watch(session=session.delegate) as cs: self.wait_and_insert(cs, 1) _ = await cs.next() motor-3.4.0/test/tornado_tests/test_motor_client.py000066400000000000000000000306441460060525600226470ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import os import test import unittest from test import SkipTest from test.test_environment import db_password, db_user, env from test.tornado_tests import MotorMockServerTest, MotorTest, remove_all_users from test.utils import get_primary_pool, one import pymongo import pymongo.mongo_client from bson import CodecOptions from mockupdb import OpQuery from pymongo import CursorType, ReadPreference, WriteConcern from pymongo.driver_info import DriverInfo from pymongo.errors import ConnectionFailure, OperationFailure from tornado import gen from tornado.testing import gen_test import motor class MotorClientTest(MotorTest): @gen_test async def test_client_lazy_connect(self): await self.db.test_client_lazy_connect.delete_many({}) # Create client without connecting; connect on demand. cx = self.motor_client() collection = cx.motor_test.test_client_lazy_connect future0 = collection.insert_one({"foo": "bar"}) future1 = collection.insert_one({"foo": "bar"}) await gen.multi([future0, future1]) self.assertEqual(2, (await collection.count_documents({"foo": "bar"}))) cx.close() @gen_test async def test_unix_socket(self): if env.mongod_started_with_ssl: raise SkipTest("Server started with SSL") mongodb_socket = "/tmp/mongodb-%d.sock" % env.port if not os.access(mongodb_socket, os.R_OK): raise SkipTest("Socket file is not accessible") encoded_socket = "%2Ftmp%2Fmongodb-" + str(env.port) + ".sock" if test.env.auth: uri = "mongodb://%s:%s@%s" % (db_user, db_password, encoded_socket) else: uri = "mongodb://%s" % (encoded_socket,) client = self.motor_client(uri) await client.motor_test.test.insert_one({"dummy": "object"}) # Confirm it fails with a missing socket. client = motor.MotorClient( "mongodb://%2Ftmp%2Fnon-existent.sock", io_loop=self.io_loop, serverSelectionTimeoutMS=100, ) with self.assertRaises(ConnectionFailure): await client.admin.command("ismaster") def test_io_loop(self): with self.assertRaises(TypeError): motor.MotorClient(test.env.uri, io_loop="foo") def test_database_named_delegate(self): self.assertTrue(isinstance(self.cx.delegate, pymongo.mongo_client.MongoClient)) self.assertTrue(isinstance(self.cx["delegate"], motor.MotorDatabase)) @gen_test async def test_connection_failure(self): # Assuming there isn't anything actually running on this port client = motor.MotorClient( "localhost", 8765, io_loop=self.io_loop, serverSelectionTimeoutMS=10 ) with self.assertRaises(ConnectionFailure): await client.admin.command("ismaster") @gen_test(timeout=30) async def test_connection_timeout(self): # Motor merely tries to time out a connection attempt within the # specified duration; DNS lookup in particular isn't charged against # the timeout. So don't measure how long this takes. client = motor.MotorClient( "example.com", port=12345, serverSelectionTimeoutMS=1, io_loop=self.io_loop ) with self.assertRaises(ConnectionFailure): await client.admin.command("ismaster") def test_max_pool_size_validation(self): with self.assertRaises(ValueError): motor.MotorClient(maxPoolSize=-1) with self.assertRaises(ValueError): motor.MotorClient(maxPoolSize="foo") cx = self.motor_client(maxPoolSize=100) self.assertEqual(cx.options.pool_options.max_pool_size, 100) cx.close() @gen_test(timeout=30) async def test_drop_database(self): # Make sure we can pass a MotorDatabase instance to drop_database db = self.cx.test_drop_database await db.test_collection.insert_one({}) names = await self.cx.list_database_names() self.assertTrue("test_drop_database" in names) await self.cx.drop_database(db) names = await self.cx.list_database_names() self.assertFalse("test_drop_database" in names) @gen_test async def test_auth_from_uri(self): if not test.env.auth: raise SkipTest("Authentication is not enabled on server") # self.db is logged in as root. await remove_all_users(self.db) db = self.db try: test.env.create_user(db.name, "mike", "password", roles=["userAdmin", "readWrite"]) client = self.motor_client("mongodb://u:pass@%s:%d" % (env.host, env.port)) with self.assertRaises(OperationFailure): await client.db.collection.find_one() client = self.motor_client( "mongodb://mike:password@%s:%d/%s" % (env.host, env.port, db.name) ) await client[db.name].collection.find_one() finally: test.env.drop_user(db.name, "mike") def test_get_database(self): codec_options = CodecOptions(tz_aware=True) write_concern = WriteConcern(w=2, j=True) db = self.cx.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) self.assertTrue(isinstance(db, motor.MotorDatabase)) self.assertEqual("foo", db.name) self.assertEqual(codec_options, db.codec_options) self.assertEqual(ReadPreference.SECONDARY, db.read_preference) self.assertEqual(write_concern, db.write_concern) @gen_test async def test_list_databases(self): await self.collection.insert_one({}) cursor = await self.cx.list_databases() self.assertIsInstance(cursor, motor.motor_tornado.MotorCommandCursor) # Make sure the cursor works, by searching for "local" database. while await cursor.fetch_next: info = cursor.next_object() if info["name"] == self.collection.database.name: break else: self.fail("'%s' database not found" % self.collection.database.name) @gen_test async def test_list_database_names(self): await self.collection.insert_one({}) names = await self.cx.list_database_names() self.assertIsInstance(names, list) self.assertIn(self.collection.database.name, names) class MotorClientTimeoutTest(MotorMockServerTest): @gen_test async def test_timeout(self): server = self.server(auto_ismaster=True) client = motor.MotorClient(server.uri, socketTimeoutMS=100) with self.assertRaises(pymongo.errors.AutoReconnect) as context: await client.motor_test.test_collection.find_one() self.assertIn("timed out", str(context.exception)) client.close() class MotorClientExhaustCursorTest(MotorMockServerTest): def primary_server(self): primary = self.server() hosts = [primary.address_string] primary.autoresponds("ismaster", ismaster=True, setName="rs", hosts=hosts, maxWireVersion=6) return primary def primary_or_standalone(self, rs): if rs: return self.primary_server() else: return self.server(auto_ismaster=True) async def _test_exhaust_query_server_error(self, rs): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid counter leak. server = self.primary_or_standalone(rs=rs) client = motor.MotorClient(server.uri, maxPoolSize=1) await client.admin.command("ismaster") pool = get_primary_pool(client) conn = one(pool.conns) cursor = client.db.collection.find(cursor_type=CursorType.EXHAUST) # With Tornado, simply accessing fetch_next starts the fetch. fetch_next = cursor.fetch_next request = await self.run_thread(server.receives, OpQuery) request.fail(code=1) with self.assertRaises(pymongo.errors.OperationFailure): await fetch_next self.assertFalse(conn.closed) self.assertEqual(conn, one(pool.conns)) @gen_test async def test_exhaust_query_server_error_standalone(self): await self._test_exhaust_query_server_error(rs=False) @gen_test async def test_exhaust_query_server_error_rs(self): await self._test_exhaust_query_server_error(rs=True) async def _test_exhaust_query_network_error(self, rs): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid counter leak. server = self.primary_or_standalone(rs=rs) client = motor.MotorClient(server.uri, maxPoolSize=1, retryReads=False) await client.admin.command("ismaster") pool = get_primary_pool(client) pool._check_interval_seconds = None # Never check. conns = pool.conns conn = one(conns) cursor = client.db.collection.find(cursor_type=CursorType.EXHAUST) # With Tornado, simply accessing fetch_next starts the fetch. fetch_next = cursor.fetch_next request = await self.run_thread(server.receives, OpQuery) request.hangs_up() with self.assertRaises(pymongo.errors.ConnectionFailure): await fetch_next self.assertTrue(conn.closed) del cursor conns = pool.conns self.assertNotIn(conn, conns) @gen_test async def test_exhaust_query_network_error_standalone(self): await self._test_exhaust_query_network_error(rs=False) @gen_test async def test_exhaust_query_network_error_rs(self): await self._test_exhaust_query_network_error(rs=True) class MotorClientHandshakeTest(MotorMockServerTest): @gen_test async def test_handshake(self): server = self.server() client = motor.MotorClient(server.uri, connectTimeoutMS=100, serverSelectionTimeoutMS=100) # Trigger connection. future = client.db.command("ping") ismaster = await self.run_thread(server.receives, "ismaster") meta = ismaster.doc["client"] self.assertEqual("PyMongo|Motor", meta["driver"]["name"]) self.assertIn("Tornado", meta["platform"]) self.assertTrue( meta["driver"]["version"].endswith(motor.version), "Version in handshake [%s] doesn't end with Motor version [%s]" % (meta["driver"]["version"], motor.version), ) ismaster.hangs_up() server.stop() client.close() try: await future except Exception: pass @gen_test async def test_driver_info(self): server = self.server() driver_info = DriverInfo(name="Foo", version="1.1.1", platform="FooPlat") client = motor.MotorClient(server.uri, driver=driver_info) # Trigger connection. future = client.db.command("ping") handshake = await self.run_thread(server.receives, "ismaster") meta = handshake.doc["client"] self.assertEqual(f"PyMongo|Motor|{driver_info.name}", meta["driver"]["name"]) self.assertIn("Tornado", meta["platform"]) self.assertIn(f"|{driver_info.platform}", meta["platform"]) self.assertTrue( meta["driver"]["version"].endswith(f"{motor.version}|{driver_info.version}"), "Version in handshake [%s] doesn't end with MotorVersion|Test version [%s]" % (meta["driver"]["version"], f"{motor.version}|{driver_info.version}"), ) handshake.ok() server.stop() client.close() try: await future except Exception: pass def test_incorrect_driver_info(self): with self.assertRaises( TypeError, msg="Allowed invalid type parameter str, driver should only be of DriverInfo", ): motor.MotorClient(driver="string") if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_collection.py000066400000000000000000000315551460060525600235260ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import sys import traceback import unittest from test.test_environment import env from test.tornado_tests import MotorTest from test.utils import ignore_deprecations import pymongo.errors from bson import CodecOptions from bson.binary import JAVA_LEGACY from pymongo import ReadPreference, WriteConcern from pymongo.encryption import Algorithm, QueryType from pymongo.errors import BulkWriteError, DuplicateKeyError, OperationFailure from pymongo.read_concern import ReadConcern from pymongo.read_preferences import Secondary from tornado import gen from tornado.testing import gen_test import motor import motor.motor_tornado if pymongo.version_tuple >= (4, 4, 0): from pymongo.encryption_options import RangeOpts class MotorCollectionTest(MotorTest): @gen_test async def test_collection(self): # Test that we can create a collection directly, not just from # MotorClient's accessors collection = motor.MotorCollection(self.db, "test_collection") # Make sure we got the right collection and it can do an operation self.assertEqual("test_collection", collection.name) await collection.insert_one({"_id": 1}) doc = await collection.find_one({"_id": 1}) self.assertEqual(1, doc["_id"]) # If you pass kwargs to PyMongo's Collection(), it calls # db.create_collection(). Motor can't do I/O in a constructor # so this is prohibited. self.assertRaises(TypeError, motor.MotorCollection, self.db, "test_collection", capped=True) @gen_test async def test_dotted_collection_name(self): # Ensure that remove, insert, and find work on collections with dots # in their names. for coll in (self.db.foo.bar, self.db.foo.bar.baz): await coll.delete_many({}) result = await coll.insert_one({"_id": "xyzzy"}) self.assertEqual("xyzzy", result.inserted_id) result = await coll.find_one({"_id": "xyzzy"}) self.assertEqual(result["_id"], "xyzzy") await coll.delete_many({}) self.assertEqual(None, (await coll.find_one({"_id": "xyzzy"}))) def test_call(self): # Prevents user error with nice message. try: self.db.foo() except TypeError as e: self.assertTrue("no such method exists" in str(e)) else: self.fail("Expected TypeError") @ignore_deprecations @gen_test async def test_update(self): await self.collection.insert_one({"_id": 1}) result = await self.collection.update_one({"_id": 1}, {"$set": {"foo": "bar"}}) self.assertIsNone(result.upserted_id) self.assertEqual(1, result.modified_count) @ignore_deprecations @gen_test async def test_update_bad(self): # Violate a unique index, make sure we handle error well coll = self.db.unique_collection await coll.create_index("s", unique=True) try: await coll.insert_many([{"s": 1}, {"s": 2}]) with self.assertRaises(DuplicateKeyError): await coll.update_one({"s": 2}, {"$set": {"s": 1}}) finally: await coll.drop() @gen_test async def test_insert_one(self): collection = self.collection result = await collection.insert_one({"_id": 201}) self.assertEqual(201, result.inserted_id) @ignore_deprecations @gen_test async def test_insert_many_one_bad(self): collection = self.collection await collection.insert_one({"_id": 2}) # Violate a unique index in one of many updates, handle error. with self.assertRaises(BulkWriteError): await collection.insert_many([{"_id": 1}, {"_id": 2}, {"_id": 3}]) # Already exists # First insert should have succeeded, but not second or third. self.assertEqual(set([1, 2]), set(await collection.distinct("_id"))) @gen_test async def test_delete_one(self): # Remove a document twice, check that we get a success responses # and n = 0 for the second time. await self.collection.insert_one({"_id": 1}) result = await self.collection.delete_one({"_id": 1}) # First time we remove, n = 1 self.assertEqual(1, result.raw_result["n"]) self.assertEqual(1, result.raw_result["ok"]) self.assertEqual(None, result.raw_result.get("err")) result = await self.collection.delete_one({"_id": 1}) # Second time, document is already gone, n = 0 self.assertEqual(0, result.raw_result["n"]) self.assertEqual(1, result.raw_result["ok"]) self.assertEqual(None, result.raw_result.get("err")) @gen_test async def test_unacknowledged_insert(self): # Test that unacknowledged inserts complete eventually. coll = self.db.test_unacknowledged_insert await coll.with_options(write_concern=WriteConcern(0)).insert_one({"_id": 1}) # The insert is eventually executed. while not (await coll.count_documents({})): await gen.sleep(0.1) @gen_test async def test_unacknowledged_update(self): # Test that unacknowledged updates complete eventually. coll = self.collection await coll.insert_one({"_id": 1}) await coll.with_options(write_concern=WriteConcern(0)).update_one( {"_id": 1}, {"$set": {"a": 1}} ) while not (await coll.find_one({"a": 1})): await gen.sleep(0.1) @ignore_deprecations @gen_test async def test_indexes(self): test_collection = self.collection # Create an index idx_name = await test_collection.create_index([("foo", 1)]) index_info = await test_collection.index_information() self.assertEqual([("foo", 1)], index_info[idx_name]["key"]) # Don't test drop_index or drop_indexes -- Synchro tests them async def _make_test_data(self, n): await self.db.drop_collection("test") await self.db.test.insert_many([{"_id": i} for i in range(n)]) return sum(range(n)) pipeline = [{"$project": {"_id": "$_id"}}] def assertAllDocs(self, expected_sum, docs): self.assertEqual(expected_sum, sum(doc["_id"] for doc in docs)) @gen_test(timeout=30) async def test_aggregation_cursor(self): db = self.db # A small collection which returns only an initial batch, # and a larger one that requires a getMore. for collection_size in (10, 1000): expected_sum = await self._make_test_data(collection_size) cursor = db.test.aggregate(self.pipeline) docs = await cursor.to_list(collection_size) self.assertAllDocs(expected_sum, docs) @gen_test async def test_aggregation_cursor_exc_info(self): await self._make_test_data(200) cursor = self.db.test.aggregate(self.pipeline) await cursor.to_list(length=10) await self.db.test.drop() try: await cursor.to_list(length=None) except OperationFailure: _, _, tb = sys.exc_info() # The call tree should include PyMongo code we ran on a thread. formatted = "\n".join(traceback.format_tb(tb)) self.assertTrue( "_unpack_response" in formatted or "_check_command_response" in formatted ) @gen_test async def test_aggregate_cursor_del(self): cursor = self.db.test.aggregate(self.pipeline) del cursor cursor = self.db.test.aggregate(self.pipeline) await cursor.close() del cursor def test_with_options(self): coll = self.db.test codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) write_concern = WriteConcern(w=2, j=True) coll2 = coll.with_options(codec_options, ReadPreference.SECONDARY, write_concern) self.assertTrue(isinstance(coll2, motor.MotorCollection)) self.assertEqual(codec_options, coll2.codec_options) self.assertEqual(Secondary(), coll2.read_preference) self.assertEqual(write_concern, coll2.write_concern) pref = Secondary([{"dc": "sf"}]) coll2 = coll.with_options(read_preference=pref) self.assertEqual(pref, coll2.read_preference) self.assertEqual(coll.codec_options, coll2.codec_options) self.assertEqual(coll.write_concern, coll2.write_concern) def test_sub_collection(self): # Verify that a collection with a dotted name inherits options from its # parent collection. write_concern = WriteConcern(w=2, j=True) read_concern = ReadConcern("majority") read_preference = Secondary([{"dc": "sf"}]) codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) coll1 = self.db.get_collection( "test", write_concern=write_concern, read_concern=read_concern, read_preference=read_preference, codec_options=codec_options, ) coll2 = coll1.subcollection coll3 = coll1["subcollection"] for c in [coll1, coll2, coll3]: self.assertEqual(write_concern, c.write_concern) self.assertEqual(read_concern, c.read_concern) self.assertEqual(read_preference, c.read_preference) self.assertEqual(codec_options, c.codec_options) @env.require_version_min(7, 0, -1, -1) @env.require_no_standalone @gen_test async def test_async_create_encrypted_collection(self): await self.db.drop_collection("test_collection") c = self.collection KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}} self.cx.drop_database("db") async with motor.MotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", c, CodecOptions() ) as client_encryption: coll, ef = await client_encryption.create_encrypted_collection( database=self.db, name="testing1", encrypted_fields={"fields": [{"path": "ssn", "bsonType": "string", "keyId": None}]}, kms_provider="local", ) with self.assertRaises(pymongo.errors.WriteError) as exc: await coll.insert_one({"ssn": "123-45-6789"}) self.assertEqual(exc.exception.code, 121) await self.db.drop_collection("testing1", encrypted_fields=ef) @gen_test async def test_async_encrypt_expression(self): c = self.collection KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}} self.cx.drop_database("db") async with motor.MotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", c, CodecOptions() ) as client_encryption: data_key = await client_encryption.create_data_key( "local", key_alt_names=["pymongo_encryption_example_1"] ) name = "DoubleNoPrecision" range_opts = RangeOpts(sparsity=1) for i in [6.0, 30.0, 200.0]: insert_payload = await client_encryption.encrypt( float(i), key_id=data_key, algorithm=Algorithm.RANGEPREVIEW, contention_factor=0, range_opts=range_opts, ) self.collection.insert_one( { f"encrypted{name}": insert_payload, } ) self.assertEqual(await client_encryption.decrypt(insert_payload), i) find_payload = await client_encryption.encrypt_expression( expression={ "$and": [ {f"encrypted{name}": {"$gte": 6.0}}, {f"encrypted{name}": {"$lte": 200.0}}, ] }, key_id=data_key, algorithm=Algorithm.RANGEPREVIEW, query_type=QueryType.RANGEPREVIEW, contention_factor=0, range_opts=range_opts, ) sorted_find = sorted( await self.collection.explicit_encryption.find(find_payload).to_list(3), key=lambda x: x["_id"], ) for elem, expected in zip(sorted_find, [6.0, 30.0, 200.0]): self.assertEqual(elem[f"encrypted{name}"], expected) if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_core.py000066400000000000000000000115771460060525600223250ustar00rootroot00000000000000# Copyright 2016 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. """Validate list of PyMongo attributes wrapped by Motor.""" from test import env from test.tornado_tests import MotorTest from gridfs import GridFSBucket, GridIn from tornado.testing import gen_test from motor import MotorGridFSBucket, MotorGridIn def attrs(klass): return set(a for a in dir(klass) if not a.startswith("_")) motor_only = set(["delegate", "get_io_loop", "io_loop", "wrap"]) pymongo_only = set(["next"]) motor_client_only = motor_only.union(["open"]) pymongo_client_only = set([]).union(pymongo_only) pymongo_database_only = set([]).union(pymongo_only) pymongo_collection_only = set([]).union(pymongo_only) motor_cursor_only = set( ["fetch_next", "to_list", "each", "started", "next_object", "closed"] ).union(motor_only) pymongo_cursor_only = set(["retrieved"]) class MotorCoreTest(MotorTest): def test_client_attrs(self): self.assertEqual( attrs(env.sync_cx) - pymongo_client_only, attrs(self.cx) - motor_client_only ) @env.require_version_min(3, 6) @env.require_replica_set @gen_test async def test_client_session_attrs(self): self.assertEqual( attrs(env.sync_cx.start_session()), attrs(await self.cx.start_session()) - motor_only ) def test_database_attrs(self): self.assertEqual( attrs(env.sync_cx.test) - pymongo_database_only, attrs(self.cx.test) - motor_only ) def test_collection_attrs(self): self.assertEqual( attrs(env.sync_cx.test.test) - pymongo_collection_only, attrs(self.cx.test.test) - motor_only, ) def test_cursor_attrs(self): self.assertEqual( attrs(env.sync_cx.test.test.find()) - pymongo_cursor_only, attrs(self.cx.test.test.find()) - motor_cursor_only, ) @env.require_replica_set @env.require_version_min(3, 6) def test_change_stream_attrs(self): # Ensure the database exists before creating a change stream. env.sync_cx.test.test.insert_one({}) self.assertEqual( attrs(env.sync_cx.test.test.watch()), attrs(self.cx.test.test.watch()) - motor_only ) @gen_test def test_command_cursor_attrs(self): motor_agg_cursor_only = set(["collection", "start", "args", "kwargs", "pipeline"]).union( motor_cursor_only ) pymongo_cursor = env.sync_cx.test.test.aggregate([], cursor={}) motor_cursor = self.cx.test.test.aggregate([]) self.assertEqual( attrs(pymongo_cursor) - pymongo_cursor_only, attrs(motor_cursor) - motor_agg_cursor_only ) class MotorCoreTestGridFS(MotorTest): def setUp(self): super().setUp() self.sync_fs = GridFSBucket(env.sync_cx.test) self.sync_fs.upload_from_stream_with_id(1, "filename", source=b"") def tearDown(self): self.sync_fs.delete(file_id=1) super().tearDown() def test_gridfs_attrs(self): motor_gridfs_only = set(["collection"]).union(motor_only) self.assertEqual( attrs(GridFSBucket(env.sync_cx.test)), attrs(MotorGridFSBucket(self.cx.test)) - motor_gridfs_only, ) def test_gridin_attrs(self): motor_gridin_only = set(["set"]).union(motor_only) gridin_only = set(["md5"]) self.assertEqual( attrs(GridIn(env.sync_cx.test.fs)) - gridin_only, attrs(MotorGridIn(self.cx.test.fs)) - motor_gridin_only, ) @gen_test async def test_gridout_attrs(self): motor_gridout_only = set(["open", "stream_to_handler"]).union(motor_only) gridin_only = set( [ "md5", "readlines", "truncate", "flush", "fileno", "closed", "writelines", "isatty", "writable", ] ) fs = MotorGridFSBucket(self.cx.test) motor_gridout = await fs.open_download_stream(1) self.assertEqual( attrs(self.sync_fs.open_download_stream(1)) - gridin_only, attrs(motor_gridout) - motor_gridout_only, ) def test_gridout_cursor_attrs(self): self.assertEqual( attrs(self.sync_fs.find()) - pymongo_cursor_only, attrs(MotorGridFSBucket(self.cx.test).find()) - motor_cursor_only, ) motor-3.4.0/test/tornado_tests/test_motor_cursor.py000066400000000000000000000545141460060525600227100ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import sys import traceback import unittest import warnings from functools import partial from test import SkipTest, env from test.tornado_tests import ( MotorMockServerTest, MotorTest, get_command_line, server_is_mongos, ) from test.utils import ( TestListener, get_async_test_timeout, get_primary_pool, one, safe_get, wait_until, ) import bson import pymongo from pymongo import CursorType from pymongo.collation import Collation from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure from tornado import gen from tornado.concurrent import Future from tornado.testing import gen_test import motor import motor.motor_tornado class MotorCursorTest(MotorMockServerTest): def test_cursor(self): cursor = self.collection.find() self.assertTrue(isinstance(cursor, motor.motor_tornado.MotorCursor)) self.assertFalse(cursor.started, "Cursor shouldn't start immediately") @gen_test async def test_count(self): await self.make_test_data() coll = self.collection self.assertEqual(100, (await coll.count_documents({"_id": {"$gt": 99}}))) @gen_test async def test_fetch_next(self): await self.make_test_data() coll = self.collection # 200 results, only including _id field, sorted by _id cursor = coll.find({}, {"_id": 1}).sort([("_id", pymongo.ASCENDING)]).batch_size(75) self.assertEqual(None, cursor.cursor_id) self.assertEqual(None, cursor.next_object()) # Haven't fetched yet i = 0 while await cursor.fetch_next: self.assertEqual({"_id": i}, cursor.next_object()) i += 1 # With batch_size 75 and 200 results, cursor should be exhausted on # the server by third fetch if i <= 150: self.assertNotEqual(0, cursor.cursor_id) else: self.assertEqual(0, cursor.cursor_id) self.assertEqual(False, (await cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) self.assertEqual(0, cursor.cursor_id) self.assertEqual(200, i) @gen_test async def test_fetch_next_delete(self): if "PyPy" in sys.version: raise SkipTest("PyPy") client, server = self.client_server(auto_ismaster=True) cursor = client.test.coll.find() # With Tornado, simply accessing fetch_next starts the fetch. cursor.fetch_next request = await self.run_thread(server.receives, "find", "coll") request.replies({"cursor": {"id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) # Decref'ing the cursor eventually closes it on the server. del cursor # Clear Runner's reference. request = await self.run_thread(server.receives, "killCursors", "coll") request.ok() @gen_test async def test_fetch_next_without_results(self): coll = self.collection # Nothing matches this query cursor = coll.find({"foo": "bar"}) self.assertEqual(None, cursor.next_object()) self.assertEqual(False, (await cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) # Now cursor knows it's exhausted self.assertEqual(0, cursor.cursor_id) @gen_test async def test_fetch_next_is_idempotent(self): # Subsequent calls to fetch_next don't do anything await self.make_test_data() coll = self.collection cursor = coll.find() self.assertEqual(None, cursor.cursor_id) await cursor.fetch_next self.assertTrue(cursor.cursor_id) self.assertEqual(101, cursor._buffer_size()) await cursor.fetch_next # Does nothing self.assertEqual(101, cursor._buffer_size()) await cursor.close() @gen_test async def test_fetch_next_exception(self): coll = self.collection await coll.insert_many([{} for _ in range(10)]) cursor = coll.find(batch_size=2) await cursor.fetch_next self.assertTrue(cursor.next_object()) # Not valid on server, causes CursorNotFound. cursor.delegate._Cursor__id = bson.int64.Int64(1234) with self.assertRaises(OperationFailure): await cursor.fetch_next self.assertTrue(cursor.next_object()) await cursor.fetch_next self.assertTrue(cursor.next_object()) def test_each_callback(self): cursor = self.collection.find() self.assertRaises(TypeError, cursor.each, callback="foo") self.assertRaises(TypeError, cursor.each, callback=None) self.assertRaises(TypeError, cursor.each) # No callback. @gen_test(timeout=30) async def test_each(self): await self.make_test_data() cursor = self.collection.find({}, {"_id": 1}) cursor.sort([("_id", pymongo.ASCENDING)]) future = Future() results = [] def callback(result, error): if error: raise error if result is not None: results.append(result) else: # Done iterating. future.set_result(True) cursor.each(callback) await future expected = [{"_id": i} for i in range(200)] self.assertEqual(expected, results) @gen_test async def test_to_list_argument_checking(self): # We need more than 10 documents so the cursor stays alive. await self.make_test_data() coll = self.collection cursor = coll.find() with self.assertRaises(ValueError): await cursor.to_list(-1) with self.assertRaises(TypeError): await cursor.to_list("foo") @gen_test async def test_to_list_with_length(self): await self.make_test_data() coll = self.collection cursor = coll.find().sort("_id") self.assertEqual([], (await cursor.to_list(0))) def expected(start, stop): return [{"_id": i} for i in range(start, stop)] self.assertEqual(expected(0, 10), (await cursor.to_list(10))) self.assertEqual(expected(10, 100), (await cursor.to_list(90))) # Test particularly rigorously around the 101-doc mark, since this is # where the first batch ends self.assertEqual(expected(100, 101), (await cursor.to_list(1))) self.assertEqual(expected(101, 102), (await cursor.to_list(1))) self.assertEqual(expected(102, 103), (await cursor.to_list(1))) self.assertEqual([], (await cursor.to_list(0))) self.assertEqual(expected(103, 105), (await cursor.to_list(2))) # Only 95 docs left, make sure length=100 doesn't error or hang self.assertEqual(expected(105, 200), (await cursor.to_list(100))) self.assertEqual(0, cursor.cursor_id) # Nothing left. self.assertEqual([], (await cursor.to_list(100))) @gen_test async def test_to_list_exc_info(self): await self.make_test_data() coll = self.collection cursor = coll.find() await cursor.to_list(length=10) await self.collection.drop() try: await cursor.to_list(length=None) except OperationFailure: _, _, tb = sys.exc_info() # The call tree should include PyMongo code we ran on a thread. formatted = "\n".join(traceback.format_tb(tb)) self.assertTrue( "_unpack_response" in formatted or "_check_command_response" in formatted ) @gen_test async def test_to_list_with_length_of_none(self): await self.make_test_data() collection = self.collection cursor = collection.find() docs = await cursor.to_list(None) # Unlimited. count = await collection.count_documents({}) self.assertEqual(count, len(docs)) @gen_test async def test_to_list_tailable(self): coll = self.collection cursor = coll.find(cursor_type=CursorType.TAILABLE) # Can't call to_list on tailable cursor. with self.assertRaises(InvalidOperation): await cursor.to_list(10) @env.require_version_min(3, 4) @gen_test async def test_to_list_with_chained_collation(self): await self.make_test_data() cursor = ( self.collection.find({}, {"_id": 1}) .sort([("_id", pymongo.ASCENDING)]) .collation(Collation("en")) ) expected = [{"_id": i} for i in range(200)] result = await cursor.to_list(length=1000) self.assertEqual(expected, result) @gen_test async def test_cursor_explicit_close(self): client, server = self.client_server(auto_ismaster=True) collection = client.test.coll cursor = collection.find() # With Tornado, simply accessing fetch_next starts the fetch. fetch_next = cursor.fetch_next request = await self.run_thread(server.receives, "find", "coll") request.replies({"cursor": {"id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) self.assertTrue(await fetch_next) async def mock_kill_cursors(): request = await self.run_thread(server.receives, "killCursors", "coll") request.ok() await gen.multi([cursor.close(), mock_kill_cursors()]) # Cursor reports it's alive because it has buffered data, even though # it's killed on the server. self.assertTrue(cursor.alive) self.assertEqual({"_id": 1}, cursor.next_object()) self.assertFalse(await cursor.fetch_next) self.assertFalse(cursor.alive) @gen_test async def test_each_cancel(self): await self.make_test_data() loop = self.io_loop collection = self.collection results = [] future = Future() def cancel(result, error): if error: future.set_exception(error) else: results.append(result) loop.add_callback(canceled) return False # Cancel iteration. def canceled(): try: self.assertFalse(cursor.delegate._Cursor__killed) self.assertTrue(cursor.alive) # Resume iteration cursor.each(each) except Exception as e: future.set_exception(e) def each(result, error): if error: future.set_exception(error) elif result: results.append(result) else: # Complete future.set_result(None) cursor = collection.find() cursor.each(cancel) await future self.assertEqual((await collection.count_documents({})), len(results)) @gen_test async def test_rewind(self): await self.collection.insert_many([{}, {}, {}]) cursor = self.collection.find().limit(2) count = 0 while await cursor.fetch_next: cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while await cursor.fetch_next: cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while await cursor.fetch_next: cursor.next_object() break cursor.rewind() while await cursor.fetch_next: cursor.next_object() count += 1 self.assertEqual(2, count) self.assertEqual(cursor, cursor.rewind()) @gen_test async def test_cursor_del(self): if "PyPy" in sys.version: raise SkipTest("PyPy") client, server = self.client_server(auto_ismaster=True) cursor = client.test.coll.find() future = cursor.fetch_next request = await self.run_thread(server.receives, "find", "coll") request.replies({"cursor": {"id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) await future # Complete the first fetch. # Dereference the cursor. del cursor # Let the event loop iterate once more to clear its references to # callbacks, allowing the cursor to be freed. await gen.sleep(0.1) request = await self.run_thread(server.receives, "killCursors", "coll") request.ok() @gen_test async def test_exhaust(self): if await server_is_mongos(self.cx): self.assertRaises(InvalidOperation, self.db.test.find, cursor_type=CursorType.EXHAUST) return cur = self.db.test.find(cursor_type=CursorType.EXHAUST) self.assertRaises(InvalidOperation, cur.limit, 5) cur = self.db.test.find(limit=5) self.assertRaises(InvalidOperation, cur.add_option, 64) cur = self.db.test.find() cur.add_option(64) self.assertRaises(InvalidOperation, cur.limit, 5) await self.db.drop_collection("test") # Insert enough documents to require more than one batch. await self.db.test.insert_many([{} for _ in range(150)]) client = self.motor_client(maxPoolSize=1) # Ensure a pool. await client.db.collection.find_one() pool = get_primary_pool(client) conns = pool.conns # Make sure the socket is returned after exhaustion. cur = client[self.db.name].test.find(cursor_type=CursorType.EXHAUST) has_next = await cur.fetch_next self.assertTrue(has_next) self.assertEqual(0, len(conns)) while await cur.fetch_next: cur.next_object() self.assertEqual(1, len(conns)) # Same as previous but with to_list instead of next_object. docs = await client[self.db.name].test.find(cursor_type=CursorType.EXHAUST).to_list(None) self.assertEqual(1, len(conns)) self.assertEqual((await self.db.test.count_documents({})), len(docs)) # If the Cursor instance is discarded before being # completely iterated we have to close and # discard the socket. conn = one(conns) cur = client[self.db.name].test.find(cursor_type=CursorType.EXHAUST).batch_size(1) await cur.fetch_next self.assertTrue(cur.next_object()) # Run at least one getMore to initiate the OP_MSG exhaust protocol. if env.version.at_least(4, 2): await cur.fetch_next self.assertTrue(cur.next_object()) self.assertEqual(0, len(conns)) if "PyPy" in sys.version: # Don't wait for GC or use gc.collect(), it's unreliable. await cur.close() del cur async def conn_closed(): return conn not in conns and conn.closed await wait_until( conn_closed, "close exhaust cursor socket", timeout=get_async_test_timeout() ) # The exhaust cursor's socket was discarded, although another may # already have been opened to send OP_KILLCURSORS. self.assertNotIn(conn, conns) self.assertTrue(conn.closed) def test_iter(self): # Iteration should be prohibited. with self.assertRaises(TypeError): for _ in self.db.test.find(): pass @gen_test async def test_close_with_docs_in_batch(self): # MOTOR-67 Killed cursor with docs batched is "alive", don't kill again. await self.make_test_data() # Ensure multiple batches. cursor = self.collection.find() await cursor.fetch_next await cursor.close() # Killed but still "alive": has a batch. self.cx.close() with warnings.catch_warnings(record=True) as w: del cursor # No-op, no error. self.assertEqual(0, len(w)) @gen_test async def test_aggregate_batch_size(self): listener = TestListener() cx = self.motor_client(event_listeners=[listener]) c = cx.motor_test.collection await c.delete_many({}) await c.insert_many({"_id": i} for i in range(3)) # Two ways of setting batchSize. cursor0 = c.aggregate([{"$sort": {"_id": 1}}]).batch_size(2) cursor1 = c.aggregate([{"$sort": {"_id": 1}}], batchSize=2) for cursor in cursor0, cursor1: lst = [] while await cursor.fetch_next: lst.append(cursor.next_object()) self.assertEqual(lst, [{"_id": 0}, {"_id": 1}, {"_id": 2}]) aggregate = listener.first_command_started("aggregate") self.assertEqual(aggregate.command["cursor"]["batchSize"], 2) getMore = listener.first_command_started("getMore") self.assertEqual(getMore.command["batchSize"], 2) @gen_test async def test_raw_batches(self): c = self.collection await c.delete_many({}) await c.insert_many({"_id": i} for i in range(4)) find = partial(c.find_raw_batches, {}) agg = partial(c.aggregate_raw_batches, [{"$sort": {"_id": 1}}]) for method in find, agg: cursor = method().batch_size(2) await cursor.fetch_next batch = cursor.next_object() self.assertEqual([{"_id": 0}, {"_id": 1}], bson.decode_all(batch)) lst = await method().batch_size(2).to_list(length=1) self.assertEqual([{"_id": 0}, {"_id": 1}], bson.decode_all(lst[0])) @gen_test async def test_generate_keys(self): c = self.collection KMS_PROVIDERS = {"local": {"key": b"\x00" * 96}} async with motor.MotorClientEncryption( KMS_PROVIDERS, "keyvault.datakeys", c, bson.codec_options.CodecOptions() ) as client_encryption: self.assertIsInstance(await client_encryption.get_keys(), motor.MotorCursor) class MotorCursorMaxTimeMSTest(MotorTest): def setUp(self): super().setUp() self.io_loop.run_sync(self.maybe_skip) def tearDown(self): self.io_loop.run_sync(self.disable_timeout) super().tearDown() async def maybe_skip(self): if await server_is_mongos(self.cx): raise SkipTest("mongos has no maxTimeAlwaysTimeOut fail point") cmdline = await get_command_line(self.cx) if "1" != safe_get(cmdline, "parsed.setParameter.enableTestCommands"): if "enableTestCommands=1" not in cmdline["argv"]: raise SkipTest("testing maxTimeMS requires failpoints") async def enable_timeout(self): await self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="alwaysOn") async def disable_timeout(self): await self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") @gen_test async def test_max_time_ms_query(self): # Cursor parses server timeout error in response to initial query. await self.enable_timeout() cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): await cursor.fetch_next cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): await cursor.to_list(10) with self.assertRaises(ExecutionTimeout): await self.collection.find_one(max_time_ms=100000) @gen_test(timeout=60) async def test_max_time_ms_getmore(self): # Cursor handles server timeout during getmore, also. await self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) await cursor.fetch_next cursor.next_object() # Test getmore timeout. await self.enable_timeout() with self.assertRaises(ExecutionTimeout): while await cursor.fetch_next: cursor.next_object() await cursor.close() # Send another initial query. await self.disable_timeout() cursor = self.collection.find().max_time_ms(100000) await cursor.fetch_next cursor.next_object() # Test getmore timeout. await self.enable_timeout() with self.assertRaises(ExecutionTimeout): await cursor.to_list(None) # Avoid 'IOLoop is closing' warning. await cursor.close() finally: # Cleanup. await self.disable_timeout() await self.collection.delete_many({}) @gen_test async def test_max_time_ms_each_query(self): # Cursor.each() handles server timeout during initial query. await self.enable_timeout() cursor = self.collection.find().max_time_ms(100000) future = Future() def callback(result, error): if error: future.set_exception(error) elif not result: # Done. future.set_result(None) with self.assertRaises(ExecutionTimeout): cursor.each(callback) await future @gen_test(timeout=30) async def test_max_time_ms_each_getmore(self): # Cursor.each() handles server timeout during getmore. await self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) await cursor.fetch_next cursor.next_object() future = Future() def callback(result, error): if error: future.set_exception(error) elif not result: # Done. future.set_result(None) await self.enable_timeout() with self.assertRaises(ExecutionTimeout): cursor.each(callback) await future await cursor.close() finally: # Cleanup. await self.disable_timeout() await self.collection.delete_many({}) if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_database.py000066400000000000000000000154551460060525600231400ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import unittest from test import env from test.tornado_tests import MotorTest import pymongo.database from bson import CodecOptions from bson.binary import JAVA_LEGACY from pymongo import ReadPreference, WriteConcern from pymongo.errors import CollectionInvalid, OperationFailure from pymongo.read_preferences import Secondary from tornado.testing import gen_test import motor class MotorDatabaseTest(MotorTest): @gen_test async def test_database(self): # Test that we can create a db directly, not just from MotorClient's # accessors db = motor.MotorDatabase(self.cx, "motor_test") # Make sure we got the right DB and it can do an operation self.assertEqual("motor_test", db.name) await db.test_collection.insert_one({"_id": 1}) doc = await db.test_collection.find_one({"_id": 1}) self.assertEqual(1, doc["_id"]) def test_collection_named_delegate(self): db = self.db self.assertTrue(isinstance(db.delegate, pymongo.database.Database)) self.assertTrue(isinstance(db["delegate"], motor.MotorCollection)) db.client.close() def test_call(self): # Prevents user error with nice message. try: self.cx.foo() except TypeError as e: self.assertTrue("no such method exists" in str(e)) else: self.fail("Expected TypeError") @env.require_version_min(3, 6) @gen_test async def test_aggregate(self): pipeline = [ {"$listLocalSessions": {}}, {"$limit": 1}, {"$addFields": {"dummy": "dummy field"}}, {"$project": {"_id": 0, "dummy": 1}}, ] expected = [{"dummy": "dummy field"}] cursor = self.cx.admin.aggregate(pipeline) docs = await cursor.to_list(10) self.assertEqual(expected, docs) @gen_test async def test_command(self): result = await self.cx.admin.command("buildinfo") # Make sure we got some sane result or other. self.assertEqual(1, result["ok"]) @gen_test async def test_create_collection(self): # Test creating collection, return val is wrapped in MotorCollection, # creating it again raises CollectionInvalid. db = self.db await db.drop_collection("test_collection2") collection = await db.create_collection("test_collection2") self.assertTrue(isinstance(collection, motor.MotorCollection)) self.assertTrue("test_collection2" in (await db.list_collection_names())) with self.assertRaises(CollectionInvalid): await db.create_collection("test_collection2") await db.drop_collection("test_collection2") # Test creating capped collection collection = await db.create_collection("test_capped", capped=True, size=4096) self.assertTrue(isinstance(collection, motor.MotorCollection)) self.assertEqual({"capped": True, "size": 4096}, (await db.test_capped.options())) await db.drop_collection("test_capped") @gen_test async def test_drop_collection(self): # Make sure we can pass a MotorCollection instance to drop_collection db = self.db collection = db.test_drop_collection await collection.insert_one({}) names = await db.list_collection_names() self.assertTrue("test_drop_collection" in names) await db.drop_collection(collection) names = await db.list_collection_names() self.assertFalse("test_drop_collection" in names) @gen_test async def test_validate_collection(self): db = self.db with self.assertRaises(TypeError): await db.validate_collection(5) with self.assertRaises(TypeError): await db.validate_collection(None) with self.assertRaises(OperationFailure): await db.validate_collection("test.doesnotexist") with self.assertRaises(OperationFailure): await db.validate_collection(db.test.doesnotexist) await db.test.insert_one({"dummy": "object"}) self.assertTrue(await db.validate_collection("test")) self.assertTrue(await db.validate_collection(db.test)) def test_get_collection(self): codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) write_concern = WriteConcern(w=2, j=True) coll = self.db.get_collection("foo", codec_options, ReadPreference.SECONDARY, write_concern) self.assertTrue(isinstance(coll, motor.MotorCollection)) self.assertEqual("foo", coll.name) self.assertEqual(codec_options, coll.codec_options) self.assertEqual(ReadPreference.SECONDARY, coll.read_preference) self.assertEqual(write_concern, coll.write_concern) pref = Secondary([{"dc": "sf"}]) coll = self.db.get_collection("foo", read_preference=pref) self.assertEqual(pref, coll.read_preference) self.assertEqual(self.db.codec_options, coll.codec_options) self.assertEqual(self.db.write_concern, coll.write_concern) def test_with_options(self): db = self.db codec_options = CodecOptions(tz_aware=True, uuid_representation=JAVA_LEGACY) write_concern = WriteConcern(w=2, j=True) db2 = db.with_options(codec_options, ReadPreference.SECONDARY, write_concern) self.assertTrue(isinstance(db2, motor.MotorDatabase)) self.assertEqual(codec_options, db2.codec_options) self.assertEqual(Secondary(), db2.read_preference) self.assertEqual(write_concern, db2.write_concern) pref = Secondary([{"dc": "sf"}]) db2 = db.with_options(read_preference=pref) self.assertEqual(pref, db2.read_preference) self.assertEqual(db.codec_options, db2.codec_options) self.assertEqual(db.write_concern, db2.write_concern) @gen_test async def test_cursor_command(self): db = self.db await db.test.drop() docs = [{"_id": i, "doc": i} for i in range(3)] await db.test.insert_many(docs) cursor = await db.cursor_command("find", "test") for i in range(3): item = await cursor.try_next() self.assertEqual(item, docs[i]) if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_grid_file.py000066400000000000000000000306751460060525600233210ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test GridFS with Motor, an asynchronous driver for MongoDB and Tornado.""" import datetime import sys import traceback import unittest from test import MockRequestHandler from test.tornado_tests import MotorTest from bson.objectid import ObjectId from gridfs.errors import NoFile from pymongo.errors import InvalidOperation from tornado.testing import gen_test import motor class MotorGridFileTest(MotorTest): async def _reset(self): await self.db.drop_collection("fs.files") await self.db.drop_collection("fs.chunks") await self.db.drop_collection("alt.files") await self.db.drop_collection("alt.chunks") def tearDown(self): self.io_loop.run_sync(self._reset) super().tearDown() @gen_test async def test_attributes(self): f = motor.MotorGridIn(self.db.fs, filename="test", foo="bar", content_type="text") await f.close() g = motor.MotorGridOut(self.db.fs, f._id) attr_names = ( "_id", "filename", "name", "name", "content_type", "length", "chunk_size", "upload_date", "aliases", "metadata", ) for attr_name in attr_names: self.assertRaises(InvalidOperation, getattr, g, attr_name) await g.open() for attr_name in attr_names: getattr(g, attr_name) @gen_test async def test_iteration(self): fs = motor.MotorGridFSBucket(self.db) _id = await fs.upload_from_stream("filename", b"foo") g = motor.MotorGridOut(self.db.fs, _id) # Iteration is prohibited. self.assertRaises(TypeError, iter, g) @gen_test async def test_basic(self): f = motor.MotorGridIn(self.db.fs, filename="test") await f.write(b"hello world") await f.close() self.assertEqual(1, (await self.db.fs.files.count_documents({}))) self.assertEqual(1, (await self.db.fs.chunks.count_documents({}))) g = motor.MotorGridOut(self.db.fs, f._id) self.assertEqual(b"hello world", (await g.read())) f = motor.MotorGridIn(self.db.fs, filename="test") await f.close() self.assertEqual(2, (await self.db.fs.files.count_documents({}))) self.assertEqual(1, (await self.db.fs.chunks.count_documents({}))) g = motor.MotorGridOut(self.db.fs, f._id) self.assertEqual(b"", (await g.read())) @gen_test async def test_readchunk(self): in_data = b"a" * 10 f = motor.MotorGridIn(self.db.fs, chunkSize=3) await f.write(in_data) await f.close() g = motor.MotorGridOut(self.db.fs, f._id) # This is starting to look like Lisp. self.assertEqual(3, len(await g.readchunk())) self.assertEqual(2, len(await g.read(2))) self.assertEqual(1, len(await g.readchunk())) self.assertEqual(3, len(await g.read(3))) self.assertEqual(1, len(await g.readchunk())) self.assertEqual(0, len(await g.readchunk())) @gen_test async def test_gridout_open_exc_info(self): g = motor.MotorGridOut(self.db.fs, "_id that doesn't exist") try: await g.open() except NoFile: _, _, tb = sys.exc_info() # The call tree should include PyMongo code we ran on a thread. formatted = "\n".join(traceback.format_tb(tb)) self.assertTrue("_ensure_file" in formatted) @gen_test async def test_alternate_collection(self): await self.db.alt.files.delete_many({}) await self.db.alt.chunks.delete_many({}) f = motor.MotorGridIn(self.db.alt) await f.write(b"hello world") await f.close() self.assertEqual(1, (await self.db.alt.files.count_documents({}))) self.assertEqual(1, (await self.db.alt.chunks.count_documents({}))) g = motor.MotorGridOut(self.db.alt, f._id) self.assertEqual(b"hello world", (await g.read())) @gen_test async def test_grid_in_default_opts(self): self.assertRaises(TypeError, motor.MotorGridIn, "foo") a = motor.MotorGridIn(self.db.fs) self.assertTrue(isinstance(a._id, ObjectId)) self.assertRaises(AttributeError, setattr, a, "_id", 5) self.assertEqual(None, a.filename) # This raises AttributeError because you can't directly set properties # in Motor, have to use set() def setter(): a.filename = "my_file" self.assertRaises(AttributeError, setter) # This method of setting attributes works in Motor await a.set("filename", "my_file") self.assertEqual("my_file", a.filename) self.assertEqual(None, a.content_type) await a.set("content_type", "text/html") self.assertEqual("text/html", a.content_type) self.assertRaises(AttributeError, getattr, a, "length") self.assertRaises(AttributeError, setattr, a, "length", 5) self.assertEqual(255 * 1024, a.chunk_size) self.assertRaises(AttributeError, setattr, a, "chunk_size", 5) self.assertRaises(AttributeError, getattr, a, "upload_date") self.assertRaises(AttributeError, setattr, a, "upload_date", 5) self.assertRaises(AttributeError, getattr, a, "aliases") await a.set("aliases", ["foo"]) self.assertEqual(["foo"], a.aliases) self.assertRaises(AttributeError, getattr, a, "metadata") await a.set("metadata", {"foo": 1}) self.assertEqual({"foo": 1}, a.metadata) await a.close() self.assertTrue(isinstance(a._id, ObjectId)) self.assertRaises(AttributeError, setattr, a, "_id", 5) self.assertEqual("my_file", a.filename) self.assertEqual("text/html", a.content_type) self.assertEqual(0, a.length) self.assertRaises(AttributeError, setattr, a, "length", 5) self.assertEqual(255 * 1024, a.chunk_size) self.assertRaises(AttributeError, setattr, a, "chunk_size", 5) self.assertTrue(isinstance(a.upload_date, datetime.datetime)) self.assertRaises(AttributeError, setattr, a, "upload_date", 5) self.assertEqual(["foo"], a.aliases) self.assertEqual({"foo": 1}, a.metadata) @gen_test async def test_grid_in_custom_opts(self): self.assertRaises(TypeError, motor.MotorGridIn, "foo") a = motor.MotorGridIn( self.db.fs, _id=5, filename="my_file", contentType="text/html", chunkSize=1000, aliases=["foo"], metadata={"foo": 1, "bar": 2}, bar=3, baz="hello", ) self.assertEqual(5, a._id) self.assertEqual("my_file", a.filename) self.assertEqual("text/html", a.content_type) self.assertEqual(1000, a.chunk_size) self.assertEqual(["foo"], a.aliases) self.assertEqual({"foo": 1, "bar": 2}, a.metadata) self.assertEqual(3, a.bar) self.assertEqual("hello", a.baz) self.assertRaises(AttributeError, getattr, a, "mike") b = motor.MotorGridIn(self.db.fs, content_type="text/html", chunk_size=1000, baz=100) self.assertEqual("text/html", b.content_type) self.assertEqual(1000, b.chunk_size) self.assertEqual(100, b.baz) @gen_test async def test_grid_out_default_opts(self): self.assertRaises(TypeError, motor.MotorGridOut, "foo") gout = motor.MotorGridOut(self.db.fs, 5) with self.assertRaises(NoFile): await gout.open() a = motor.MotorGridIn(self.db.fs) await a.close() b = await motor.MotorGridOut(self.db.fs, a._id).open() self.assertEqual(a._id, b._id) self.assertEqual(0, b.length) self.assertEqual(None, b.content_type) self.assertEqual(255 * 1024, b.chunk_size) self.assertTrue(isinstance(b.upload_date, datetime.datetime)) self.assertEqual(None, b.aliases) self.assertEqual(None, b.metadata) @gen_test async def test_grid_out_custom_opts(self): one = motor.MotorGridIn( self.db.fs, _id=5, filename="my_file", contentType="text/html", chunkSize=1000, aliases=["foo"], metadata={"foo": 1, "bar": 2}, bar=3, baz="hello", ) await one.write(b"hello world") await one.close() two = await motor.MotorGridOut(self.db.fs, 5).open() self.assertEqual(5, two._id) self.assertEqual(11, two.length) self.assertEqual("text/html", two.content_type) self.assertEqual(1000, two.chunk_size) self.assertTrue(isinstance(two.upload_date, datetime.datetime)) self.assertEqual(["foo"], two.aliases) self.assertEqual({"foo": 1, "bar": 2}, two.metadata) self.assertEqual(3, two.bar) @gen_test async def test_grid_out_file_document(self): one = motor.MotorGridIn(self.db.fs) await one.write(b"foo bar") await one.close() file_document = await self.db.fs.files.find_one() two = motor.MotorGridOut(self.db.fs, file_document=file_document) self.assertEqual(b"foo bar", (await two.read())) file_document = await self.db.fs.files.find_one() three = motor.MotorGridOut(self.db.fs, 5, file_document) self.assertEqual(b"foo bar", (await three.read())) gridout = motor.MotorGridOut(self.db.fs, file_document={}) with self.assertRaises(NoFile): await gridout.open() @gen_test async def test_write_file_like(self): one = motor.MotorGridIn(self.db.fs) await one.write(b"hello world") await one.close() two = motor.MotorGridOut(self.db.fs, one._id) three = motor.MotorGridIn(self.db.fs) await three.write(two) await three.close() four = motor.MotorGridOut(self.db.fs, three._id) self.assertEqual(b"hello world", (await four.read())) @gen_test async def test_set_after_close(self): f = motor.MotorGridIn(self.db.fs, _id="foo", bar="baz") self.assertEqual("foo", f._id) self.assertEqual("baz", f.bar) self.assertRaises(AttributeError, getattr, f, "baz") self.assertRaises(AttributeError, getattr, f, "uploadDate") self.assertRaises(AttributeError, setattr, f, "_id", 5) f.bar = "foo" f.baz = 5 self.assertEqual("foo", f.bar) self.assertEqual(5, f.baz) self.assertRaises(AttributeError, getattr, f, "uploadDate") await f.close() self.assertEqual("foo", f._id) self.assertEqual("foo", f.bar) self.assertEqual(5, f.baz) self.assertTrue(f.uploadDate) self.assertRaises(AttributeError, setattr, f, "_id", 5) await f.set("bar", "a") await f.set("baz", "b") self.assertRaises(AttributeError, setattr, f, "upload_date", 5) g = await motor.MotorGridOut(self.db.fs, f._id).open() self.assertEqual("a", g.bar) self.assertEqual("b", g.baz) @gen_test async def test_stream_to_handler(self): fs = motor.MotorGridFSBucket(self.db) for content_length in (0, 1, 100, 100 * 1000): _id = await fs.upload_from_stream("filename", b"a" * content_length) gridout = await fs.open_download_stream(_id) handler = MockRequestHandler() await gridout.stream_to_handler(handler) self.assertEqual(content_length, handler.n_written) await fs.delete(_id) @gen_test async def test_exception_closed(self): contents = b"Imagine this is some important data..." with self.assertRaises(ConnectionError): async with motor.MotorGridIn(self.db.fs, _id="foo", bar="baz") as infile: infile.write(contents) raise ConnectionError("Test exception") self.assertTrue(infile.closed) if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_gridfsbucket.py000066400000000000000000000042561460060525600240450ustar00rootroot00000000000000# Copyright 2016 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. """Test MotorGridFSBucket.""" from io import BytesIO from test.tornado_tests import MotorTest from gridfs.errors import NoFile from tornado.testing import gen_test import motor class MotorGridFSBucketTest(MotorTest): async def _reset(self): await self.db.drop_collection("fs.files") await self.db.drop_collection("fs.chunks") await self.db.drop_collection("alt.files") await self.db.drop_collection("alt.chunks") def setUp(self): super().setUp() self.io_loop.run_sync(self._reset) self.bucket = motor.MotorGridFSBucket(self.db) def tearDown(self): self.io_loop.run_sync(self._reset) super().tearDown() @gen_test async def test_basic(self): oid = await self.bucket.upload_from_stream("test_filename", b"hello world") gout = await self.bucket.open_download_stream(oid) self.assertEqual(b"hello world", (await gout.read())) self.assertEqual(1, (await self.db.fs.files.count_documents({}))) self.assertEqual(1, (await self.db.fs.chunks.count_documents({}))) await self.bucket.delete(oid) with self.assertRaises(NoFile): await self.bucket.open_download_stream(oid) self.assertEqual(0, (await self.db.fs.files.count_documents({}))) self.assertEqual(0, (await self.db.fs.chunks.count_documents({}))) gin = self.bucket.open_upload_stream("test_filename") await gin.write(b"hello world") await gin.close() dst = BytesIO() await self.bucket.download_to_stream(gin._id, dst) self.assertEqual(b"hello world", dst.getvalue()) motor-3.4.0/test/tornado_tests/test_motor_ipv6.py000066400000000000000000000037651460060525600222610ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import test import unittest from test import SkipTest from test.test_environment import connected, db_password, db_user, env from test.tornado_tests import MotorTest from pymongo import MongoClient from pymongo.errors import ConnectionFailure from tornado.testing import gen_test import motor class MotorIPv6Test(MotorTest): @gen_test async def test_ipv6(self): assert env.host in ( "localhost", "127.0.0.1", ), "This unittest isn't written to test IPv6 with host %s" % repr(env.host) try: connected( MongoClient( "[::1]", username=db_user, password=db_password, serverSelectionTimeoutMS=100 ) ) except ConnectionFailure: # Either mongod was started without --ipv6 # or the OS doesn't support it (or both). raise SkipTest("No IPV6") if test.env.auth: cx_string = "mongodb://%s:%s@[::1]:%d" % (db_user, db_password, env.port) else: cx_string = "mongodb://[::1]:%d" % env.port cx = motor.MotorClient(cx_string, io_loop=self.io_loop) collection = cx.motor_test.test_collection await collection.insert_one({"dummy": "object"}) self.assertTrue(await collection.find_one({"dummy": "object"})) if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_replica_set.py000066400000000000000000000050671460060525600236640ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test replica set MotorClient.""" import test import unittest from test import SkipTest from test.test_environment import env from test.tornado_tests import MotorReplicaSetTestBase, MotorTest import pymongo import pymongo.auth import pymongo.errors from tornado import gen from tornado.testing import gen_test import motor import motor.core class MotorReplicaSetTest(MotorReplicaSetTestBase): def test_io_loop(self): with self.assertRaises(TypeError): motor.MotorClient(test.env.rs_uri, io_loop="foo") @gen_test async def test_connection_failure(self): # Assuming there isn't anything actually running on this port client = motor.MotorClient( "localhost:8765", replicaSet="rs", io_loop=self.io_loop, serverSelectionTimeoutMS=10 ) # Test the Future interface. with self.assertRaises(pymongo.errors.ConnectionFailure): await client.admin.command("ismaster") @gen_test async def test_open_concurrent(self): # MOTOR-66: don't block on PyMongo's __monitor_lock, but also don't # spawn multiple monitors. c = self.motor_rsc() await gen.multi([c.db.collection.find_one(), c.db.collection.find_one()]) class TestReplicaSetClientAgainstStandalone(MotorTest): """This is a funny beast -- we want to run tests for a replica set MotorClient but only if the database at DB_IP and DB_PORT is a standalone. """ def setUp(self): super().setUp() if test.env.is_replica_set: raise SkipTest("Connected to a replica set, not a standalone mongod") @gen_test async def test_connect(self): with self.assertRaises(pymongo.errors.ServerSelectionTimeoutError): await motor.MotorClient( "%s:%s" % (env.host, env.port), replicaSet="anything", io_loop=self.io_loop, serverSelectionTimeoutMS=10, ).test.test.find_one() if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_session.py000066400000000000000000000176711460060525600230610ustar00rootroot00000000000000# Copyright 2017-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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import copy import sys import unittest from test import SkipTest from test.test_environment import env from test.tornado_tests import MotorTest from test.utils import TestListener, session_ids from pymongo import IndexModel, InsertOne from pymongo.errors import InvalidOperation from tornado.testing import gen_test class MotorSessionTest(MotorTest): @classmethod def setUpClass(cls): super().setUpClass() if not env.sessions_enabled: raise SkipTest("Sessions not supported") async def _test_ops(self, client, *ops): listener = client.options.event_listeners[0] for f, args, kw in ops: # Simulate "async with" on all Pythons. s = await client.start_session() try: listener.results.clear() # In case "f" modifies its inputs. args2 = copy.copy(args) kw2 = copy.copy(kw) kw2["session"] = s await f(*args2, **kw2) for event in listener.results["started"]: self.assertTrue( "lsid" in event.command, "%s sent no lsid with %s" % (f.__name__, event.command_name), ) self.assertEqual( s.session_id, event.command["lsid"], "%s sent wrong lsid with %s" % (f.__name__, event.command_name), ) self.assertFalse(s.has_ended) finally: await s.end_session() with self.assertRaises(InvalidOperation) as ctx: await f(*args2, **kw2) self.assertIn("ended session", str(ctx.exception)) # No explicit session. for f, args, kw in ops: listener.results.clear() await f(*args, **kw) self.assertGreaterEqual(len(listener.results["started"]), 1) lsids = [] for event in listener.results["started"]: self.assertTrue( "lsid" in event.command, "%s sent no lsid with %s" % (f.__name__, event.command_name), ) lsids.append(event.command["lsid"]) if "PyPy" not in sys.version: # Server session was returned to pool. Ignore interpreters with # non-deterministic GC. for lsid in lsids: self.assertIn( lsid, session_ids(client), "%s did not return implicit session to pool" % (f.__name__,), ) @gen_test async def test_database(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) db = client.pymongo_test ops = [ (db.command, ["ping"], {}), (db.drop_collection, ["collection"], {}), (db.create_collection, ["collection"], {}), (db.list_collection_names, [], {}), ] await self._test_ops(client, *ops) @gen_test(timeout=30) async def test_collection(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) await client.drop_database("motor_test") coll = client.motor_test.test_collection async def list_indexes(session=None): await coll.list_indexes(session=session).to_list(length=None) async def aggregate(session=None): await coll.aggregate([], session=session).to_list(length=None) # Test some collection methods - the rest are in test_cursor. await self._test_ops( client, (coll.drop, [], {}), (coll.bulk_write, [[InsertOne({})]], {}), (coll.insert_one, [{}], {}), (coll.insert_many, [[{}, {}]], {}), (coll.replace_one, [{}, {}], {}), (coll.update_one, [{}, {"$set": {"a": 1}}], {}), (coll.update_many, [{}, {"$set": {"a": 1}}], {}), (coll.delete_one, [{}], {}), (coll.delete_many, [{}], {}), (coll.find_one_and_replace, [{}, {}], {}), (coll.find_one_and_update, [{}, {"$set": {"a": 1}}], {}), (coll.find_one_and_delete, [{}, {}], {}), (coll.rename, ["collection2"], {}), # Drop collection2 between tests of "rename", above. (client.motor_test.drop_collection, ["collection2"], {}), (coll.distinct, ["a"], {}), (coll.find_one, [], {}), (coll.count_documents, [{}], {}), (coll.create_indexes, [[IndexModel("a")]], {}), (coll.create_index, ["a"], {}), (coll.drop_index, ["a_1"], {}), (coll.drop_indexes, [], {}), (list_indexes, [], {}), (coll.index_information, [], {}), (coll.options, [], {}), (aggregate, [], {}), ) @gen_test async def test_cursor(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) await self.make_test_data() coll = client.motor_test.test_collection s = await client.start_session() # Simulate "async with" on all Pythons. try: listener.results.clear() cursor = coll.find(session=s) await cursor.to_list(length=None) self.assertEqual(len(listener.results["started"]), 2) for event in listener.results["started"]: self.assertTrue( "lsid" in event.command, "find sent no lsid with %s" % (event.command_name,) ) self.assertEqual( s.session_id, event.command["lsid"], "find sent wrong lsid with %s" % (event.command_name,), ) finally: await s.end_session() with self.assertRaises(InvalidOperation) as ctx: await coll.find(session=s).to_list(length=None) self.assertIn("ended session", str(ctx.exception)) # No explicit session. listener.results.clear() cursor = coll.find() await cursor.to_list(length=None) self.assertEqual(len(listener.results["started"]), 2) event0 = listener.first_command_started() self.assertTrue( "lsid" in event0.command, "find sent no lsid with %s" % (event0.command_name,) ) lsid = event0.command["lsid"] for event in listener.results["started"][1:]: self.assertTrue( "lsid" in event.command, "find sent no lsid with %s" % (event.command_name,) ) self.assertEqual( lsid, event.command["lsid"], "find sent wrong lsid with %s" % (event.command_name,) ) @gen_test async def test_options(self): s = await self.cx.start_session() self.assertTrue(s.options.causal_consistency) s = await self.cx.start_session(False) self.assertFalse(s.options.causal_consistency) s = await self.cx.start_session(causal_consistency=True) self.assertTrue(s.options.causal_consistency) s = await self.cx.start_session(causal_consistency=False) self.assertFalse(s.options.causal_consistency) if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_ssl.py000066400000000000000000000126641460060525600221740ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test Motor, an asynchronous driver for MongoDB and Tornado.""" try: import ssl except ImportError: ssl = None import test from test import SkipTest from test.test_environment import CA_PEM, CLIENT_PEM, env from test.tornado_tests import MotorTest from pymongo.errors import ConfigurationError, ConnectionFailure from tornado.testing import gen_test import motor # Start a mongod instance like: # # mongod \ # --sslOnNormalPorts \ # --sslPEMKeyFile test/certificates/server.pem \ # --sslCAFile test/certificates/ca.pem # # Also, make sure you have 'server' as an alias for localhost in /etc/hosts class MotorSSLTest(MotorTest): ssl = True def setUp(self): if not test.env.server_is_resolvable: raise SkipTest("The hostname 'server' must be a localhost alias") super().setUp() def test_config_ssl(self): self.assertRaises(ValueError, motor.MotorClient, tls="foo") self.assertRaises( ConfigurationError, motor.MotorClient, tls=False, tlsCertificateKeyFile=CLIENT_PEM ) self.assertRaises(IOError, motor.MotorClient, tlsCertificateKeyFile="NoFile") self.assertRaises(TypeError, motor.MotorClient, tlsCertificateKeyFile=True) @gen_test async def test_cert_ssl(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("can't test with auth") client = motor.MotorClient( env.host, env.port, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.io_loop, ) await client.db.collection.find_one() response = await client.admin.command("ismaster") if "setName" in response: client = self.motor_rsc(tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM) await client.db.collection.find_one() @gen_test async def test_cert_ssl_validation(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("can't test with auth") client = motor.MotorClient( env.host, env.port, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.io_loop, ) await client.db.collection.find_one() response = await client.admin.command("ismaster") if "setName" in response: client = motor.MotorClient( env.host, env.port, replicaSet=response["setName"], tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.io_loop, ) await client.db.collection.find_one() @gen_test async def test_cert_ssl_validation_none(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("can't test with auth") client = motor.MotorClient( test.env.fake_hostname_uri, tlsCertificateKeyFile=CLIENT_PEM, tlsAllowInvalidCertificates=True, tlsCAFile=CA_PEM, io_loop=self.io_loop, ) await client.admin.command("ismaster") @gen_test async def test_cert_ssl_validation_hostname_fail(self): if not test.env.mongod_validates_client_cert: raise SkipTest("No mongod available over SSL with certs") if test.env.auth: raise SkipTest("can't test with auth") client = motor.MotorClient( env.host, env.port, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.io_loop, ) response = await client.admin.command("ismaster") with self.assertRaises(ConnectionFailure): # Create client with hostname 'server', not 'localhost', # which is what the server cert presents. client = motor.MotorClient( test.env.fake_hostname_uri, serverSelectionTimeoutMS=100, tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.io_loop, ) await client.db.collection.find_one() if "setName" in response: with self.assertRaises(ConnectionFailure): client = motor.MotorClient( test.env.fake_hostname_uri, serverSelectionTimeoutMS=100, replicaSet=response["setName"], tlsCertificateKeyFile=CLIENT_PEM, tlsCAFile=CA_PEM, io_loop=self.io_loop, ) await client.db.collection.find_one() motor-3.4.0/test/tornado_tests/test_motor_transaction.py000066400000000000000000000151541460060525600237150ustar00rootroot00000000000000# Copyright 2018-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 test.utils import TestListener from pymongo.read_concern import ReadConcern """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import unittest from test.test_environment import env from test.tornado_tests import MotorTest from pymongo import ReadPreference, WriteConcern from pymongo.errors import ConnectionFailure, OperationFailure from tornado.testing import gen_test from motor import core class PatchSessionTimeout: """Patches the client_session's with_transaction timeout for testing.""" def __init__(self, mock_timeout): self.real_timeout = core._WITH_TRANSACTION_RETRY_TIME_LIMIT self.mock_timeout = mock_timeout def __enter__(self): core._WITH_TRANSACTION_RETRY_TIME_LIMIT = self.mock_timeout return self def __exit__(self, exc_type, exc_val, exc_tb): core._WITH_TRANSACTION_RETRY_TIME_LIMIT = self.real_timeout class TestTransactionsConvenientAPI(MotorTest): @env.require_transactions @gen_test async def test_basic(self): # Create the collection. await self.collection.insert_one({}) async def coro(session): await self.collection.insert_one({"_id": 1}, session=session) async with await self.cx.start_session() as s: await s.with_transaction( coro, read_concern=ReadConcern("local"), write_concern=WriteConcern("majority"), read_preference=ReadPreference.PRIMARY, max_commit_time_ms=30000, ) doc = await self.collection.find_one({"_id": 1}) self.assertEqual(doc, {"_id": 1}) @env.require_transactions @gen_test async def test_callback_raises_custom_error(self): class _MyException(Exception): pass async def coro_raise_error(_): raise _MyException() async with await self.cx.start_session() as s: with self.assertRaises(_MyException): await s.with_transaction(coro_raise_error) @env.require_transactions @gen_test async def test_callback_returns_value(self): async def callback(_): return "Foo" async with await self.cx.start_session() as s: self.assertEqual(await s.with_transaction(callback), "Foo") await self.db.test.insert_one({}) async def callback(session): await self.db.test.insert_one({}, session=session) return "Foo" async with await self.cx.start_session() as s: self.assertEqual(await s.with_transaction(callback), "Foo") @env.require_transactions @gen_test async def test_callback_not_retried_after_timeout(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) coll = client[self.db.name].test async def callback(session): await coll.insert_one({}, session=session) err = { "ok": 0, "errmsg": "Transaction 7819 has been aborted.", "code": 251, "codeName": "NoSuchTransaction", "errorLabels": ["TransientTransactionError"], } raise OperationFailure(err["errmsg"], err["code"], err) # Create the collection. await coll.insert_one({}) listener.results.clear() async with await client.start_session() as s: with PatchSessionTimeout(0): with self.assertRaises(OperationFailure): await s.with_transaction(callback) self.assertEqual(listener.started_command_names(), ["insert", "abortTransaction"]) @env.require_transactions @gen_test async def test_callback_not_retried_after_commit_timeout(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) coll = client[self.db.name].test async def callback(session): await coll.insert_one({}, session=session) # Create the collection. await coll.insert_one({}) await self.set_fail_point( client, { "configureFailPoint": "failCommand", "mode": {"times": 1}, "data": { "failCommands": ["commitTransaction"], "errorCode": 251, # NoSuchTransaction }, }, ) listener.results.clear() async with await client.start_session() as s: with PatchSessionTimeout(0): with self.assertRaises(OperationFailure): await s.with_transaction(callback) self.assertEqual(listener.started_command_names(), ["insert", "commitTransaction"]) await self.set_fail_point(client, {"configureFailPoint": "failCommand", "mode": "off"}) @env.require_transactions @gen_test async def test_commit_not_retried_after_timeout(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) coll = client[self.db.name].test async def callback(session): await coll.insert_one({}, session=session) # Create the collection. await coll.insert_one({}) await self.set_fail_point( client, { "configureFailPoint": "failCommand", "mode": {"times": 2}, "data": {"failCommands": ["commitTransaction"], "closeConnection": True}, }, ) listener.results.clear() async with await client.start_session() as s: with PatchSessionTimeout(0): with self.assertRaises(ConnectionFailure): await s.with_transaction(callback) # One insert for the callback and two commits (includes the automatic # retry). self.assertEqual( listener.started_command_names(), ["insert", "commitTransaction", "commitTransaction"] ) await self.set_fail_point(client, {"configureFailPoint": "failCommand", "mode": "off"}) if __name__ == "__main__": unittest.main() motor-3.4.0/test/tornado_tests/test_motor_web.py000066400000000000000000000212631460060525600221430ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Test utilities for using Motor with Tornado web applications.""" import datetime import email import re import test import time import unittest from test.test_environment import CA_PEM, CLIENT_PEM, env import gridfs from tornado.testing import AsyncHTTPTestCase from tornado.web import Application import motor import motor.web from motor.motor_gridfs import _hash_gridout # We're using Tornado's AsyncHTTPTestCase instead of our own MotorTestCase for # the convenience of self.fetch(). class GridFSHandlerTestBase(AsyncHTTPTestCase): def setUp(self): super().setUp() self.fs = gridfs.GridFS(test.env.sync_cx.motor_test) # Make a 500k file in GridFS with filename 'foo' self.contents = b"Jesse" * 100 * 1024 # Record when we created the file, to check the Last-Modified header self.put_start = datetime.datetime.now(datetime.timezone.utc).replace( microsecond=0, tzinfo=None ) file_id = "id" self.file_id = file_id self.fs.delete(self.file_id) self.fs.put(self.contents, _id=file_id, filename="foo", content_type="my type") item = self.fs.get(file_id) self.contents_hash = _hash_gridout(item) self.put_end = datetime.datetime.now(datetime.timezone.utc).replace( microsecond=0, tzinfo=None ) self.assertTrue(self.fs.get_last_version("foo")) def motor_db(self, **kwargs): if env.mongod_started_with_ssl: kwargs.setdefault("tlsCAFile", CA_PEM) kwargs.setdefault("tlsCertificateKeyFile", CLIENT_PEM) kwargs.setdefault("tls", env.mongod_started_with_ssl) client = motor.MotorClient(test.env.uri, io_loop=self.io_loop, **kwargs) return client.motor_test def tearDown(self): self.fs.delete(self.file_id) super().tearDown() def get_app(self): return Application([("/(.+)", motor.web.GridFSHandler, {"database": self.motor_db()})]) def stop(self, *args, **kwargs): # A stop() method more permissive about the number of its positional # arguments than AsyncHTTPTestCase.stop if len(args) == 1: AsyncHTTPTestCase.stop(self, args[0], **kwargs) else: AsyncHTTPTestCase.stop(self, args, **kwargs) def parse_date(self, d): date_tuple = email.utils.parsedate(d) return datetime.datetime.fromtimestamp(time.mktime(date_tuple)) def last_mod(self, response): """Parse the 'Last-Modified' header from an HTTP response into a datetime. """ return self.parse_date(response.headers["Last-Modified"]) def expires(self, response): return self.parse_date(response.headers["Expires"]) class GridFSHandlerTest(GridFSHandlerTestBase): def test_basic(self): # First request response = self.fetch("/foo") self.assertEqual(200, response.code) self.assertEqual(self.contents, response.body) self.assertEqual(len(self.contents), int(response.headers["Content-Length"])) self.assertEqual("my type", response.headers["Content-Type"]) self.assertEqual("public", response.headers["Cache-Control"]) self.assertTrue("Expires" not in response.headers) etag = response.headers["Etag"] last_mod_dt = self.last_mod(response) self.assertEqual(self.contents_hash, etag.strip('"')) self.assertTrue(self.put_start <= last_mod_dt <= self.put_end) # Now check we get 304 NOT MODIFIED responses as appropriate for ims_value in (last_mod_dt, last_mod_dt + datetime.timedelta(seconds=1)): response = self.fetch("/foo", if_modified_since=ims_value) self.assertEqual(304, response.code) self.assertEqual(b"", response.body) # If-Modified-Since in the past, get whole response back response = self.fetch("/foo", if_modified_since=last_mod_dt - datetime.timedelta(seconds=1)) self.assertEqual(200, response.code) self.assertEqual(self.contents, response.body) # Matching Etag response = self.fetch("/foo", headers={"If-None-Match": etag}) self.assertEqual(304, response.code) self.assertEqual(b"", response.body) # Mismatched Etag response = self.fetch("/foo", headers={"If-None-Match": etag + "a"}) self.assertEqual(200, response.code) self.assertEqual(self.contents, response.body) def test_404(self): response = self.fetch("/bar") self.assertEqual(404, response.code) def test_head(self): response = self.fetch("/foo", method="HEAD") # Get Etag and parse Last-Modified into a datetime etag = response.headers["Etag"] last_mod_dt = self.last_mod(response) # Test the result self.assertEqual(200, response.code) self.assertEqual(b"", response.body) # Empty body for HEAD request self.assertEqual(len(self.contents), int(response.headers["Content-Length"])) self.assertEqual("my type", response.headers["Content-Type"]) self.assertEqual(self.contents_hash, etag.strip('"')) self.assertTrue(self.put_start <= last_mod_dt <= self.put_end) self.assertEqual("public", response.headers["Cache-Control"]) def test_content_type(self): # Check that GridFSHandler uses file extension to guess Content-Type # if not provided for filename, expected_type in [ ("foo.jpg", "jpeg"), ("foo.png", "png"), ("ht.html", "html"), ]: # 'fs' is PyMongo's blocking GridFS self.fs.put(b"", filename=filename) for method in "GET", "HEAD": response = self.fetch("/" + filename, method=method) self.assertEqual(200, response.code) # mimetypes are platform-defined, be fuzzy self.assertIn(expected_type, response.headers["Content-Type"].lower()) class TZAwareGridFSHandlerTest(GridFSHandlerTestBase): def motor_db(self): return super().motor_db(tz_aware=True) def test_tz_aware(self): now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) ago = now - datetime.timedelta(minutes=10) hence = now + datetime.timedelta(minutes=10) response = self.fetch("/foo", if_modified_since=ago) self.assertEqual(200, response.code) response = self.fetch("/foo", if_modified_since=hence) self.assertEqual(304, response.code) class CustomGridFSHandlerTest(GridFSHandlerTestBase): def get_app(self): class CustomGridFSHandler(motor.web.GridFSHandler): def get_gridfs_file(self, bucket, filename, request): # Test overriding the get_gridfs_file() method, path is # interpreted as file_id instead of filename. return bucket.open_download_stream(file_id=filename) def get_cache_time(self, path, modified, mime_type): return 10 def set_extra_headers(self, path, gridout): self.set_header("quux", "fizzledy") return Application([("/(.+)", CustomGridFSHandler, {"database": self.motor_db()})]) def test_get_gridfs_file(self): # We overrode get_gridfs_file so we expect getting by filename *not* to # work now; we'll get a 404. We have to get by file_id now. response = self.fetch("/foo") self.assertEqual(404, response.code) response = self.fetch("/" + str(self.file_id)) self.assertEqual(200, response.code) self.assertEqual(self.contents, response.body) cache_control = response.headers["Cache-Control"] self.assertTrue(re.match(r"max-age=\d+", cache_control)) self.assertEqual(10, int(cache_control.split("=")[1])) expires = self.expires(response) # It should expire about 10 seconds from now self.assertTrue( datetime.timedelta(seconds=8) < expires - datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) < datetime.timedelta(seconds=12) ) self.assertEqual("fizzledy", response.headers["quux"]) if __name__ == "__main__": unittest.main() motor-3.4.0/test/utils.py000066400000000000000000000111511460060525600153520ustar00rootroot00000000000000# Copyright 2012-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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 collections import defaultdict from bson import SON from pymongo import monitoring """Utilities for testing Motor with any framework.""" import contextlib import functools import os import time import warnings # mypy: ignore-errors def one(s): """Get one element of a set""" return next(iter(s)) def safe_get(dct, dotted_key, default=None): for key in dotted_key.split("."): if key not in dct: return default dct = dct[key] return dct # Use as a decorator or in a "with" statement. def ignore_deprecations(fn=None): if fn: @functools.wraps(fn) def wrapper(*args, **kwargs): with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) return fn(*args, **kwargs) return wrapper else: @contextlib.contextmanager def ignore_deprecations_context(): with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) yield return ignore_deprecations_context() def get_primary_pool(client): for s in client.delegate._topology._servers.values(): if s.description.is_writable: return s.pool # Ignore auth commands like saslStart, so we can assert lsid is in all commands. class TestListener(monitoring.CommandListener): __test__ = False def __init__(self): self.results = defaultdict(list) def started(self, event): if not event.command_name.startswith("sasl"): self.results["started"].append(event) def succeeded(self, event): if not event.command_name.startswith("sasl"): self.results["succeeded"].append(event) def failed(self, event): if not event.command_name.startswith("sasl"): self.results["failed"].append(event) def first_command_started(self, name=None): assert len(self.results["started"]) >= 1, "No command-started events" if name: for result in self.results["started"]: if result.command_name == name: return result else: return self.results["started"][0] def started_command_names(self): """Return list of command names started.""" return [event.command_name for event in self.results["started"]] def session_ids(client): return [s.session_id for s in client.delegate._topology._session_pool] def create_user(authdb, user, pwd=None, roles=None, **kwargs): cmd = SON([("createUser", user)]) # X509 doesn't use a password if pwd: cmd["pwd"] = pwd cmd["roles"] = roles or ["root"] cmd.update(**kwargs) return authdb.command(cmd) def get_async_test_timeout(default=5): """Get the global timeout setting for async tests. Returns a float, the timeout in seconds. """ try: timeout = float(os.environ.get("ASYNC_TEST_TIMEOUT")) return max(timeout, default) except (ValueError, TypeError): return default async def wait_until(predicate, success_description, timeout=10): """Copied from PyMongo's test.utils.wait_until. Wait up to 10 seconds (by default) for predicate to be true. The predicate must be an awaitable. Returns the predicate's first true value. """ start = time.time() interval = min(float(timeout) / 100, 0.1) while True: retval = await predicate() if retval: return retval if time.time() - start > timeout: raise AssertionError("Didn't ever %s" % success_description) time.sleep(interval) class FailPoint: def __init__(self, client, command_args): self.client = client self.cmd_on = SON([("configureFailPoint", "failCommand")]) self.cmd_on.update(command_args) async def __aenter__(self): await self.client.admin.command(self.cmd_on) async def __aexit__(self, exc_type, exc, tb): await self.client.admin.command( "configureFailPoint", self.cmd_on["configureFailPoint"], mode="off" ) motor-3.4.0/test/version.py000066400000000000000000000060261460060525600157040ustar00rootroot00000000000000# Copyright 2009-2015 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # 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. """Some tools for running tests based on MongoDB server version.""" # mypy: ignore-errors class Version(tuple): """Copied from PyMongo's test.version submodule.""" def __new__(cls, *version): padded_version = cls._padded(version, 4) return super().__new__(cls, tuple(padded_version)) @classmethod def _padded(cls, iter, length, padding=0): as_list = list(iter) if len(as_list) < length: for _ in range(length - len(as_list)): as_list.append(padding) return as_list @classmethod def from_string(cls, version_string): mod = 0 bump_patch_level = False if version_string.endswith("+"): version_string = version_string[0:-1] mod = 1 elif version_string.endswith("-pre-"): version_string = version_string[0:-5] mod = -1 elif version_string.endswith("-"): version_string = version_string[0:-1] mod = -1 # Deal with '-rcX' substrings if "-rc" in version_string: version_string = version_string[0 : version_string.find("-rc")] mod = -1 # Deal with git describe generated substrings elif "-" in version_string: version_string = version_string[0 : version_string.find("-")] mod = -1 bump_patch_level = True version = [int(part) for part in version_string.split(".")] version = cls._padded(version, 3) # Make from_string and from_version_array agree. For example: # MongoDB Enterprise > db.runCommand('buildInfo').versionArray # [ 3, 2, 1, -100 ] # MongoDB Enterprise > db.runCommand('buildInfo').version # 3.2.0-97-g1ef94fe if bump_patch_level: version[-1] += 1 version.append(mod) return Version(*version) @classmethod def from_version_array(cls, version_array): version = list(version_array) if version[-1] < 0: version[-1] = -1 version = cls._padded(version, 3) return Version(*version) @classmethod def from_client(cls, client): info = client.server_info() if "versionArray" in info: return cls.from_version_array(info["versionArray"]) return cls.from_string(info["version"]) def at_least(self, *other_version): return self >= Version(*other_version) def __str__(self): return ".".join(map(str, self)) motor-3.4.0/tox.ini000066400000000000000000000100031460060525600141670ustar00rootroot00000000000000# Tox (https://tox.readthedocs.io) is a tool for running tests in multiple # virtualenvs. "pip install tox>3.18" and run "tox" from this directory. # Adapted from Tornado's tox.ini. [tox] requires = tox>=4 envlist = # Run the unit test suite test # Ensure the sphinx build has no errors or warnings. docs, # Run the doctests, include examples and tutorial, via Sphinx. doctest, # Check links of sphinx docs linkcheck, # Test with the latest PyMongo. test-pymongo-latest, # Apply PyMongo's test suite to Motor via Synchro. synchro # Run synchro tests with enterprise auth enterprise-synchro # Run pre-commit on all files. lint # Run pre-commit on all files with manual checks. lint-manual # Check the sdist integrity. manifest # Typecheck with mypy typecheck-mypy labels = # Use labels and -m instead of -e so that tox -m