pax_global_header00006660000000000000000000000064135743036140014520gustar00rootroot0000000000000052 comment=0690c83326231123d2405ec982fd2978c04a8c0c motor-2.1.0/000077500000000000000000000000001357430361400126605ustar00rootroot00000000000000motor-2.1.0/.evergreen/000077500000000000000000000000001357430361400147205ustar00rootroot00000000000000motor-2.1.0/.evergreen/config.yml000066400000000000000000000720321357430361400167140ustar00rootroot00000000000000######################################## # 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: | # 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 set -o xtrace 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 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 git://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} sh ${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} sh ${DRIVERS_TOOLS}/.evergreen/stop-orchestration.sh "run tox": - command: shell.exec type: test params: working_dir: "src" script: | ${PREPARE_SHELL} PYTHON_BINARY="${PYTHON_BINARY}" \ TOX_BINARY="${TOX_BINARY}" \ TOX_ENV="${TOX_ENV}" \ INSTALL_TOX="${INSTALL_TOX}" \ VIRTUALENV="${VIRTUALENV}" \ AUTH="${AUTH}" \ SSL="${SSL}" \ CERT_DIR="${DRIVERS_TOOLS}/.evergreen/x509gen" \ sh ${PROJECT_DIRECTORY}/.evergreen/run-tox.sh "run enterprise auth tests": - command: shell.exec type: test params: silent: true working_dir: "src" script: | # DO NOT ECHO WITH XTRACE (which PREPARE_SHELL does) CLIENT_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem \ CA_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem \ PYTHON_BINARY=${PYTHON_BINARY} \ TOX_ENV=${TOX_ENV} \ SASL_HOST=${sasl_host} \ SASL_PORT=${sasl_port} \ SASL_USER=${sasl_user} \ SASL_PASS=${sasl_pass} \ SASL_DB=${sasl_db} \ PRINCIPAL=${principal} \ GSSAPI_DB=${gssapi_db} \ KEYTAB_BASE64=${keytab_base64} \ PROJECT_DIRECTORY=${PROJECT_DIRECTORY} \ sh ${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.0-standalone" tags: ["3.0", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.0" TOPOLOGY: "server" - func: "run tox" - name: "test-3.0-replica_set" tags: ["3.0", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.0" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-3.0-sharded_cluster" tags: ["3.0", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.0" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-3.2-standalone" tags: ["3.2", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.2" TOPOLOGY: "server" - func: "run tox" - name: "test-3.2-replica_set" tags: ["3.2", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.2" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-3.2-sharded_cluster" tags: ["3.2", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.2" TOPOLOGY: "sharded_cluster" - func: "run tox" - name: "test-3.4-standalone" tags: ["3.4", "standalone"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.4" TOPOLOGY: "server" - func: "run tox" - name: "test-3.4-replica_set" tags: ["3.4", "replica_set"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.4" TOPOLOGY: "replica_set" - func: "run tox" - name: "test-3.4-sharded_cluster" tags: ["3.4", "sharded_cluster"] commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "3.4" TOPOLOGY: "sharded_cluster" - func: "run tox" - 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-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-enterprise-auth" commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "latest" TOPOLOGY: "server" - func: "run enterprise auth tests" - name: "docs" commands: - func: "run tox" vars: TOX_ENV: py3-sphinx-docs - name: "doctest" commands: - func: "bootstrap mongo-orchestration" vars: VERSION: "4.0" TOPOLOGY: "server" - func: "run tox" vars: TOX_ENV: py3-sphinx-doctest # }}} 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" values: - id: "tornado4-py27" variables: TOX_ENV: "tornado4-py27" PYTHON_BINARY: "/opt/python/2.7/bin/python" - id: "tornado4-py37" variables: TOX_ENV: "tornado4-py37" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "tornado5-py27" variables: TOX_ENV: "tornado5-py27" PYTHON_BINARY: "/opt/python/2.7/bin/python" - id: "tornado5-pypy" variables: TOX_ENV: "tornado5-pypy" PYTHON_BINARY: "/opt/python/pypy/bin/pypy" - id: "tornado5-pypy3" variables: TOX_ENV: "tornado5-pypy3" PYTHON_BINARY: "/opt/python/pypy3.5/bin/pypy3" - id: "tornado5-py34" variables: TOX_ENV: "tornado5-py34" PYTHON_BINARY: "/opt/python/3.4/bin/python3" - id: "tornado5-py35" variables: TOX_ENV: "tornado5-py35" PYTHON_BINARY: "/opt/python/3.5/bin/python3" - id: "tornado5-py36" variables: TOX_ENV: "tornado5-py36" PYTHON_BINARY: "/opt/python/3.6/bin/python3" - id: "tornado5-py37" variables: TOX_ENV: "tornado5-py37" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "tornado6-py35" variables: TOX_ENV: "tornado6-py35" PYTHON_BINARY: "/opt/python/3.5/bin/python3" - id: "tornado6-py36" variables: TOX_ENV: "tornado6-py36" PYTHON_BINARY: "/opt/python/3.6/bin/python3" - id: "tornado6-py37" variables: TOX_ENV: "tornado6-py37" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "tornado6-py38" variables: TOX_ENV: "tornado6-py38" PYTHON_BINARY: "/opt/python/3.8/bin/python3" - id: "tornado_git-py35" variables: TOX_ENV: "tornado_git-py35" PYTHON_BINARY: "/opt/python/3.5/bin/python3" - id: "tornado_git-py36" variables: TOX_ENV: "tornado_git-py36" PYTHON_BINARY: "/opt/python/3.6/bin/python3" - id: "tornado_git-py37" variables: TOX_ENV: "tornado_git-py37" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "asyncio-py37" variables: TOX_ENV: "asyncio-py37" PYTHON_BINARY: "/opt/python/3.7/bin/python3" - id: "asyncio-py38" variables: TOX_ENV: "asyncio-py38" PYTHON_BINARY: "/opt/python/3.8/bin/python3" - id: "py3-pymongo-master" variables: TOX_ENV: "py3-pymongo-master" # Use 3.6 for now until 3.7 is on all Evergreen distros. PYTHON_BINARY: "/opt/python/3.6/bin/python3" - id: "synchro27" variables: TOX_ENV: "synchro27" PYTHON_BINARY: "/opt/python/2.7/bin/python" - id: "synchro37" variables: TOX_ENV: "synchro37" PYTHON_BINARY: "/opt/python/3.7/bin/python3" # Test Python only up to 3.6 on Mac, until our toolchain is updated. - id: tox-env-osx display_name: "Tox Env OSX" values: - id: "tornado5-py36" variables: TOX_ENV: "tornado5-py36" PYTHON_BINARY: "/opt/mongodbtoolchain/v2/bin/python3.6" - id: "tornado_git-py36" variables: TOX_ENV: "tornado_git-py36" PYTHON_BINARY: "/opt/mongodbtoolchain/v2/bin/python3.6" - id: tox-env-win display_name: "Tox Env Windows" values: - id: "tornado5-py36" variables: TOX_ENV: "tornado5-py36" PYTHON_BINARY: "c:\\python\\Python36\\python.exe" - id: "tornado_git-py36" variables: TOX_ENV: "tornado_git-py36" PYTHON_BINARY: "c:\\python\\Python36\\python.exe" - id: os display_name: "Operating System" values: - id: "rhel" display_name: "RHEL 6.2" run_on: "rhel62-small" variables: TOX_BINARY: "/opt/python/3.6/bin/tox" - id: "ubuntu" display_name: "Ubuntu 16.04" run_on: "ubuntu1604-test" variables: TOX_BINARY: "/opt/python/3.6/bin/tox" - id: "win" display_name: "Windows" run_on: "windows-64-vs2015-test" variables: INSTALL_TOX: true VIRTUALENV: "/cygdrive/c/python/Python36/python.exe -m virtualenv" - id: "macos-1012" display_name: "macOS 10.12" run_on: "macos-1012" variables: INSTALL_TOX: true VIRTUALENV: "/opt/mongodbtoolchain/v2/bin/python3.6 -m virtualenv" buildvariants: # Test MongoDB 3.0 and Python only up to 3.6 on RHEL. - matrix_name: "test-mongodb-3.0-rhel" display_name: "${os}-${tox-env}-${ssl}" matrix_spec: os: "rhel" tox-env: ["tornado4-py27", "tornado5-py36", "py3-pymongo-master"] ssl: "*" tasks: - ".3.0" # Main test matrix. - matrix_name: "main" display_name: "${os}-${tox-env}-${ssl}" matrix_spec: os: "ubuntu" tox-env: "*" ssl: "*" exclude_spec: # TODO: synchro needs PyMongo master's updated SSL test certs, # which may require Motor test suite changes. - os: "*" tox-env: ["synchro27", "synchro37"] ssl: "ssl" tasks: - ".latest" - ".4.2" - ".4.0" - ".3.6" - ".3.4" - ".3.2" - matrix_name: "test-win" display_name: "${os}-${tox-env-win}-${ssl}" matrix_spec: os: "win" tox-env-win: "*" ssl: "*" tasks: - ".latest" - matrix_name: "test-macos" display_name: "${os}-${tox-env-osx}-${ssl}" matrix_spec: os: "macos-1012" tox-env-osx: "*" ssl: "*" tasks: - ".latest" - matrix_name: "enterprise-auth" display_name: "Enterprise Auth-${tox-env}" matrix_spec: {"tox-env": ["synchro27", "synchro37"], ssl: "ssl"} run_on: - ubuntu1604-test tasks: - name: "test-enterprise-auth" - name: "docs" display_name: "Docs - Build" run_on: - ubuntu1604-test expansions: TOX_ENV: "py3-sphinx-docs" TOX_BINARY: "/opt/python/3.6/bin/tox" PYTHON_BINARY: "/opt/python/3.6/bin/python3" tasks: - name: "docs" - name: "doctests" display_name: "Docs - Test" run_on: - ubuntu1604-test expansions: TOX_ENV: "py3-sphinx-doctest" TOX_BINARY: "/opt/python/3.6/bin/tox" PYTHON_BINARY: "/opt/python/3.6/bin/python3" tasks: - name: "doctest" motor-2.1.0/.evergreen/install-dependencies.sh000066400000000000000000000005601357430361400213470ustar00rootroot00000000000000#!/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-2.1.0/.evergreen/run-enterprise-auth-tests.sh000066400000000000000000000015621357430361400223410ustar00rootroot00000000000000#!/bin/bash # Don't trace to avoid secrets showing up in the logs set -o errexit echo "Running enterprise authentication tests" 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_TESTENV_PASSENV=* # --sitepackages allows use of pykerberos without a test dep. /opt/python/3.6/bin/python3 -m tox -e "$TOX_ENV" --sitepackages -- -x test.test_auth motor-2.1.0/.evergreen/run-tox.sh000077500000000000000000000027251357430361400167010ustar00rootroot00000000000000#!/bin/sh set -o xtrace # Write all commands first to stderr set -o errexit # Exit the script with error if any of the commands fail # Supported/used environment variables: # AUTH Set to enable authentication. Defaults to "noauth" # SSL Set to enable SSL. Defaults to "nossl" # TOX_ENV Tox environment name, e.g. "tornado4-py36" # TOX_BINARY Path to tox executable # INSTALL_TOX Whether to install tox in a virtualenv # PYTHON_BINARY Path to python # VIRTUALENV Path to virtualenv script AUTH=${AUTH:-noauth} SSL=${SSL:-nossl} if [ "$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 [ "$TOX_ENV" = "synchro27" -o "$TOX_ENV" = "synchro37" ]; then SETUP_ARGS="-- --check-exclude-patterns" fi if [ "${INSTALL_TOX}" = "true" ]; then $VIRTUALENV motorenv set +o xtrace if [ -f motorenv/bin/activate ]; then source motorenv/bin/activate else # Windows. ls -l motorenv source motorenv/Scripts/activate fi set -o xtrace pip install tox TOX_BINARY=tox fi # Run the tests, and store the results in Evergreen compatible XUnit XML ${TOX_BINARY} -e ${TOX_ENV} ${SETUP_ARGS} "$@" motor-2.1.0/.gitignore000066400000000000000000000001531357430361400146470ustar00rootroot00000000000000*~ *#* .DS* *.pyc *.pyd build/ dist/ motor.egg-info/ setup.cfg *.egg .tox doc/_build/ .idea/ xunit-results motor-2.1.0/.travis.yml000066400000000000000000000003131357430361400147660ustar00rootroot00000000000000language: python python: - "2.7" - "3.4" - "3.5" - "3.6" - "3.7" - "3.8" services: mongodb install: - pip install tornado script: "python setup.py test" branches: only: - master motor-2.1.0/CONTRIBUTING.rst000066400000000000000000000031421357430361400153210ustar00rootroot00000000000000Contributing 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 `_ for your specific issue or feature request. Running Tests ------------- Install a recent version of MongoDB and run it on the default port from a clean data directory. Pass "--setParameter enableTestCommands=1" to mongod to enable testing MotorCursor's ``max_time_ms`` method. Control how the tests connect to MongoDB with these environment variables: - ``DB_IP``: Defaults to "localhost", can be a domain name or IP - ``DB_PORT``: Defaults to 27017 - ``DB_USER``, ``DB_PASSWORD``: To test with authentication, create an admin user and set these environment variables to the username and password - ``CERT_DIR``: Path with alternate client.pem and ca.pem for testing. Otherwise the suite uses those in test/certificates/. Install `tox`_ and run it from the command line in the repository directory. You will need a variety of Python interpreters installed. For a minimal test, ensure you have Python 2.7 and 3.5, and run:: > tox -e tornado4-py27,tornado4-py36 The doctests pass with Python 3.6 and a MongoDB 3.6 instance running on port 27017: > tox -e py3-sphinx-doctest .. _tox: https://testrun.org/tox/ General Guidelines ------------------ - Avoid backward breaking changes if at all possible. - Write inline documentation for new classes and methods. - Add yourself to doc/contributors.rst :) motor-2.1.0/LICENSE000066400000000000000000000261361357430361400136750ustar00rootroot00000000000000 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-2.1.0/MANIFEST.in000066400000000000000000000003121357430361400144120ustar00rootroot00000000000000include README.rst include LICENSE recursive-include doc *.rst recursive-include doc *.py recursive-include test *.py recursive-include doc *.conf recursive-include doc *.css recursive-include doc *.js motor-2.1.0/README.rst000066400000000000000000000067201357430361400143540ustar00rootroot00000000000000===== Motor ===== .. image:: https://raw.github.com/mongodb/motor/master/doc/_static/motor.png :Info: Motor is a full-featured, non-blocking MongoDB_ driver for Python Tornado_ and asyncio_ applications. :Author: A\. Jesse Jiryu Davis About ===== Motor presents a coroutine-based API for non-blocking access to MongoDB. 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* Installation ============ $ pip install motor Dependencies ============ Motor works in all the environments officially supported by Tornado or by asyncio. It requires: * Unix, including Mac OS X. Windows is not supported. * PyMongo_ 3.10 or later. * Python 2.7 or later. * `futures`_ on Python 2.7. * `backports.pbkdf2`_ for faster authentication with MongoDB 3.0+, especially on Python older than 2.7.8, or on Python 3 before Python 3.4. See `requirements `_ for details about compatibility. How To Ask For Help =================== Issues with, questions about, or feedback for Motor should be sent to the `mongodb-user list on Google Groups`_. For confirmed issues or feature requests, open a case in `Jira `_ in the "MOTOR" project. Please include all of the following information: - Detailed steps to reproduce the problem, including your code and a full traceback, if possible. - What you expected to happen, and what actually happened. - The exact python version used, with patch level:: $ python -c "import sys; print(sys.version)" - The exact version of PyMongo used:: $ python -c "import pymongo; print(pymongo.version); print(pymongo.has_c())" - The exact Tornado version, if you are using Tornado:: $ python -c "import tornado; print(tornado.version)" - The operating system and version (e.g. RedHat Enterprise Linux 6.4, OSX 10.9.5, ...) Documentation ============= Motor's documentation is on ReadTheDocs_. Build the documentation with Python 3.5. Install sphinx, Tornado, and aiohttp, and do ``cd doc; make html``. Examples ======== See the `examples on ReadTheDocs `_. Testing ======= Run ``python setup.py test``. Tests are located in the ``test/`` directory. .. _PyMongo: http://pypi.python.org/pypi/pymongo/ .. _MongoDB: http://mongodb.org/ .. _Tornado: http://tornadoweb.org/ .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _futures: https://pypi.python.org/pypi/futures .. _backports.pbkdf2: https://pypi.python.org/pypi/backports.pbkdf2/ .. _ReadTheDocs: https://motor.readthedocs.io/ .. _mongodb-user list on Google Groups: https://groups.google.com/forum/?fromgroups#!forum/mongodb-user .. _sphinx: http://sphinx.pocoo.org/ motor-2.1.0/doc/000077500000000000000000000000001357430361400134255ustar00rootroot00000000000000motor-2.1.0/doc/Makefile000066400000000000000000000060641357430361400150730ustar00rootroot00000000000000# 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-2.1.0/doc/__init__.py000066400000000000000000000000011357430361400155250ustar00rootroot00000000000000 motor-2.1.0/doc/_static/000077500000000000000000000000001357430361400150535ustar00rootroot00000000000000motor-2.1.0/doc/_static/motor.png000066400000000000000000001576221357430361400167360ustar00rootroot00000000000000‰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-2.1.0/doc/api-asyncio/000077500000000000000000000000001357430361400156415ustar00rootroot00000000000000motor-2.1.0/doc/api-asyncio/aiohttp.rst000066400000000000000000000003641357430361400200460ustar00rootroot00000000000000:mod:`motor.aiohttp` - Integrate Motor with the aiohttp web framework ===================================================================== .. currentmodule:: motor.aiohttp .. automodule:: motor.aiohttp :members: :no-inherited-members: motor-2.1.0/doc/api-asyncio/asyncio_gridfs.rst000066400000000000000000000352221357430361400214020ustar00rootroot00000000000000asyncio GridFS Classes ====================== .. currentmodule:: motor.motor_asyncio Store blobs of data in `GridFS `_. .. seealso:: :ref:`Differences between PyMongo's and Motor's GridFS APIs `. .. class:: AsyncIOMotorGridFSBucket Create a new instance of :class:`AsyncIOMotorGridFSBucket`. Raises :exc:`TypeError` if `database` is not an instance of :class:`AsyncIOMotorDatabase`. Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern` is not acknowledged. :Parameters: - `database`: database to use. - `bucket_name` (optional): The name of the bucket. Defaults to 'fs'. - `chunk_size_bytes` (optional): The chunk size in bytes. Defaults to 255KB. - `write_concern` (optional): The :class:`~pymongo.write_concern.WriteConcern` to use. If ``None`` (the default) db.write_concern is used. - `read_preference` (optional): The read preference to use. If ``None`` (the default) db.read_preference is used. .. mongodoc:: gridfs .. coroutinemethod:: delete(self, file_id) Delete a file's metadata and data chunks from a GridFS bucket:: async def delete(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) # Get _id of file to delete file_id = await fs.upload_from_stream("test_file", b"data I want to store!") await fs.delete(file_id) Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be deleted. .. coroutinemethod:: download_to_stream(self, file_id, destination) Downloads the contents of the stored file specified by file_id and writes the contents to `destination`:: async def download(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) # Get _id of file to read file_id = await fs.upload_from_stream("test_file", b"data I want to store!") # Get file to write to file = open('myfile','wb+') await fs.download_to_stream(file_id, file) file.seek(0) contents = file.read() Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be downloaded. - `destination`: a file-like object implementing :meth:`write`. .. coroutinemethod:: download_to_stream_by_name(self, filename, destination, revision=-1) Write the contents of `filename` (with optional `revision`) to `destination`. For example:: async def download_by_name(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) # Get file to write to file = open('myfile','wb') await fs.download_to_stream_by_name("test_file", file) Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `filename`: The name of the file to read from. - `destination`: A file-like object that implements :meth:`write`. - `revision` (optional): Which revision (documents with the same filename and different uploadDate) of the file to retrieve. Defaults to -1 (the most recent revision). :Note: Revision numbers are defined as follows: - 0 = the original stored file - 1 = the first revision - 2 = the second revision - etc... - -2 = the second most recent revision - -1 = the most recent revision .. method:: find(self, *args, **kwargs) Find and return the files collection documents that match ``filter``. Returns a cursor that iterates across files matching arbitrary queries on the files collection. Can be combined with other modifiers for additional control. For example:: async def find(): cursor = fs.find({"filename": "lisa.txt"}, no_cursor_timeout=True) async for grid_data in cursor: data = grid_data.read() iterates through all versions of "lisa.txt" stored in GridFS. Setting no_cursor_timeout may be important to prevent the cursor from timing out during long multi-file processing work. As another example, the call:: most_recent_three = fs.find().sort("uploadDate", -1).limit(3) returns a cursor to the three most recently uploaded files in GridFS. Follows a similar interface to :meth:`~AsyncIOMotorCollection.find` in :class:`AsyncIOMotorCollection`. :Parameters: - `filter`: Search query. - `batch_size` (optional): The number of documents to return per batch. - `limit` (optional): The maximum number of documents to return. - `no_cursor_timeout` (optional): The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use. Set this option to True prevent that. - `skip` (optional): The number of documents to skip before returning. - `sort` (optional): The order by which to sort results. Defaults to None. .. coroutinemethod:: open_download_stream(self, file_id) Opens a stream to read the contents of the stored file specified by file_id:: async def download_stream(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) # get _id of file to read. file_id = await fs.upload_from_stream("test_file", b"data I want to store!") grid_out = await fs.open_download_stream(file_id) contents = await grid_out.read() Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be downloaded. Returns a :class:`AsyncIOMotorGridOut`. .. coroutinemethod:: open_download_stream_by_name(self, filename, revision=-1) Opens a stream to read the contents of `filename` and optional `revision`:: async def download_by_name(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) # get _id of file to read. file_id = await fs.upload_from_stream("test_file", b"data I want to store!") grid_out = await fs.open_download_stream_by_name(file_id) contents = await grid_out.read() Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` filename is not a string. :Parameters: - `filename`: The name of the file to read from. - `revision` (optional): Which revision (documents with the same filename and different uploadDate) of the file to retrieve. Defaults to -1 (the most recent revision). Returns a :class:`AsyncIOMotorGridOut`. :Note: Revision numbers are defined as follows: - 0 = the original stored file - 1 = the first revision - 2 = the second revision - etc... - -2 = the second most recent revision - -1 = the most recent revision .. method:: open_upload_stream(self, filename, chunk_size_bytes=None, metadata=None) Opens a stream for writing. Specify the filename, and add any additional information in the metadata field of the file document or modify the chunk size:: async def upload(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) grid_in, file_id = fs.open_upload_stream( "test_file", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) await grid_in.write(b"data I want to store!") await grid_in.close() # uploaded on close Returns an instance of :class:`AsyncIOMotorGridIn`. Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. In a Python 3.5 native coroutine, the "async with" statement calls :meth:`~AsyncIOMotorGridIn.close` automatically:: async def upload(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) async with await fs.new_file() as gridin: await gridin.write(b'First part\n') await gridin.write(b'Second part') # gridin is now closed automatically. :Parameters: - `filename`: The name of the file to upload. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes in :class:`AsyncIOMotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. .. method:: open_upload_stream_with_id(self, file_id, filename, chunk_size_bytes=None, metadata=None) Opens a stream for writing. Specify the filed_id and filename, and add any additional information in the metadata field of the file document, or modify the chunk size:: async def upload(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) grid_in, file_id = fs.open_upload_stream_with_id( ObjectId(), "test_file", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) await grid_in.write(b"data I want to store!") await grid_in.close() # uploaded on close Returns an instance of :class:`AsyncIOMotorGridIn`. Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `file_id`: The id to use for this file. The id must not have already been used for another file. - `filename`: The name of the file to upload. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes in :class:`AsyncIOMotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. .. coroutinemethod:: rename(self, file_id, new_filename) Renames the stored file with the specified file_id. For example:: async def rename(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) # get _id of file to read. file_id = await fs.upload_from_stream("test_file", b"data I want to store!") await fs.rename(file_id, "new_test_name") Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be renamed. - `new_filename`: The new name of the file. .. coroutinemethod:: upload_from_stream(self, filename, source, chunk_size_bytes=None, metadata=None) Uploads a user file to a GridFS bucket. Reads the contents of the user file from `source` and uploads it to the file `filename`. Source can be a string or file-like object. For example:: async def upload_from_stream(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) file_id = await fs.upload_from_stream( "test_file", b"data I want to store!", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `filename`: The name of the file to upload. - `source`: The source stream of the content to be uploaded. Must be a file-like object that implements :meth:`read` or a string. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes of :class:`AsyncIOMotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. Returns the _id of the uploaded file. .. coroutinemethod:: upload_from_stream_with_id(self, file_id, filename, source, chunk_size_bytes=None, metadata=None) Uploads a user file to a GridFS bucket with a custom file id. Reads the contents of the user file from `source` and uploads it to the file `filename`. Source can be a string or file-like object. For example:: async def upload_from_stream_with_id(): my_db = AsyncIOMotorClient().test fs = AsyncIOMotorGridFSBucket(my_db) file_id = await fs.upload_from_stream_with_id( ObjectId(), "test_file", b"data I want to store!", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `file_id`: The id to use for this file. The id must not have already been used for another file. - `filename`: The name of the file to upload. - `source`: The source stream of the content to be uploaded. Must be a file-like object that implements :meth:`read` or a string. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes of :class:`AsyncIOMotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. .. autoclass:: AsyncIOMotorGridIn :members: .. autoclass:: AsyncIOMotorGridOut :members: .. autoclass:: AsyncIOMotorGridOutCursor :members: motor-2.1.0/doc/api-asyncio/asyncio_motor_change_stream.rst000066400000000000000000000003131357430361400241350ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorChangeStream` ====================================================== .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorChangeStream :members: motor-2.1.0/doc/api-asyncio/asyncio_motor_client.rst000066400000000000000000000007001357430361400226130ustar00rootroot00000000000000: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-2.1.0/doc/api-asyncio/asyncio_motor_client_session.rst000066400000000000000000000003561357430361400243650ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorClientSession` -- Sequence of operations ================================================================================= .. autoclass:: motor.motor_asyncio.AsyncIOMotorClientSession :members: motor-2.1.0/doc/api-asyncio/asyncio_motor_collection.rst000066400000000000000000000121531357430361400234750ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorCollection` ==================================================== .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorCollection :members: :exclude-members: create_index, inline_map_reduce .. 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. .. coroutinemethod:: create_index(self, keys, **kwargs) Creates an index on this collection. Takes either a single key or a list of (key, direction) pairs. The key(s) must be an instance of :class:`basestring` (:class:`str` in python 3), and the direction(s) must be one of (:data:`~pymongo.ASCENDING`, :data:`~pymongo.DESCENDING`, :data:`~pymongo.GEO2D`, :data:`~pymongo.GEOHAYSTACK`, :data:`~pymongo.GEOSPHERE`, :data:`~pymongo.HASHED`, :data:`~pymongo.TEXT`). To create a single key ascending index on the key ``'mike'`` we just use a string argument:: await my_collection.create_index("mike") For a compound index on ``'mike'`` descending and ``'eliot'`` ascending we need to use a list of tuples:: await my_collection.create_index([("mike", pymongo.DESCENDING), ("eliot", pymongo.ASCENDING)]) All optional index creation parameters should be passed as keyword arguments to this method. For example:: await my_collection.create_index([("mike", pymongo.DESCENDING)], background=True) Valid options include, but are not limited to: - `name`: custom name to use for this index - if none is given, a name will be generated. - `unique`: if ``True`` creates a uniqueness constraint on the index. - `background`: if ``True`` this index should be created in the background. - `sparse`: if ``True``, omit from the index any documents that lack the indexed field. - `bucketSize`: for use with geoHaystack indexes. Number of documents to group together within a certain proximity to a given longitude and latitude. - `min`: minimum value for keys in a :data:`~pymongo.GEO2D` index. - `max`: maximum value for keys in a :data:`~pymongo.GEO2D` index. - `expireAfterSeconds`: 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`. This option is only supported on MongoDB 3.4 and above. See the MongoDB documentation for a full list of supported options by server version. .. warning:: `dropDups` is not supported by MongoDB 3.0 or newer. The option is silently ignored by the server and unique index builds using the option will fail if a duplicate value is detected. .. note:: `partialFilterExpression` requires server version **>= 3.2** .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of this collection is automatically applied to this operation when using MongoDB >= 3.4. :Parameters: - `keys`: a single key or a list of (key, direction) pairs specifying the index to create - `**kwargs` (optional): any additional index creation options (see the above list) should be passed as keyword arguments .. mongodoc:: indexes .. coroutinemethod:: inline_map_reduce(self, map, reduce, full_response=False, **kwargs) Perform an inline map/reduce operation on this collection. Perform the map/reduce operation on the server in RAM. A result collection is not created. The result set is returned as a list of documents. If `full_response` is ``False`` (default) returns the result documents in a list. Otherwise, returns the full response from the server to the `map reduce command`_. The :meth:`inline_map_reduce` method obeys the :attr:`read_preference` of this :class:`Collection`. :Parameters: - `map`: map function (as a JavaScript string) - `reduce`: reduce function (as a JavaScript string) - `full_response` (optional): if ``True``, return full response to this command - otherwise just return the result collection - `**kwargs` (optional): additional arguments to the `map reduce command`_ may be passed as keyword arguments to this helper method, e.g.:: await db.test.inline_map_reduce(map, reduce, limit=2) .. _map reduce command: http://docs.mongodb.org/manual/reference/command/mapReduce/ .. mongodoc:: mapreduce motor-2.1.0/doc/api-asyncio/asyncio_motor_database.rst000066400000000000000000000006771357430361400231160ustar00rootroot00000000000000: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-2.1.0/doc/api-asyncio/cursors.rst000066400000000000000000000006111357430361400200710ustar00rootroot00000000000000:class:`~motor.motor_asyncio.AsyncIOMotorCursor` ================================================ .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorCursor :members: :class:`~motor.motor_asyncio.AsyncIOMotorCommandCursor` ======================================================= .. currentmodule:: motor.motor_asyncio .. autoclass:: AsyncIOMotorCommandCursor :members: motor-2.1.0/doc/api-asyncio/index.rst000066400000000000000000000006031357430361400175010ustar00rootroot00000000000000Motor asyncio API ================= .. toctree:: asyncio_motor_client asyncio_motor_client_session asyncio_motor_database asyncio_motor_collection asyncio_motor_change_stream cursors asyncio_gridfs aiohttp .. seealso:: :doc:`../tutorial-asyncio` This page describes using Motor with asyncio. For Tornado integration, see :doc:`../api-tornado/index`. motor-2.1.0/doc/api-tornado/000077500000000000000000000000001357430361400156425ustar00rootroot00000000000000motor-2.1.0/doc/api-tornado/cursors.rst000066400000000000000000000005371357430361400201010ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorCursor` ========================================= .. currentmodule:: motor.motor_tornado .. autoclass:: MotorCursor :members: :class:`~motor.motor_tornado.MotorCommandCursor` ================================================ .. currentmodule:: motor.motor_tornado .. autoclass:: MotorCommandCursor :members: motor-2.1.0/doc/api-tornado/gridfs.rst000066400000000000000000000355531357430361400176650ustar00rootroot00000000000000Motor 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` .. class:: MotorGridFSBucket Create a new instance of :class:`MotorGridFSBucket`. Raises :exc:`TypeError` if `database` is not an instance of :class:`MotorDatabase`. Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern` is not acknowledged. :Parameters: - `database`: database to use. - `bucket_name` (optional): The name of the bucket. Defaults to 'fs'. - `chunk_size_bytes` (optional): The chunk size in bytes. Defaults to 255KB. - `write_concern` (optional): The :class:`~pymongo.write_concern.WriteConcern` to use. If ``None`` (the default) db.write_concern is used. - `read_preference` (optional): The read preference to use. If ``None`` (the default) db.read_preference is used. .. mongodoc:: gridfs .. coroutinemethod:: delete(self, file_id)) Delete a file's metadata and data chunks from a GridFS bucket:: @gen.coroutine def delete(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) # Get _id of file to delete file_id = yield fs.upload_from_stream("test_file", b"data I want to store!") yield fs.delete(file_id) Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be deleted. Returns a Future. .. coroutinemethod:: download_to_stream(self, file_id, destination)) Downloads the contents of the stored file specified by file_id and writes the contents to `destination`:: @gen.coroutine def download(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) # Get _id of file to read file_id = yield fs.upload_from_stream("test_file", b"data I want to store!") # Get file to write to file = open('myfile','wb+') yield fs.download_to_stream(file_id, file) file.seek(0) contents = file.read() Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be downloaded. - `destination`: a file-like object implementing :meth:`write`. Returns a Future. .. coroutinemethod:: download_to_stream_by_name(self, filename, destination, revision=-1) Write the contents of `filename` (with optional `revision`) to `destination`. For example:: @gen.coroutine def download_by_name(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) # Get file to write to file = open('myfile','wb') yield fs.download_to_stream_by_name("test_file", file) Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `filename`: The name of the file to read from. - `destination`: A file-like object that implements :meth:`write`. - `revision` (optional): Which revision (documents with the same filename and different uploadDate) of the file to retrieve. Defaults to -1 (the most recent revision). :Note: Revision numbers are defined as follows: - 0 = the original stored file - 1 = the first revision - 2 = the second revision - etc... - -2 = the second most recent revision - -1 = the most recent revision .. method:: find(self, *args, **kwargs) Find and return the files collection documents that match ``filter``. Returns a cursor that iterates across files matching arbitrary queries on the files collection. Can be combined with other modifiers for additional control. For example:: @gen.coroutine def find(): cursor = fs.find({"filename": "lisa.txt"}, no_cursor_timeout=True) while (yield cursor.fetch_next): grid_data = cursor.next_object() data = grid_data.read() iterates through all versions of "lisa.txt" stored in GridFS. Setting no_cursor_timeout may be important to prevent the cursor from timing out during long multi-file processing work. As another example, the call:: most_recent_three = fs.find().sort("uploadDate", -1).limit(3) returns a cursor to the three most recently uploaded files in GridFS. Follows a similar interface to :meth:`~MotorCollection.find` in :class:`MotorCollection`. :Parameters: - `filter`: Search query. - `batch_size` (optional): The number of documents to return per batch. - `limit` (optional): The maximum number of documents to return. - `no_cursor_timeout` (optional): The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use. Set this option to True prevent that. - `skip` (optional): The number of documents to skip before returning. - `sort` (optional): The order by which to sort results. Defaults to None. Returns a Future. .. coroutinemethod:: open_download_stream(self, file_id) Opens a stream to read the contents of the stored file specified by file_id:: @gen.coroutine def download_stream(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) # get _id of file to read. file_id = yield fs.upload_from_stream("test_file", b"data I want to store!") grid_out = yield fs.open_download_stream(file_id) contents = yield grid_out.read() Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be downloaded. Returns a Future that resolves to a :class:`MotorGridOut`. .. coroutinemethod:: open_download_stream_by_name(self, filename, revision=-1) Opens a stream to read the contents of `filename` and optional `revision`:: @gen.coroutine def download_by_name(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) # get _id of file to read. file_id = yield fs.upload_from_stream("test_file", b"data I want to store!") grid_out = yield fs.open_download_stream_by_name(file_id) contents = yield grid_out.read() Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` filename is not a string. :Parameters: - `filename`: The name of the file to read from. - `revision` (optional): Which revision (documents with the same filename and different uploadDate) of the file to retrieve. Defaults to -1 (the most recent revision). Returns a Future that resolves to a :class:`MotorGridOut`. :Note: Revision numbers are defined as follows: - 0 = the original stored file - 1 = the first revision - 2 = the second revision - etc... - -2 = the second most recent revision - -1 = the most recent revision .. method:: open_upload_stream(self, filename, chunk_size_bytes=None, metadata=None) Opens a stream for writing. Specify the filename, and add any additional information in the metadata field of the file document or modify the chunk size:: @gen.coroutine def upload(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) grid_in, file_id = fs.open_upload_stream( "test_file", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) yield grid_in.write(b"data I want to store!") yield grid_in.close() # uploaded on close Returns an instance of :class:`MotorGridIn`. Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. In a Python 3.5 native coroutine, the "async with" statement calls :meth:`~MotorGridIn.close` automatically:: async def upload(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) async with await fs.new_file() as gridin: await gridin.write(b'First part\n') await gridin.write(b'Second part') # gridin is now closed automatically. :Parameters: - `filename`: The name of the file to upload. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes in :class:`MotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. .. method:: open_upload_stream_with_id(self, file_id, filename, chunk_size_bytes=None, metadata=None) Opens a stream for writing. Specify the filed_id and filename, and add any additional information in the metadata field of the file document, or modify the chunk size:: @gen.coroutine def upload(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) grid_in, file_id = fs.open_upload_stream_with_id( ObjectId(), "test_file", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) yield grid_in.write(b"data I want to store!") yield grid_in.close() # uploaded on close Returns an instance of :class:`MotorGridIn`. Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `file_id`: The id to use for this file. The id must not have already been used for another file. - `filename`: The name of the file to upload. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes in :class:`MotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. .. coroutinemethod:: rename(self, file_id, new_filename)) Renames the stored file with the specified file_id. For example:: @gen.coroutine def rename(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) # get _id of file to read. file_id = yield fs.upload_from_stream("test_file", b"data I want to store!") yield fs.rename(file_id, "new_test_name") Raises :exc:`~gridfs.errors.NoFile` if no file with file_id exists. :Parameters: - `file_id`: The _id of the file to be renamed. - `new_filename`: The new name of the file. Returns a Future. .. coroutinemethod:: upload_from_stream(self, filename, source, chunk_size_bytes=None, metadata=None)) Uploads a user file to a GridFS bucket. Reads the contents of the user file from `source` and uploads it to the file `filename`. Source can be a string or file-like object. For example:: @gen.coroutine def upload_from_stream(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) file_id = yield fs.upload_from_stream( "test_file", b"data I want to store!", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `filename`: The name of the file to upload. - `source`: The source stream of the content to be uploaded. Must be a file-like object that implements :meth:`read` or a string. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes of :class:`MotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. Returns a Future that resolves to the _id of the uploaded file. .. coroutinemethod:: upload_from_stream_with_id(self, file_id, filename, source, chunk_size_bytes=None, metadata=None)) Uploads a user file to a GridFS bucket with a custom file id. Reads the contents of the user file from `source` and uploads it to the file `filename`. Source can be a string or file-like object. For example:: @gen.coroutine def upload_from_stream_with_id(): my_db = MotorClient().test fs = MotorGridFSBucket(my_db) file_id = yield fs.upload_from_stream_with_id( ObjectId(), "test_file", b"data I want to store!", chunk_size_bytes=4, metadata={"contentType": "text/plain"}) Raises :exc:`~gridfs.errors.NoFile` if no such version of that file exists. Raises :exc:`~ValueError` if `filename` is not a string. :Parameters: - `file_id`: The id to use for this file. The id must not have already been used for another file. - `filename`: The name of the file to upload. - `source`: The source stream of the content to be uploaded. Must be a file-like object that implements :meth:`read` or a string. - `chunk_size_bytes` (options): The number of bytes per chunk of this file. Defaults to the chunk_size_bytes of :class:`MotorGridFSBucket`. - `metadata` (optional): User data for the 'metadata' field of the files collection document. If not provided the metadata field will be omitted from the files collection document. Returns a Future. .. autoclass:: MotorGridIn :members: .. autoclass:: MotorGridOut :members: .. autoclass:: MotorGridOutCursor :members: motor-2.1.0/doc/api-tornado/index.rst000066400000000000000000000005161357430361400175050ustar00rootroot00000000000000Motor Tornado API ================= .. toctree:: motor_client motor_client_session motor_database motor_collection motor_change_stream cursors gridfs web .. seealso:: :doc:`../tutorial-tornado` This page describes using Motor with Tornado. For asyncio integration, see :doc:`../api-asyncio/index`. motor-2.1.0/doc/api-tornado/motor_change_stream.rst000066400000000000000000000002661357430361400224200ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorChangeStream` =============================================== .. currentmodule:: motor.motor_tornado .. autoclass:: MotorChangeStream :members: motor-2.1.0/doc/api-tornado/motor_client.rst000066400000000000000000000006611357430361400210750ustar00rootroot00000000000000: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-2.1.0/doc/api-tornado/motor_client_session.rst000066400000000000000000000004011357430361400226300ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorClientSession` -- Sequence of operations ========================================================================== .. currentmodule:: motor.motor_tornado .. autoclass:: motor.motor_tornado.MotorClientSession :members: motor-2.1.0/doc/api-tornado/motor_collection.rst000066400000000000000000000121631357430361400217520ustar00rootroot00000000000000:class:`~motor.motor_tornado.MotorCollection` ============================================= .. currentmodule:: motor.motor_tornado .. autoclass:: MotorCollection :members: :exclude-members: create_index, inline_map_reduce .. 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. .. coroutinemethod:: create_index(self, keys, **kwargs) Creates an index on this collection. Takes either a single key or a list of (key, direction) pairs. The key(s) must be an instance of :class:`basestring` (:class:`str` in python 3), and the direction(s) must be one of (:data:`~pymongo.ASCENDING`, :data:`~pymongo.DESCENDING`, :data:`~pymongo.GEO2D`, :data:`~pymongo.GEOHAYSTACK`, :data:`~pymongo.GEOSPHERE`, :data:`~pymongo.HASHED`, :data:`~pymongo.TEXT`). To create a single key ascending index on the key ``'mike'`` we just use a string argument:: yield my_collection.create_index("mike") For a compound index on ``'mike'`` descending and ``'eliot'`` ascending we need to use a list of tuples:: yield my_collection.create_index([("mike", pymongo.DESCENDING), ("eliot", pymongo.ASCENDING)]) All optional index creation parameters should be passed as keyword arguments to this method. For example:: yield my_collection.create_index([("mike", pymongo.DESCENDING)], background=True) Valid options include, but are not limited to: - `name`: custom name to use for this index - if none is given, a name will be generated. - `unique`: if ``True`` creates a uniqueness constraint on the index. - `background`: if ``True`` this index should be created in the background. - `sparse`: if ``True``, omit from the index any documents that lack the indexed field. - `bucketSize`: for use with geoHaystack indexes. Number of documents to group together within a certain proximity to a given longitude and latitude. - `min`: minimum value for keys in a :data:`~pymongo.GEO2D` index. - `max`: maximum value for keys in a :data:`~pymongo.GEO2D` index. - `expireAfterSeconds`: 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`. This option is only supported on MongoDB 3.4 and above. See the MongoDB documentation for a full list of supported options by server version. .. warning:: `dropDups` is not supported by MongoDB 3.0 or newer. The option is silently ignored by the server and unique index builds using the option will fail if a duplicate value is detected. .. note:: `partialFilterExpression` requires server version **>= 3.2** .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of this collection is automatically applied to this operation when using MongoDB >= 3.4. :Parameters: - `keys`: a single key or a list of (key, direction) pairs specifying the index to create - `**kwargs` (optional): any additional index creation options (see the above list) should be passed as keyword arguments Returns a Future. .. mongodoc:: indexes .. coroutinemethod:: inline_map_reduce(self, map, reduce, full_response=False, **kwargs) Perform an inline map/reduce operation on this collection. Perform the map/reduce operation on the server in RAM. A result collection is not created. The result set is returned as a list of documents. If `full_response` is ``False`` (default) returns the result documents in a list. Otherwise, returns the full response from the server to the `map reduce command`_. The :meth:`inline_map_reduce` method obeys the :attr:`read_preference` of this :class:`Collection`. :Parameters: - `map`: map function (as a JavaScript string) - `reduce`: reduce function (as a JavaScript string) - `full_response` (optional): if ``True``, return full response to this command - otherwise just return the result collection - `**kwargs` (optional): additional arguments to the `map reduce command`_ may be passed as keyword arguments to this helper method, e.g.:: yield db.test.inline_map_reduce(map, reduce, limit=2) Returns a Future. .. _map reduce command: http://docs.mongodb.org/manual/reference/command/mapReduce/ .. mongodoc:: mapreduce motor-2.1.0/doc/api-tornado/motor_database.rst000066400000000000000000000006341357430361400213630ustar00rootroot00000000000000: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-2.1.0/doc/api-tornado/web.rst000066400000000000000000000003441357430361400171520ustar00rootroot00000000000000:mod:`motor.web` - Integrate Motor with the Tornado web framework ================================================================= .. currentmodule:: motor.web .. automodule:: motor.web :members: :no-inherited-members: motor-2.1.0/doc/changelog.rst000066400000000000000000001172611357430361400161160ustar00rootroot00000000000000Changelog ========= .. currentmodule:: motor.motor_tornado 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://docs.mongodb.com/manual/reference/command/createUser/ .. _usersInfo: https://docs.mongodb.com/manual/reference/command/usersInfo/ .. _updateUser: https://docs.mongodb.com/manual/reference/command/updateUser/ .. _dropUser: https://docs.mongodb.com/manual/reference/command/createUser/ The deprecated GridFS classes ``MotorGridFS`` and ``AsyncIOMotorGridFS`` are deleted in favor of :class:`~motor.motor_tornado.MotorGridFSBucket` and :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: http://api.mongodb.com/python/current/changelog.html#changes-in-version-3-0 :class:`MotorReplicaSetClient` is removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In Motor 1.0, :class:`MotorClient` is the only class. Connect to a replica set with a "replicaSet" URI option or parameter:: 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.new_file() 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 messsage 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. .. _copy_database: http://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient.copy_database .. 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.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: http://docs.mongodb.org/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: http://api.mongodb.org/python/current/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-2.1.0/doc/conf.py000066400000000000000000000147171357430361400147360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Motor documentation build configuration file # # This file is execfile()d with the current directory set to its containing dir. import sys, os sys.path[0:0] = [os.path.abspath('..')] from pymongo import version as pymongo_version import motor # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.todo', 'doc.mongo_extensions', 'doc.motor_extensions', 'sphinx.ext.intersphinx', 'doc.coroutine_annotation'] 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 master toctree document. master_doc = 'index' # General information about the project. project = u'Motor' copyright = u'2016-present MongoDB, Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # 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 = [] # -- 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() ismaster = sync_client.admin.command('isMaster') server_info = sync_client.server_info() if 'setName' in ismaster: raise Exception( "Run doctests with standalone MongoDB 4.0 server, not a replica set") if ismaster.get('msg') == 'isdbgrid': raise Exception( "Run doctests with standalone MongoDB 4.0 server, not mongos") if server_info['versionArray'][:2] != [4, 0]: raise Exception( "Run doctests with standalone MongoDB 4.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', u'Motor Documentation', u'A. Jesse Jiryu Davis', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # 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_flags = ['inherited-members'] autodoc_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-2.1.0/doc/configuration.rst000066400000000000000000000035331357430361400170320ustar00rootroot00000000000000Configuration ============= 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 here: ``_ .. _python.org: https://www.python.org/downloads/ .. _homebrew: https://brew.sh/ .. _macports: https://www.macports.org/ .. _requests: https://pypi.python.org/pypi/requests 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. motor-2.1.0/doc/contributors.rst000066400000000000000000000005361357430361400167200ustar00rootroot00000000000000Contributors ============ 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 motor-2.1.0/doc/coroutine_annotation.py000066400000000000000000000017001357430361400202360ustar00rootroot00000000000000"""Gratefully adapted from aiohttp, provides coroutine support to autodoc.""" from sphinx.domains.python import PyModulelevel, PyClassmember from sphinx import addnodes class PyCoroutineMixin(object): def handle_signature(self, sig, signode): ret = super(PyCoroutineMixin, self).handle_signature(sig, signode) signode.insert(0, addnodes.desc_annotation('coroutine ', 'coroutine ')) return ret class PyCoroutineFunction(PyCoroutineMixin, PyModulelevel): def run(self): self.name = 'py:function' return PyModulelevel.run(self) class PyCoroutineMethod(PyCoroutineMixin, PyClassmember): def run(self): self.name = 'py:method' return PyClassmember.run(self) def setup(app): app.add_directive_to_domain('py', 'coroutinefunction', PyCoroutineFunction) app.add_directive_to_domain('py', 'coroutinemethod', PyCoroutineMethod) return {'version': '1.0', 'parallel_read_safe': True} motor-2.1.0/doc/developer-guide.rst000066400000000000000000000110521357430361400172360ustar00rootroot00000000000000=============== 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.4 and later. Motor also works with Tornado 4.5 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. Motor is single-source compatible with all supported Python versions, although there are some tricks for Python 3. There is some code for the ``async`` and ``await`` features of Python 3.5+ that is conditionally compiled with ``eval`` in ``core.py``. In ``setup.py`` there are tricks to conditionally import tests depending on Python version. ``setup.py`` also avoids installing the ``frameworks/asyncio`` directory in a Python 2 environment. 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`` - ``get_event_loop`` - ``get_future`` - ``is_event_loop`` - ``is_future`` - ``platform_info`` - ``pymongo_class_wrapper`` - ``run_on_executor`` - ``yieldable`` See the ``frameworks/tornado`` and ``frameworks/asyncio`` modules. 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 coroutines to call Motor methods with ``yield``, ``yield from``, or ``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 synchro27`` or ``tox -e synchro37`` to check out PyMongo's test suite and run it with Synchro. motor-2.1.0/doc/differences.rst000066400000000000000000000106331357430361400164370ustar00rootroot00000000000000.. 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, file_id = fs.open_upload_stream('test_file') grid_in.close() grid_in.my_field = 'my_value' # Sends update to server. Updating metadata on a :class:`MotorGridIn` is asynchronous, so the API is different:: @gen.coroutine def f(): fs = motor.motor_tornado.MotorGridFSBucket(db) grid_in, file_id = fs.open_upload_stream('test_file') yield grid_in.close() # Sends update to server. yield 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 = yield client.admin.current_op() locked = bool(result.get('fsyncLock', None)) system_js --------- PyMongo supports Javascript procedures stored in MongoDB with syntax like: .. code-block:: python >>> 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 @gen.coroutine def f(): cursor = db.collection.find()[100] # Iterates zero or one times. while (yield cursor.fetch_next): doc = cursor.next_object() The difference arises because the PyMongo :class:`~pymongo.cursor.Cursor`'s slicing operator blocks until it has queried the MongoDB server, and determines 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 @gen.coroutine def f(): yield db.create_collection( 'collection1', capped=True, size=1000) motor-2.1.0/doc/docs-requirements.txt000066400000000000000000000000201357430361400176270ustar00rootroot00000000000000tornado aiohttp motor-2.1.0/doc/examples/000077500000000000000000000000001357430361400152435ustar00rootroot00000000000000motor-2.1.0/doc/examples/aiohttp_example.py000066400000000000000000000027021357430361400210010ustar00rootroot00000000000000# These comments let tutorial-asyncio.rst include this code in sections. # -- setup-start -- import asyncio from aiohttp import web from motor.motor_asyncio import AsyncIOMotorClient @asyncio.coroutine def setup_db(): db = AsyncIOMotorClient().test yield from db.pages.drop() html = '{}' yield from db.pages.insert_one({'_id': 'page-one', 'body': html.format('Hello!')}) yield from db.pages.insert_one({'_id': 'page-two', 'body': html.format('Goodbye.')}) return db # -- setup-end -- # -- handler-start -- @asyncio.coroutine def page_handler(request): # If the visitor gets "/pages/page-one", then page_name is "page-one". page_name = request.match_info.get('page_name') # Retrieve the long-lived database handle. db = request.app['db'] # Find the page by its unique id. document = yield from db.pages.find_one(page_name) if not document: return web.HTTPNotFound(text='No page named {!r}'.format(page_name)) return web.Response(body=document['body'].encode(), content_type='text/html') # -- handler-end -- # -- main-start -- loop = asyncio.get_event_loop() db = loop.run_until_complete(setup_db()) app = web.Application() app['db'] = db # Route requests to the page_handler() coroutine. app.router.add_get('/pages/{page_name}', page_handler) web.run_app(app) # -- main-end -- motor-2.1.0/doc/examples/aiohttp_gridfs_example.py000066400000000000000000000032511357430361400223370ustar00rootroot00000000000000"""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.get_event_loop().run_until_complete(put_gridfile()) # Add "Content-Encoding: gzip" header for compressed data. def gzip_header(response, gridout): if gridout.metadata.get('compressed'): response.headers['Content-Encoding'] = 'gzip' 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-2.1.0/doc/examples/aiohttp_gridfs_example.rst000066400000000000000000000010501357430361400225120ustar00rootroot00000000000000AIOHTTPGridFS 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-2.1.0/doc/examples/authentication.rst000066400000000000000000000016011357430361400210120ustar00rootroot00000000000000.. 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: http://docs.mongodb.org/manual/reference/connection-string/ motor-2.1.0/doc/examples/bulk.rst000066400000000000000000000141561357430361400167410ustar00rootroot00000000000000.. 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:: >>> @gen.coroutine ... def f(): ... yield db.test.insert_many(({'i': i} for i in range(10000))) ... count = yield 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 >>> @gen.coroutine ... def f(): ... result = yield 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 >>> @gen.coroutine ... def f(): ... requests = [ ... ReplaceOne({'j': 2}, {'i': 5}), ... InsertOne({'_id': 4}), # Violates the unique key constraint on _id. ... DeleteOne({'i': 5})] ... try: ... yield 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, '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 >>> @gen.coroutine ... def f(): ... requests = [ ... InsertOne({'_id': 1}), ... DeleteOne({'_id': 2}), ... InsertOne({'_id': 3}), ... ReplaceOne({'_id': 4}, {'i': 1})] ... try: ... yield 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, 'op': {'_id': 1}}, {'code': 11000, 'errmsg': '... duplicate key error ...', 'index': 2, '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 >>> @gen.coroutine ... def f(): ... coll = db.get_collection( ... 'test', write_concern=WriteConcern(w=4, wtimeout=1)) ... try: ... yield 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-2.1.0/doc/examples/index.rst000066400000000000000000000003771357430361400171130ustar00rootroot00000000000000Motor Examples ============== .. seealso:: :doc:`../tutorial-tornado` .. toctree:: bulk monitoring tailable-cursors tornado_change_stream_example authentication aiohttp_gridfs_example See also :ref:`example-web-application-aiohttp`. motor-2.1.0/doc/examples/monitoring.rst000066400000000000000000000110741357430361400201650ustar00rootroot00000000000000.. 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.py`_ example file in the Motor repository .. _The Command Monitoring Spec: .. _Command Monitoring: https://github.com/mongodb/specifications/blob/master/source/command-monitoring/command-monitoring.rst .. _The Topology Monitoring Spec: .. _Topology Monitoring: https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring-monitoring.rst .. _monitoring.py: https://github.com/mongodb/motor/blob/master/doc/examples/monitoring.py motor-2.1.0/doc/examples/monitoring_example.py000066400000000000000000000107361357430361400215240ustar00rootroot00000000000000# 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("Command {0.command_name} with request id " "{0.request_id} started on server " "{0.connection_id}".format(event)) def succeeded(self, event): logging.info("Command {0.command_name} with request id " "{0.request_id} on server {0.connection_id} " "succeeded in {0.duration_micros} " "microseconds".format(event)) def failed(self, event): logging.info("Command {0.command_name} with request id " "{0.request_id} on server {0.connection_id} " "failed in {0.duration_micros} " "microseconds".format(event)) # command logger end # command logger register start 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("Server {0.server_address} added to topology " "{0.topology_id}".format(event)) def description_changed(self, event): previous_server_type = event.previous_description.server_type new_server_type = event.new_description.server_type if new_server_type != previous_server_type: logging.info( "Server {0.server_address} changed type from " "{0.previous_description.server_type_name} to " "{0.new_description.server_type_name}".format(event)) def closed(self, event): logging.warning("Server {0.server_address} removed from topology " "{0.topology_id}".format(event)) monitoring.register(ServerLogger()) # server logger end # topology logger start class TopologyLogger(monitoring.TopologyListener): def opened(self, event): logging.info("Topology with id {0.topology_id} " "opened".format(event)) def description_changed(self, event): logging.info("Topology description updated for " "topology id {0.topology_id}".format(event)) previous_topology_type = event.previous_description.topology_type new_topology_type = event.new_description.topology_type if new_topology_type != previous_topology_type: logging.info( "Topology {0.topology_id} changed type from " "{0.previous_description.topology_type_name} to " "{0.new_description.topology_type_name}".format(event)) def closed(self, event): logging.info("Topology with id {0.topology_id} " "closed".format(event)) monitoring.register(TopologyLogger()) # topology logger end # heartbeat logger start class HeartbeatLogger(monitoring.ServerHeartbeatListener): def started(self, event): logging.info("Heartbeat sent to server " "{0.connection_id}".format(event)) def succeeded(self, event): logging.info("Heartbeat to server {0.connection_id} " "succeeded with reply " "{0.reply.document}".format(event)) def failed(self, event): logging.warning("Heartbeat to server {0.connection_id} " "failed with error {0.reply}".format(event)) monitoring.register(HeartbeatLogger()) # heartbeat logger end motor-2.1.0/doc/examples/tailable-cursors.rst000066400000000000000000000020541357430361400212510ustar00rootroot00000000000000.. currentmodule:: motor.motor_tornado Motor Tailable Cursor Example ============================= This example describes using Motor with Tornado. Beginning in version 0.5 Motor can also integrate with asyncio instead of Tornado. A cursor on a capped collection can be tailed using :meth:`~MotorCursor.fetch_next`: .. code-block:: python @gen.coroutine def tail_example(): results = [] collection = db.my_capped_collection cursor = collection.find(cursor_type=CursorType.TAILABLE, await_data=True) while True: if not cursor.alive: now = datetime.datetime.utcnow() # While collection is empty, tailable cursor dies immediately yield gen.sleep(1) cursor = collection.find(cursor_type=CursorType.TAILABLE, await_data=True) if (yield cursor.fetch_next): results.append(cursor.next_object()) print results .. seealso:: `Tailable cursors `_ motor-2.1.0/doc/examples/tornado_change_stream_example.py000066400000000000000000000070051357430361400236600ustar00rootroot00000000000000import logging import os import sys from base64 import urlsafe_b64encode from pprint import pformat from motor.motor_tornado import MotorClient from bson import json_util # Installed with PyMongo. import tornado.escape import tornado.ioloop import tornado.options import tornado.web import tornado.websocket from tornado.options import define, options 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(Application, self).__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: logging.error("Error sending message", exc_info=True) @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. html_id = urlsafe_b64encode(change['_id']['_data']).decode().rstrip('=') change.pop('_id') change['html'] = '
    %s
    ' % ( html_id, tornado.escape.xhtml_escape(pformat(change))) 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('Invalid ns "%s", must contain a "."' % (options.ns,)) 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. loop.add_callback(watch, collection) try: loop.start() except KeyboardInterrupt: pass finally: if change_stream is not None: change_stream.close() if __name__ == "__main__": main() motor-2.1.0/doc/examples/tornado_change_stream_example.rst000066400000000000000000000027561357430361400240500ustar00rootroot00000000000000.. _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:: none > 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:: none 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-2.1.0/doc/examples/tornado_change_stream_templates/000077500000000000000000000000001357430361400236475ustar00rootroot00000000000000motor-2.1.0/doc/examples/tornado_change_stream_templates/index.html000066400000000000000000000023241357430361400256450ustar00rootroot00000000000000

    Changes

    {% for change in changes %} {% raw change['html'] %} {% end %}
    motor-2.1.0/doc/features.rst000066400000000000000000000025671357430361400160070ustar00rootroot00000000000000============== 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-2.1.0/doc/index.rst000066400000000000000000000040741357430361400152730ustar00rootroot00000000000000Motor: 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 How To Ask For Help ------------------- Post questions about Motor to the `mongodb-user list on Google Groups `_. For confirmed issues or feature requests, open a case in `Jira `_ in the "MOTOR" project. Contents -------- .. toctree:: :maxdepth: 1 differences features installation requirements configuration tutorial-tornado tutorial-asyncio examples/index changelog migrate-to-motor-2 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-2.1.0/doc/installation.rst000066400000000000000000000004051357430361400166570ustar00rootroot00000000000000Installation ============ Install Motor from PyPI_ with pip_:: $ python -m pip install motor Pip automatically installs Motor's prerequisite packages. See :doc:`requirements`. .. _PyPI: http://pypi.python.org/pypi/motor .. _pip: http://pip-installer.org motor-2.1.0/doc/make.bat000066400000000000000000000057751357430361400150500ustar00rootroot00000000000000@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-2.1.0/doc/migrate-to-motor-2.rst000066400000000000000000000165241357430361400175340ustar00rootroot00000000000000Motor 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-2.1.0/doc/mongo_extensions.py000066400000000000000000000056661357430361400174120ustar00rootroot00000000000000# 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(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-2.1.0/doc/motor_extensions.py000066400000000000000000000155101357430361400174200ustar00rootroot00000000000000# 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: 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']: # 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) # Store some info for process_motor_nodes() full_name = '%s.%s.%s' % ( motor_class.__module__, motor_class.__name__, name) full_name_legacy = 'motor.%s.%s.%s' % ( motor_class.__module__, motor_class.__name__, name) # These sub-attributes are set in motor.asynchronize() has_coroutine_annotation = getattr(attr, 'coroutine_annotation', False) is_async_method = getattr(attr, 'is_async_method', False) is_cursor_method = getattr(attr, 'is_motorcursor_chaining_method', False) 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-2.1.0/doc/pydoctheme/000077500000000000000000000000001357430361400155665ustar00rootroot00000000000000motor-2.1.0/doc/pydoctheme/static/000077500000000000000000000000001357430361400170555ustar00rootroot00000000000000motor-2.1.0/doc/pydoctheme/static/pydoctheme.css000066400000000000000000000056441357430361400217410ustar00rootroot00000000000000@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-2.1.0/doc/pydoctheme/theme.conf000066400000000000000000000010231357430361400175330ustar00rootroot00000000000000[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-2.1.0/doc/requirements.rst000066400000000000000000000147071357430361400167130ustar00rootroot00000000000000Requirements ============ The current version of Motor requires: * CPython 2.7, or 3.4 and later. * PyMongo_ 3.10 and later. Motor can integrate with either Tornado or asyncio. Requires the `futures`_ package from PyPI on Python 2. The default authentication mechanism for MongoDB 3.0+ is SCRAM-SHA-1. Install `backports.pbkdf2`_ for faster authentication with MongoDB 3.0+, especially on Python older than 2.7.8. (Python 2.7.9 and later, or Python 3.4 and later, have builtin hash functions nearly as fast as backports.pbkdf2.) Building the docs requires `sphinx`_. .. _PyMongo: https://pypi.python.org/pypi/pymongo/ .. _futures: https://pypi.python.org/pypi/futures .. _backports.pbkdf2: https://pypi.python.org/pypi/backports.pbkdf2/ .. _sphinx: http://sphinx.pocoo.org/ Starting in version 2.1, Motor 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. .. _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+ | +-------------------+-----------------+ Motor and MongoDB ````````````````` +---------------------------------------------------------------------------+ | MongoDB Version | +=====================+=====+=====+=====+=====+=====+=====+=====+=====+=====+ | | 2.2 | 2.4 | 2.6 | 3.0 | 3.2 | 3.4 | 3.6 | 4.0 | 4.2 | +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | Motor Version | 1.0 | Y | Y | Y | Y | Y |**N**|**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.1 | Y | Y | Y | Y | Y | Y |**N**|**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.2 |**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 1.3 |**N**|**N**| Y | Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.0 |**N**|**N**|**N**| Y | Y | Y | Y | Y | Y | +---------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | 2.1 |**N**|**N**|**N**| 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://docs.mongodb.org/ecosystem/drivers/python/#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 | +---------------+-----+-----+-----+-----+-----+ 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. Python 3.4.4 and later are supported by all Motor versions. +-------------------------------------------------------------------------------------+ | Python Version | +=====================+=====+=====+=====+=====+=====+=======+=======+=====+=====+=====+ | | 2.5 | 2.6 | 2.7 | 3.3 | 3.4 | 3.5.0 | 3.5.2 | 3.6 | 3.7 | 3.8 | +---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+ | Motor Version | 1.0 |**N**| Y | Y | Y | Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+ | | 1.1 |**N**| Y | Y | Y | Y | Y | Y | Y |**N**|**N**| +---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+ | | 1.2 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y |**N**| +---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+ | | 1.3 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y |**N**| +---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+ | | 2.0 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y |**N**| +---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+ | | 2.1 |**N**|**N**| Y |**N**| Y |**N** | Y | Y | Y | Y | +---------------+-----+-----+-----+-----+-----+-----+-------+-------+-----+-----+-----+ Not Supported ------------- Motor does not support Jython or IronPython. motor-2.1.0/doc/static/000077500000000000000000000000001357430361400147145ustar00rootroot00000000000000motor-2.1.0/doc/static/sidebar.js000066400000000000000000000142141357430361400166650ustar00rootroot00000000000000/* * 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-2.1.0/doc/tutorial-asyncio.rst000066400000000000000000000417741357430361400175020ustar00rootroot00000000000000.. 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 from asyncio import coroutine db = motor.motor_asyncio.AsyncIOMotorClient().test_database .. testsetup:: after-inserting-2000-docs import pymongo import motor.motor_asyncio import asyncio from asyncio import coroutine db = motor.motor_asyncio.AsyncIOMotorClient().test_database pymongo.MongoClient().test_database.test_collection.insert_many( [{'i': i} for i in range(2000)]) .. 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.4 or later, do:: $ python3 -m pip install motor This tutorial assumes that a MongoDB instance is running on the default host and port. Assuming you have `downloaded and installed `_ MongoDB, you can start it like so: .. code-block:: bash $ mongod .. _pip: http://www.pip-installer.org/en/latest/installing.html .. _MongoDB Tutorial: http://docs.mongodb.org/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: http://docs.mongodb.org/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 = asyncio.get_event_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({}) 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 = asyncio.get_event_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 = asyncio.get_event_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 = asyncio.get_event_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 3} {'_id': ObjectId('...'), 'i': 4} 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 = asyncio.get_event_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 = asyncio.get_event_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://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches Iteration in Python 3.4 ~~~~~~~~~~~~~~~~~~~~~~~ In Python versions without ``async for``, handle one document at a time with :attr:`~motor.motor_asyncio.AsyncIOMotorCursor.fetch_next` and :meth:`~motor.motor_asyncio.AsyncIOMotorCursor.next_object`: .. doctest:: after-inserting-2000-docs >>> @coroutine ... def do_find(): ... cursor = db.test_collection.find({'i': {'$lt': 5}}) ... while (yield from cursor.fetch_next): ... document = cursor.next_object() ... pprint.pprint(document) ... >>> loop = asyncio.get_event_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 3} {'_id': ObjectId('...'), 'i': 4} Counting Documents ------------------ Use :meth:`~motor.motor_asyncio.AsyncIOMotorCollection.count_documents` to determine the number of documents in a collection, or the number of documents that match a query: .. 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 = asyncio.get_event_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 = asyncio.get_event_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 = asyncio.get_event_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_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 = asyncio.get_event_loop() >>> loop.run_until_complete(do_delete_many()) 2000 documents before calling delete_many() 1000 documents after .. mongodoc:: remove Commands -------- All operations on MongoDB are implemented internally as commands. Run them using the :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command` method on :class:`~motor.motor_asyncio.AsyncIOMotorDatabase`:: .. 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 = asyncio.get_event_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: http://docs.mongodb.org/manual/ motor-2.1.0/doc/tutorial-tornado.rst000066400000000000000000000416401357430361400174730ustar00rootroot00000000000000.. 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: http://docs.mongodb.org/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: http://docs.mongodb.org/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({}) 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({}) 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://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches Iteration in Python 3.4 ~~~~~~~~~~~~~~~~~~~~~~~ In Python versions without ``async for``, handle one document at a time with :attr:`~MotorCursor.fetch_next` and :meth:`~MotorCursor.next_object`: .. doctest:: after-inserting-2000-docs >>> @gen.coroutine ... def do_find(): ... cursor = db.test_collection.find({'i': {'$lt': 5}}) ... while (yield cursor.fetch_next): ... document = cursor.next_object() ... pprint.pprint(document) ... >>> IOLoop.current().run_sync(do_find) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 3} {'_id': ObjectId('...'), 'i': 4} 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_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) 2000 documents before calling delete_many() 1000 documents after .. mongodoc:: remove Commands -------- All operations on MongoDB are implemented internally as commands. Run them using the :meth:`~motor.motor_tornado.MotorDatabase.command` method on :class:`~motor.motor_tornado.MotorDatabase`:: .. 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: http://docs.mongodb.org/manual/ motor-2.1.0/ez_setup.py000066400000000000000000000301621357430361400150720ustar00rootroot00000000000000#!/usr/bin/env python """ Setuptools bootstrapping installer. Maintained at https://github.com/pypa/setuptools/tree/bootstrap. Run this script to install or upgrade setuptools. """ import os import shutil import sys import tempfile import zipfile import optparse import subprocess import platform import textwrap import contextlib import json import codecs from distutils import log try: from urllib.request import urlopen from urllib.parse import urljoin except ImportError: from urllib2 import urlopen from urlparse import urljoin try: from site import USER_SITE except ImportError: USER_SITE = None LATEST = object() DEFAULT_VERSION = LATEST DEFAULT_URL = "https://pypi.io/packages/source/s/setuptools/" DEFAULT_SAVE_DIR = os.curdir def _python_cmd(*args): """ Execute a command. Return True if the command succeeded. """ args = (sys.executable,) + args return subprocess.call(args) == 0 def _install(archive_filename, install_args=()): """Install Setuptools.""" with archive_context(archive_filename): # installing log.warn('Installing Setuptools') if not _python_cmd('setup.py', 'install', *install_args): log.warn('Something went wrong during the installation.') log.warn('See the error message above.') # exitcode will be 2 return 2 def _build_egg(egg, archive_filename, to_dir): """Build Setuptools egg.""" with archive_context(archive_filename): # building an egg log.warn('Building a Setuptools egg in %s', to_dir) _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) # returning the result log.warn(egg) if not os.path.exists(egg): raise IOError('Could not build the egg.') class ContextualZipFile(zipfile.ZipFile): """Supplement ZipFile class to support context manager for Python 2.6.""" def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() def __new__(cls, *args, **kwargs): """Construct a ZipFile or ContextualZipFile as appropriate.""" if hasattr(zipfile.ZipFile, '__exit__'): return zipfile.ZipFile(*args, **kwargs) return super(ContextualZipFile, cls).__new__(cls) @contextlib.contextmanager def archive_context(filename): """ Unzip filename to a temporary directory, set to the cwd. The unzipped target is cleaned up after. """ tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) with ContextualZipFile(filename) as archive: archive.extractall() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) yield finally: os.chdir(old_wd) shutil.rmtree(tmpdir) def _do_download(version, download_base, to_dir, download_delay): """Download Setuptools.""" py_desig = 'py{sys.version_info[0]}.{sys.version_info[1]}'.format(sys=sys) tp = 'setuptools-{version}-{py_desig}.egg' egg = os.path.join(to_dir, tp.format(**locals())) if not os.path.exists(egg): archive = download_setuptools(version, download_base, to_dir, download_delay) _build_egg(egg, archive, to_dir) sys.path.insert(0, egg) # Remove previously-imported pkg_resources if present (see # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). if 'pkg_resources' in sys.modules: _unload_pkg_resources() import setuptools setuptools.bootstrap_install_from = egg def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=DEFAULT_SAVE_DIR, download_delay=15): """ Ensure that a setuptools version is installed. Return None. Raise SystemExit if the requested version or later cannot be installed. """ version = _resolve_version(version) to_dir = os.path.abspath(to_dir) # prior to importing, capture the module state for # representative modules. rep_modules = 'pkg_resources', 'setuptools' imported = set(sys.modules).intersection(rep_modules) try: import pkg_resources pkg_resources.require("setuptools>=" + version) # a suitable version is already installed return except ImportError: # pkg_resources not available; setuptools is not installed; download pass except pkg_resources.DistributionNotFound: # no version of setuptools was found; allow download pass except pkg_resources.VersionConflict as VC_err: if imported: _conflict_bail(VC_err, version) # otherwise, unload pkg_resources to allow the downloaded version to # take precedence. del pkg_resources _unload_pkg_resources() return _do_download(version, download_base, to_dir, download_delay) def _conflict_bail(VC_err, version): """ Setuptools was imported prior to invocation, so it is unsafe to unload it. Bail out. """ conflict_tmpl = textwrap.dedent(""" The required version of setuptools (>={version}) is not available, and can't be installed while this script is running. Please install a more recent version first, using 'easy_install -U setuptools'. (Currently using {VC_err.args[0]!r}) """) msg = conflict_tmpl.format(**locals()) sys.stderr.write(msg) sys.exit(2) def _unload_pkg_resources(): sys.meta_path = [ importer for importer in sys.meta_path if importer.__class__.__module__ != 'pkg_resources.extern' ] del_modules = [ name for name in sys.modules if name.startswith('pkg_resources') ] for mod_name in del_modules: del sys.modules[mod_name] def _clean_check(cmd, target): """ Run the command to download target. If the command fails, clean up before re-raising the error. """ try: subprocess.check_call(cmd) except subprocess.CalledProcessError: if os.access(target, os.F_OK): os.unlink(target) raise def download_file_powershell(url, target): """ Download the file at url to target using Powershell. Powershell will validate trust. Raise an exception if the command cannot complete. """ target = os.path.abspath(target) ps_cmd = ( "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " "[System.Net.CredentialCache]::DefaultCredentials; " '(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")' % locals() ) cmd = [ 'powershell', '-Command', ps_cmd, ] _clean_check(cmd, target) def has_powershell(): """Determine if Powershell is available.""" if platform.system() != 'Windows': return False cmd = ['powershell', '-Command', 'echo test'] with open(os.path.devnull, 'wb') as devnull: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except Exception: return False return True download_file_powershell.viable = has_powershell def download_file_curl(url, target): cmd = ['curl', url, '--location', '--silent', '--output', target] _clean_check(cmd, target) def has_curl(): cmd = ['curl', '--version'] with open(os.path.devnull, 'wb') as devnull: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except Exception: return False return True download_file_curl.viable = has_curl def download_file_wget(url, target): cmd = ['wget', url, '--quiet', '--output-document', target] _clean_check(cmd, target) def has_wget(): cmd = ['wget', '--version'] with open(os.path.devnull, 'wb') as devnull: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except Exception: return False return True download_file_wget.viable = has_wget def download_file_insecure(url, target): """Use Python to download the file, without connection authentication.""" src = urlopen(url) try: # Read all the data in one block. data = src.read() finally: src.close() # Write all the data in one block to avoid creating a partial file. with open(target, "wb") as dst: dst.write(data) download_file_insecure.viable = lambda: True def get_best_downloader(): downloaders = ( download_file_powershell, download_file_curl, download_file_wget, download_file_insecure, ) viable_downloaders = (dl for dl in downloaders if dl.viable()) return next(viable_downloaders, None) def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=DEFAULT_SAVE_DIR, delay=15, downloader_factory=get_best_downloader): """ Download setuptools from a specified location and return its filename. `version` should be a valid setuptools version number that is available as an sdist for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. ``downloader_factory`` should be a function taking no arguments and returning a function for downloading a URL to a target. """ version = _resolve_version(version) # making sure we use the absolute path to_dir = os.path.abspath(to_dir) zip_name = "setuptools-%s.zip" % version url = download_base + zip_name saveto = os.path.join(to_dir, zip_name) if not os.path.exists(saveto): # Avoid repeated downloads log.warn("Downloading %s", url) downloader = downloader_factory() downloader(url, saveto) return os.path.realpath(saveto) def _resolve_version(version): """ Resolve LATEST version """ if version is not LATEST: return version meta_url = urljoin(DEFAULT_URL, '/pypi/setuptools/json') resp = urlopen(meta_url) with contextlib.closing(resp): try: charset = resp.info().get_content_charset() except Exception: # Python 2 compat; assume UTF-8 charset = 'UTF-8' reader = codecs.getreader(charset) doc = json.load(reader(resp)) return str(doc['info']['version']) def _build_install_args(options): """ Build the arguments to 'python setup.py install' on the setuptools package. Returns list of command line arguments. """ return ['--user'] if options.user_install else [] def _parse_args(): """Parse the command line for options.""" parser = optparse.OptionParser() parser.add_option( '--user', dest='user_install', action='store_true', default=False, help='install in user site package') parser.add_option( '--download-base', dest='download_base', metavar="URL", default=DEFAULT_URL, help='alternative URL from where to download the setuptools package') parser.add_option( '--insecure', dest='downloader_factory', action='store_const', const=lambda: download_file_insecure, default=get_best_downloader, help='Use internal, non-validating downloader' ) parser.add_option( '--version', help="Specify which version to download", default=DEFAULT_VERSION, ) parser.add_option( '--to-dir', help="Directory to save (and re-use) package", default=DEFAULT_SAVE_DIR, ) options, args = parser.parse_args() # positional arguments are ignored return options def _download_args(options): """Return args for download_setuptools function from cmdline args.""" return dict( version=options.version, download_base=options.download_base, downloader_factory=options.downloader_factory, to_dir=options.to_dir, ) def main(): """Install or upgrade setuptools and EasyInstall.""" options = _parse_args() archive = download_setuptools(**_download_args(options)) return _install(archive, _build_install_args(options)) if __name__ == '__main__': sys.exit(main()) motor-2.1.0/motor/000077500000000000000000000000001357430361400140205ustar00rootroot00000000000000motor-2.1.0/motor/__init__.py000066400000000000000000000030621357430361400161320ustar00rootroot00000000000000# 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 __future__ import unicode_literals, absolute_import from motor.motor_py2_compat import text_type version_tuple = (2, 1, 0) def get_version_string(): return '.'.join(map(str, version_tuple)) version = get_version_string() """Current version of Motor.""" try: import tornado except ImportError: tornado = None else: # For backwards compatibility with Motor 0.4, export Motor's Tornado classes # at module root. This may change in the future. First get __all__. from .motor_tornado import * # Now some classes that aren't in __all__ but might be expected. from .motor_tornado import (MotorCollection, MotorDatabase, MotorGridFSBucket, MotorGridIn, MotorGridOut) # Make "from motor import *" the same as "from motor.motor_tornado import *" from .motor_tornado import __all__ motor-2.1.0/motor/aiohttp/000077500000000000000000000000001357430361400154705ustar00rootroot00000000000000motor-2.1.0/motor/aiohttp/__init__.py000066400000000000000000000225441357430361400176100ustar00rootroot00000000000000# 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 asyncio import datetime import mimetypes import aiohttp.web import gridfs from motor.motor_asyncio import (AsyncIOMotorDatabase, AsyncIOMotorGridFSBucket) 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 """ pass def _config_error(request): try: formatter = request.match_info.route.resource.get_info()['formatter'] msg = ('Bad AIOHTTPGridFS route "%s", requires a {filename} variable' % formatter) 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://docs.mongodb.com/manual/core/gridfs/ """ def __init__(self, database, root_collection='fs', get_gridfs_file=get_gridfs_file, get_cache_time=get_cache_time, set_extra_headers=set_extra_headers): 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 @asyncio.coroutine 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 = yield from self._get_gridfs_file(self._bucket, filename, request) except gridfs.NoFile: raise aiohttp.web.HTTPNotFound(text=request.path) resp = aiohttp.web.StreamResponse() self._set_standard_headers(request.path, resp, gridout) # 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('"') == gridout.md5: resp.set_status(304) return resp resp.content_length = gridout.length yield from resp.prepare(request) if request.method == 'GET': written = 0 while written < gridout.length: # Reading chunk_size at a time minimizes buffering. chunk = yield from gridout.read(gridout.chunk_size) yield from resp.write(chunk) written += len(chunk) return resp def _set_standard_headers(self, path, resp, gridout): 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 # MD5 is calculated on the MongoDB server when GridFS file is created. resp.headers["Etag"] = '"%s"' % gridout.md5 # Overridable method get_cache_time. cache_time = self._get_cache_time(path, gridout.upload_date, gridout.content_type) if cache_time > 0: resp.headers["Expires"] = ( datetime.datetime.utcnow() + datetime.timedelta(seconds=cache_time) ).strftime("%a, %d %b %Y %H:%M:%S GMT") resp.headers["Cache-Control"] = "max-age=" + str(cache_time) else: resp.headers["Cache-Control"] = "public" motor-2.1.0/motor/core.py000066400000000000000000002013511357430361400153240ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import """Framework-agnostic core of Motor, an asynchronous driver for MongoDB.""" import functools import sys import textwrap import pymongo import pymongo.auth import pymongo.common import pymongo.database import pymongo.errors import pymongo.mongo_client import pymongo.mongo_replica_set_client import pymongo.monotonic 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 Cursor, RawBatchCursor, _QUERY_OPTIONS from pymongo.database import Database from pymongo.driver_info import DriverInfo from . import version as motor_version from .metaprogramming import (AsyncCommand, AsyncRead, AsyncWrite, coroutine_annotation, create_class_with_framework, DelegateMethod, motor_coroutine, MotorCursorChainingMethod, ReadOnlyProperty, unwrap_args_session, unwrap_kwargs_session) from .motor_common import callback_type_error from motor.docstrings import * HAS_SSL = True try: import ssl except ImportError: ssl = None HAS_SSL = False PY35 = sys.version_info >= (3, 5) # 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 (pymongo.monotonic.time() - 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(object): 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 '%s(%r)' % (self.__class__.__name__, self.delegate) 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() drop_database = AsyncCommand().unwrap('MotorDatabase') event_listeners = ReadOnlyProperty() fsync = AsyncCommand(doc=fsync_doc) get_database = DelegateMethod(doc=get_database_doc).wrap(Database) get_default_database = DelegateMethod(doc=get_default_database_doc).wrap(Database) HOST = ReadOnlyProperty() is_mongos = ReadOnlyProperty() is_primary = ReadOnlyProperty() list_databases = AsyncRead().wrap(CommandCursor) list_database_names = AsyncRead() local_threshold_ms = ReadOnlyProperty() max_bson_size = ReadOnlyProperty() max_idle_time_ms = ReadOnlyProperty() max_message_size = ReadOnlyProperty() max_pool_size = ReadOnlyProperty() max_write_batch_size = ReadOnlyProperty() min_pool_size = ReadOnlyProperty() nodes = ReadOnlyProperty() PORT = ReadOnlyProperty() primary = ReadOnlyProperty() read_concern = ReadOnlyProperty() retry_reads = ReadOnlyProperty() retry_writes = ReadOnlyProperty() secondaries = ReadOnlyProperty() server_info = AsyncRead() server_selection_timeout = ReadOnlyProperty() start_session = AsyncCommand(doc=start_session_doc).wrap(ClientSession) unlock = AsyncCommand() 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 = self._framework.get_event_loop() kwargs.setdefault('connect', False) kwargs.setdefault( 'driver', DriverInfo('Motor', motor_version, self._framework.platform_info())) delegate = self.__delegate_class__(*args, **kwargs) super(AgnosticBaseProperties, self).__init__(delegate) self.io_loop = 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): """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. :Returns: A :class:`~MotorChangeStream`. .. 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) def __getattr__(self, name): if name.startswith('_'): raise AttributeError( "%s has no attribute %r. To access the %s" " database, use client['%s']." % ( self.__class__.__name__, name, name, 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(object): """Internal transaction context manager for start_transaction.""" def __init__(self, session): self._session = session if PY35: exec(textwrap.dedent(""" 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() """), globals(), locals()) 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() if PY35: exec(textwrap.dedent(""" async def with_transaction(self, coro, read_concern=None, write_concern=None, read_preference=None, max_commit_time_ms=None): start_time = pymongo.monotonic.time() 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 with_transaction.__doc__ = with_transaction_doc"""), globals(), locals()) 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 if PY35: exec(textwrap.dedent(""" 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) """), globals(), locals()) 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 command = AsyncCommand(doc=cmd_doc) create_collection = AsyncCommand().wrap(Collection) current_op = AsyncRead(doc=current_op_doc) dereference = AsyncRead() drop_collection = AsyncCommand().unwrap('MotorCollection') get_collection = DelegateMethod().wrap(Collection) list_collection_names = AsyncRead(doc=list_collection_names_doc) list_collections = AsyncRead() name = ReadOnlyProperty() profiling_info = AsyncRead() profiling_level = AsyncRead() set_profiling_level = AsyncCommand() validate_collection = AsyncRead().unwrap('MotorCollection') with_options = DelegateMethod().wrap(Database) incoming_manipulators = ReadOnlyProperty() incoming_copying_manipulators = ReadOnlyProperty() outgoing_manipulators = ReadOnlyProperty() outgoing_copying_manipulators = ReadOnlyProperty() _async_aggregate = AsyncRead(attr_name='aggregate') def __init__(self, client, name, **kwargs): self._client = client delegate = kwargs.get('_delegate') or Database( client.delegate, name, **kwargs) super(self.__class__, self).__init__(delegate) def aggregate(self, pipeline, **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`. Returns a :class:`MotorCommandCursor` that can be iterated like a cursor from :meth:`find`:: # Lists all operations currently running on the server. pipeline = [{"$currentOp": {}}] cursor = client.admin.aggregate(pipeline) while (yield cursor.fetch_next): operation = cursor.next_object() print(operation) In Python 3.5 and newer, aggregation cursors can be iterated elegantly in native coroutines with `async for`:: async def f(): 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://docs.mongodb.com/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_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): """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. :Returns: A :class:`~MotorChangeStream`. .. 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) @property def client(self): """This MotorDatabase's :class:`MotorClient`.""" return self._client def __getattr__(self, name): if name.startswith('_'): raise AttributeError( "%s has no attribute %r. To access the %s" " collection, use database['%s']." % ( self.__class__.__name__, name, name, 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( "%s.open_sync() is unnecessary Motor 0.2, " "see changelog for details." % client_class_name) raise TypeError( "MotorDatabase object is not callable. If you meant to " "call the '%s' method on a %s object it is " "failing because no such method exists." % ( database_name, client_class_name)) 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) else: return obj def get_io_loop(self): return self._client.get_io_loop() class AgnosticCollection(AgnosticBaseProperties): __motor_class_name__ = 'MotorCollection' __delegate_class__ = Collection bulk_write = AsyncCommand(doc=bulk_write_doc) count_documents = AsyncRead() create_index = AsyncCommand() create_indexes = AsyncCommand(doc=create_indexes_doc) delete_many = AsyncCommand(doc=delete_many_doc) delete_one = AsyncCommand(doc=delete_one_doc) distinct = AsyncRead() drop = AsyncCommand(doc=drop_doc) drop_index = AsyncCommand() drop_indexes = AsyncCommand() estimated_document_count = AsyncCommand() find_one = AsyncRead(doc=find_one_doc) find_one_and_delete = AsyncCommand(doc=find_one_and_delete_doc) find_one_and_replace = AsyncCommand(doc=find_one_and_replace_doc) find_one_and_update = AsyncCommand(doc=find_one_and_update_doc) full_name = ReadOnlyProperty() index_information = AsyncRead(doc=index_information_doc) inline_map_reduce = AsyncRead() insert_many = AsyncWrite(doc=insert_many_doc) insert_one = AsyncCommand(doc=insert_one_doc) map_reduce = AsyncCommand(doc=mr_doc).wrap(Collection) name = ReadOnlyProperty() options = AsyncRead() reindex = AsyncCommand() rename = AsyncCommand() replace_one = AsyncCommand(doc=replace_one_doc) update_many = AsyncCommand(doc=update_many_doc) update_one = AsyncCommand(doc=update_one_doc) 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') 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 or Collection( database.delegate, name, codec_options=codec_options, read_preference=read_preference, write_concern=write_concern, read_concern=read_concern) super(self.__class__, self).__init__(delegate) self.database = database def __getattr__(self, name): # Dotted collection name, like "foo.bar". if name.startswith('_'): full_name = "%s.%s" % (self.name, name) raise AttributeError( "%s has no attribute %r. To access the %s" " collection, use database['%s']." % ( self.__class__.__name__, name, full_name, 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, **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 Returns a :class:`MotorCommandCursor` that can be iterated like a cursor from :meth:`find`:: pipeline = [{'$project': {'name': {'$toUpper': '$name'}}}] cursor = collection.aggregate(pipeline) while (yield cursor.fetch_next): doc = cursor.next_object() print(doc) In Python 3.5 and newer, aggregation cursors can be iterated elegantly in native coroutines with `async for`:: async def f(): async for doc in collection.aggregate(pipeline): print(doc) :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 a ``yield``. See :ref:`aggregation changes in Motor 0.5 `. .. versionchanged:: 0.2 Added cursor support. .. _aggregate command: http://docs.mongodb.org/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_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): """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. loop.add_callback(watch_collection) try: loop.start() except KeyboardInterrupt: pass finally: if change_stream is not None: change_stream.close() # asyncio from asyncio import get_event_loop def main(): loop = get_event_loop() task = loop.create_task(watch_collection) try: loop.run_forever() except KeyboardInterrupt: pass finally: if change_stream is not None: change_stream.close() # Prevent "Task was destroyed but it is pending!" loop.run_until_complete(task) 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. :Returns: A :class:`~MotorChangeStream`. See the :ref:`tornado_change_stream_example`. .. 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.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) def list_indexes(self, session=None): """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_'), ('ns', 'test.test')]) """ 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) 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""" _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 :attr:`fetch_next`, 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(AgnosticBaseCursor, self).__init__(delegate=cursor) self.collection = collection self.started = False self.closed = False # python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions if PY35: exec(textwrap.dedent(""" def __aiter__(self): return self async def __anext__(self): # An optimization: skip the "await" if possible. if self._buffer_size() or await self.fetch_next: return self.next_object() raise StopAsyncIteration() """), globals(), locals()) 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): """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. .. _`gen.coroutine`: http://tornadoweb.org/en/stable/gen.html .. doctest:: fetch_next :hide: >>> _ = MongoClient().test.test_collection.delete_many({}) >>> collection = MotorClient().test.test_collection .. doctest:: fetch_next >>> @gen.coroutine ... def f(): ... yield collection.insert_many([{'_id': i} for i in range(5)]) ... cursor = collection.find().sort([('_id', 1)]) ... while (yield 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 While it appears that fetch_next retrieves each document from the server individually, the cursor actually fetches documents efficiently in `large batches`_. In Python 3.5 and newer, cursors can be iterated elegantly and very efficiently in native coroutines with `async for`: .. doctest:: fetch_next >>> async def f(): ... 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 .. _`large batches`: https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches """ 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): """Get a document from the most recently fetched batch, or ``None``. See :attr:`fetch_next`. """ 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``, :meth:`to_list`, :attr:`fetch_next` 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 >>> >>> @gen.coroutine ... def f(): ... cursor = collection.find().sort([('_id', 1)]) ... docs = yield cursor.to_list(length=2) ... while docs: ... print(docs) ... docs = yield 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() collection = self.collection fix_outgoing = collection.database.delegate._fix_outgoing if length is None: n = result else: n = min(length, result) for _ in range(n): the_list.append(fix_outgoing(self._data().popleft(), collection)) 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() @motor_coroutine def close(self): """Explicitly kill this cursor on the server. Call like (in Tornado): .. code-block:: python yield cursor.close() """ if not self.closed: self.closed = True yield self._framework.yieldable(self._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 _clear_cursor_id(self): raise NotImplementedError def _close_exhaust_cursor(self): raise NotImplementedError def _killed(self): raise NotImplementedError @motor_coroutine def _close(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=cursor_sort_doc) hint = MotorCursorChainingMethod() where = MotorCursorChainingMethod() max_await_time_ms = MotorCursorChainingMethod() max_time_ms = MotorCursorChainingMethod() min = MotorCursorChainingMethod() max = MotorCursorChainingMethod() comment = 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 _clear_cursor_id(self): self.delegate._Cursor__id = 0 def _close_exhaust_cursor(self): # If an exhaust cursor is dying without fully iterating its results, # it must close the socket. PyMongo's Cursor does this, but we've # disabled its cleanup so we must do it ourselves. if self.delegate._Cursor__exhaust: manager = self.delegate._Cursor__exhaust_mgr if manager.sock: manager.sock.close() manager.close() def _killed(self): return self.delegate._Cursor__killed @motor_coroutine def _close(self): yield self._framework.yieldable(self._Cursor__die()) class AgnosticRawBatchCursor(AgnosticCursor): __motor_class_name__ = 'MotorRawBatchCursor' __delegate_class__ = RawBatchCursor class AgnosticCommandCursor(AgnosticBaseCursor): __motor_class_name__ = 'MotorCommandCursor' __delegate_class__ = CommandCursor _CommandCursor__die = AsyncRead() def _query_flags(self): return 0 def _data(self): return self.delegate._CommandCursor__data def _clear_cursor_id(self): self.delegate._CommandCursor__id = 0 def _close_exhaust_cursor(self): # MongoDB doesn't have exhaust command cursors yet. pass def _killed(self): return self.delegate._CommandCursor__killed @motor_coroutine def _close(self): yield self._framework.yieldable(self._CommandCursor__die()) class AgnosticRawBatchCommandCursor(AgnosticCommandCursor): __motor_class_name__ = 'MotorRawBatchCommandCursor' __delegate_class__ = RawBatchCommandCursor class _LatentCursor(object): """Take the place of a PyMongo CommandCursor until aggregate() begins.""" alive = True _CommandCursor__data = [] _CommandCursor__id = None _CommandCursor__killed = False cursor_id = None def _CommandCursor__die(self, *args, **kwargs): pass def clone(self): return _LatentCursor() def rewind(self): pass class AgnosticLatentCommandCursor(AgnosticCommandCursor): __motor_class_name__ = 'MotorLatentCommandCursor' def __init__(self, collection, start, *args, **kwargs): # We're being constructed without yield or 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 yield or await triggers _get_more(), which # will execute the callback "start", which gets a PyMongo CommandCursor. super(self.__class__, self).__init__(_LatentCursor(), 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(self.__class__, self)._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: 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(self.__class__, self)._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') alive = ReadOnlyProperty() 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): super(self.__class__, self).__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} def _lazy_init(self): if not self.delegate: self.delegate = self._target.delegate.watch(**self._kwargs) def _next(self): # This method is run on a thread. try: self._lazy_init() return self.delegate.next() except StopIteration: raise StopAsyncIteration() def _try_next(self): # This method is run on a thread. self._lazy_init() return self.delegate.try_next() @coroutine_annotation 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. """ loop = self.get_io_loop() return self._framework.run_on_executor(loop, self._next) @coroutine_annotation 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 self._framework.run_on_executor(loop, self._try_next) @coroutine_annotation def close(self): """Close this change stream. Stops any "async for" loops using this change stream. """ if self.delegate: return self._close() # Never started. future = self._framework.get_future(self.get_io_loop()) future.set_result(None) return future if PY35: exec(textwrap.dedent(""" def __aiter__(self): return self __anext__ = next async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self.delegate: self.delegate.close() """), globals(), locals()) 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 motor-2.1.0/motor/docstrings.py000066400000000000000000001237071357430361400165630ustar00rootroot00000000000000# 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. from __future__ import unicode_literals 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. .. 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. """ fsync_doc = """Flush all pending writes to datafiles. Optional parameters can be passed as keyword arguments: - `lock`: If True lock the server to disallow writes. - `async`: If True don't block while synchronizing. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. .. note:: Starting with Python 3.7 `async` is a reserved keyword. The async option to the fsync command can be passed using a dictionary instead:: options = {'async': True} await client.fsync(**options) .. versionchanged:: 1.2 Added session parameter. .. warning:: `async` and `lock` can not be used together. .. warning:: MongoDB does not support the `async` option on Windows and will raise an exception on that platform. """ current_op_doc = """ **DEPRECATED**: Get information on operations currently running. Starting with MongoDB 3.6 this helper is obsolete. The functionality provided by this helper is available in MongoDB 3.6+ using the `$currentOp aggregation pipeline stage`_, which can be used with :meth:`aggregate`. Note that, while this helper can only return a single document limited to a 16MB result, :meth:`aggregate` returns a cursor avoiding that limitation. Users of MongoDB versions older than 3.6 can use the `currentOp command`_ directly:: # MongoDB 3.2 and 3.4 await client.admin.command("currentOp") Or query the "inprog" virtual collection:: # MongoDB 2.6 and 3.0 await client.admin["$cmd.sys.inprog"].find_one() :Parameters: - `include_all` (optional): if ``True`` also list currently idle operations in the result - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. .. versionchanged:: 2.1 Deprecated, use :meth:`aggregate` instead. .. versionchanged:: 1.2 Added session parameter. .. _$currentOp aggregation pipeline stage: https://docs.mongodb.com/manual/reference/operator/aggregation/currentOp/ .. _currentOp command: https://docs.mongodb.com/manual/reference/command/currentOp/ """ 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. - `**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:: 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`. :Returns: An instance of :class:`~pymongo.results.BulkWriteResult`. .. seealso:: :ref:`writes-and-ids` .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 1.2 Added session parameter. """ 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`. - `**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 when using MongoDB >= 3.4. .. 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 = yield db.command("buildinfo") For a command where the value matters, like ``{collstats: collection_name}`` we can do:: result = yield db.command("collstats", collection_name) For commands that take additional arguments we can use kwargs. So ``{filemd5: object_id, root: file_root}`` becomes:: result = yield db.command("filemd5", object_id, root=file_root) :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`. - `**kwargs` (optional): additional keyword arguments will be added to the command document before it is sent .. 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`. This option is only supported on MongoDB 3.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. :Returns: - An instance of :class:`~pymongo.results.DeleteResult`. .. 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`. This option is only supported on MongoDB 3.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. :Returns: - An instance of :class:`~pymongo.results.DeleteResult`. .. 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") """ exists_doc = """Check if a file exists. The file to check for can be specified by the value of its ``_id`` key, or by passing in a query document. A query document can be passed in as dictionary, or by using keyword arguments. Thus, the following three calls are equivalent:: await fs.exists(file_id) await fs.exists({"_id": file_id}) await fs.exists(_id=file_id) As are the following two calls:: await fs.exists({"filename": "mike.txt"}) await fs.exists(filename="mike.txt") And the following two:: await fs.exists({"foo": {"$gt": 12}}) await fs.exists(foo={"$gt": 12}) Returns ``True`` if a matching file exists, ``False`` otherwise. Calls to :meth:`exists` will not automatically create appropriate indexes; application developers should be sure to create indexes if needed and as appropriate. :Parameters: - `document_or_id` (optional): query document, or _id of the document to check for - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `**kwargs` (optional): keyword arguments are used as a query document, if they're present. .. versionchanged:: 1.2 Added session parameter. """ 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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `**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:: 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. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `**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:: 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+. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. - `**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:: 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:: 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`. :Returns: An instance of :class:`~pymongo.results.InsertManyResult`. .. seealso:: :ref:`writes-and-ids` .. note:: `bypass_document_validation` requires server version **>= 3.2** .. 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`. :Returns: - An instance of :class:`~pymongo.results.InsertOneResult`. .. seealso:: :ref:`writes-and-ids` .. note:: `bypass_document_validation` requires server version **>= 3.2** .. 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 = yield 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: http://docs.mongodb.org/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`. This option is only supported on MongoDB 3.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. .. note:: `bypass_document_validation` requires server version **>= 3.2** .. versionchanged:: 1.2 Added session parameter. """ update_doc = """Update a document(s) in this collection. **DEPRECATED** - Use :meth:`replace_one`, :meth:`update_one`, or :meth:`update_many` instead.""" 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`. This option is only supported on MongoDB 3.4 and above. - `array_filters` (optional): A list of filters specifying which array elements an update should apply. Requires MongoDB 3.6+. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. .. note:: `bypass_document_validation` requires server version **>= 3.2** .. 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`. This option is only supported on MongoDB 3.4 and above. - `array_filters` (optional): A list of filters specifying which array elements an update should apply. Requires MongoDB 3.6+. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`, created with :meth:`~MotorClient.start_session`. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. .. note:: `bypass_document_validation` requires server version **>= 3.2** .. 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 >>> @gen.coroutine ... def f(): ... cursor = collection.find().sort('_id', pymongo.DESCENDING) ... docs = yield 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 >>> @gen.coroutine ... def f(): ... cursor = collection.find().sort([ ... ('field1', pymongo.ASCENDING), ... ('field2', pymongo.DESCENDING)]) ... ... docs = yield 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 >>> @gen.coroutine ... def f(): ... cursor = collection.find({ ... '$text': {'$search': 'some words'}}, ... {'score': {'$meta': 'textScore'}}) ... ... # Sort by 'score' field. ... cursor.sort([('score', {'$meta': 'textScore'})]) ... docs = yield cursor.to_list(None) ... for doc in docs: ... 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 """ with_transaction_doc = """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 possiblity 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:: 3.9 """ motor-2.1.0/motor/frameworks/000077500000000000000000000000001357430361400162005ustar00rootroot00000000000000motor-2.1.0/motor/frameworks/__init__.py000066400000000000000000000000001357430361400202770ustar00rootroot00000000000000motor-2.1.0/motor/frameworks/asyncio/000077500000000000000000000000001357430361400176455ustar00rootroot00000000000000motor-2.1.0/motor/frameworks/asyncio/__init__.py000066400000000000000000000114051357430361400217570ustar00rootroot00000000000000# 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 from asyncio import coroutine # For framework interface. from concurrent.futures import ThreadPoolExecutor CLASS_PREFIX = 'AsyncIO' def get_event_loop(): return asyncio.get_event_loop() 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 asyncio.Future(loop=loop) 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 run_on_executor(loop, fn, *args, **kwargs): # Adapted from asyncio's wrap_future and _chain_future. Ensure the wrapped # future is resolved on the main thread when the executor's future is # resolved on a worker thread. asyncio's wrap_future does the same, but # throws an error if the loop is stopped. We want to avoid errors if a # background task completes after the loop stops, e.g. ChangeStream.next() # returns while the program is shutting down. def _set_state(): if dest.cancelled(): return if source.cancelled(): dest.cancel() else: exception = source.exception() if exception is not None: dest.set_exception(exception) else: result = source.result() dest.set_result(result) def _call_check_cancel(_): if dest.cancelled(): source.cancel() def _call_set_state(_): if loop.is_closed(): return loop.call_soon_threadsafe(_set_state) source = _EXECUTOR.submit(functools.partial(fn, *args, **kwargs)) dest = asyncio.Future(loop=loop) dest.add_done_callback(_call_check_cancel) source.add_done_callback(_call_set_state) return dest # 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 = asyncio.Future(loop=loop) def copy(_future): if _future.exception() is not None: chained.set_exception(_future.exception()) else: chained.set_result(return_value) future._future.add_done_callback(functools.partial(loop.add_callback, copy)) return chained def is_future(f): return isinstance(f, asyncio.Future) 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) @asyncio.coroutine def _wrapper(self, *args, **kwargs): result = yield from f(self, *args, **kwargs) # Don't call isinstance(), not checking subclasses. if result.__class__ == pymongo_class: # Delegate to the current object to wrap the result. return self.wrap(result) else: return result return _wrapper def yieldable(future): return next(iter(future)) def platform_info(): return None motor-2.1.0/motor/frameworks/tornado/000077500000000000000000000000001357430361400176465ustar00rootroot00000000000000motor-2.1.0/motor/frameworks/tornado/__init__.py000066400000000000000000000075261357430361400217710ustar00rootroot00000000000000# 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. from __future__ import absolute_import, unicode_literals """Tornado compatibility layer for Motor, an asynchronous MongoDB driver. See "Frameworks" in the Developer Guide. """ import functools import os from concurrent.futures import ThreadPoolExecutor import tornado.process from tornado import concurrent, gen, ioloop, version as tornado_version from tornado.gen import chain_future, coroutine # For framework interface. 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 run_on_executor(loop, fn, *args, **kwargs): # Need a Tornado Future for "await" expressions. exec_fut is resolved on a # worker thread, loop.add_future ensures "future" is resolved on main. future = concurrent.Future() exec_fut = _EXECUTOR.submit(fn, *args, **kwargs) def copy(_): if future.done(): return if exec_fut.exception() is not None: future.set_exception(exec_fut.exception()) else: future.set_result(exec_fut.result()) # Ensure copy runs on main thread. loop.add_future(exec_fut, copy) return future 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): 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) @coroutine def _wrapper(self, *args, **kwargs): result = yield f(self, *args, **kwargs) # Don't call isinstance(), not checking subclasses. if result.__class__ == pymongo_class: # Delegate to the current object to wrap the result. raise gen.Return(self.wrap(result)) else: raise gen.Return(result) return _wrapper def yieldable(future): return future def platform_info(): return 'Tornado %s' % (tornado_version,) motor-2.1.0/motor/metaprogramming.py000066400000000000000000000257651357430361400176020ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import """Dynamic class-creation for Motor.""" import inspect import functools from pymongo.cursor import Cursor _class_cache = {} def asynchronize( framework, sync_method, doc=None, wrap_class=None, unwrap_class=None): """Decorate `sync_method` so it returns a Future. The method runs on a thread and resolves the Future when it completes. :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 motor_coroutine(f): """Used by Motor classes to mark functions as coroutines. create_class_with_framework will decorate the function with a framework- specific coroutine decorator, like asyncio.coroutine or Tornado's gen.coroutine. You cannot return a value from a motor_coroutine, the syntax differences between Tornado on Python 2 and asyncio with Python 3.5 are impossible to bridge. """ f._is_motor_coroutine = _coro_token return f def coroutine_annotation(f): """In docs, annotate a function that returns a Future with 'coroutine'. Unlike @motor_coroutine, this doesn't affect behavior. """ # Like: # @coroutine_annotation # def method(self): # f.coroutine_annotation = True return f class MotorAttributeFactory(object): """Used by Motor classes to mark attributes that delegate in some way to PyMongo. At module import time, create_class_with_framework calls create_attribute() for each attr to create the final class attribute. """ def __init__(self, doc=None): self.doc = doc 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(Async, self).__init__(doc) self.attr_name = attr_name self.wrap_class = None self.unwrap_class = None def create_attribute(self, cls, attr_name): name = self.attr_name or attr_name method = getattr(cls.__delegate_class__, name) return asynchronize(framework=cls._framework, sync_method=method, doc=self.doc, wrap_class=self.wrap_class, unwrap_class=self.unwrap_class) 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() 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 def create_class_with_framework(cls, framework, module_name): motor_class_name = framework.CLASS_PREFIX + cls.__motor_class_name__ cache_key = (cls, motor_class_name, framework) cached_class = _class_cache.get(cache_key) if cached_class: return cached_class # Remove the special __dict__ proxy and let type() create it instead, # otherwise attempting to access cls().__dict__ will raise a TypeError: # TypeError: descriptor '__dict__' for 'AgnosticGridIn' objects doesn't # apply to 'MotorGridIn' object. cls_dict = dict(cls.__dict__) cls_dict.pop('__dict__', None) new_class = type(str(motor_class_name), cls.__bases__, cls_dict) 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) elif getattr(attr, '_is_motor_coroutine', None) is _coro_token: coro = framework.coroutine(attr) del coro._is_motor_coroutine coro.coroutine_annotation = True setattr(new_class, name, coro) _class_cache[cache_key] = new_class return new_class motor-2.1.0/motor/motor_asyncio.py000066400000000000000000000036401357430361400172620ustar00rootroot00000000000000# 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 create_class_with_framework __all__ = ['AsyncIOMotorClient'] def create_asyncio_class(cls): 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) motor-2.1.0/motor/motor_common.py000066400000000000000000000013601357430361400171020ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import """Common code to support all async frameworks.""" callback_type_error = TypeError("callback must be a callable") motor-2.1.0/motor/motor_gridfs.py000066400000000000000000000511121357430361400170700ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import """GridFS implementation for Motor, an asynchronous driver for MongoDB.""" import textwrap import warnings import gridfs import pymongo import pymongo.errors from gridfs import (DEFAULT_CHUNK_SIZE, grid_file) from motor.core import (AgnosticBaseCursor, AgnosticCollection, AgnosticDatabase, PY35) from motor.docstrings import * from motor.metaprogramming import (AsyncCommand, AsyncRead, coroutine_annotation, create_class_with_framework, DelegateMethod, motor_coroutine, MotorCursorChainingMethod, ReadOnlyProperty) class AgnosticGridOutCursor(AgnosticBaseCursor): __motor_class_name__ = 'MotorGridOutCursor' __delegate_class__ = gridfs.GridOutCursor add_option = MotorCursorChainingMethod() address = ReadOnlyProperty() collation = ReadOnlyProperty() comment = MotorCursorChainingMethod() distinct = AsyncRead() explain = AsyncRead() hint = MotorCursorChainingMethod() limit = MotorCursorChainingMethod() max = MotorCursorChainingMethod() max_await_time_ms = MotorCursorChainingMethod() max_scan = MotorCursorChainingMethod() max_time_ms = MotorCursorChainingMethod() min = MotorCursorChainingMethod() remove_option = MotorCursorChainingMethod() skip = MotorCursorChainingMethod() sort = MotorCursorChainingMethod(doc=cursor_sort_doc) where = MotorCursorChainingMethod() # PyMongo's GridOutCursor inherits __die from Cursor. _Cursor__die = AsyncCommand() def clone(self): """Get a clone of this cursor.""" return self.__class__(self.delegate.clone(), self.collection) def next_object(self): """Get next GridOut object from cursor.""" grid_out = super(self.__class__, self).next_object() if grid_out: grid_out_class = create_class_with_framework( AgnosticGridOut, self._framework, self.__module__) return grid_out_class(self.collection, delegate=grid_out) else: # Exhausted. return None def rewind(self): """Rewind this cursor to its unevaluated state.""" self.delegate.rewind() self.started = False return self def _empty(self): return self.delegate._Cursor__empty def _query_flags(self): return self.delegate._Cursor__query_flags def _data(self): return self.delegate._Cursor__data def _clear_cursor_id(self): self.delegate._Cursor__id = 0 def _close_exhaust_cursor(self): # Exhaust MotorGridOutCursors are prohibited. pass def _killed(self): return self.delegate._Cursor__killed @motor_coroutine def _close(self): yield self._framework.yieldable(self._Cursor__die()) class MotorGridOutProperty(ReadOnlyProperty): """Creates a readonly attribute on the wrapped PyMongo GridOut.""" def create_attribute(self, cls, attr_name): def fget(obj): if not obj.delegate._file: raise pymongo.errors.InvalidOperation( "You must call MotorGridOut.open() before accessing " "the %s property" % attr_name) return getattr(obj.delegate, attr_name) doc = getattr(cls.__delegate_class__, attr_name).__doc__ return property(fget=fget, doc=doc) class AgnosticGridOut(object): """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() md5 = MotorGridOutProperty() metadata = MotorGridOutProperty() name = MotorGridOutProperty() read = AsyncRead() readable = DelegateMethod() readchunk = AsyncRead() readline = AsyncRead() seek = DelegateMethod() seekable = DelegateMethod() tell = DelegateMethod() upload_date = MotorGridOutProperty() write = DelegateMethod() def __init__(self, root_collection, file_id=None, file_document=None, delegate=None, session=None): 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() # python.org/dev/peps/pep-0492/#api-design-and-implementation-revisions if PY35: exec(textwrap.dedent(""" def __aiter__(self): return self async def __anext__(self): chunk = await self.readchunk() if chunk: return chunk raise StopAsyncIteration() """), globals(), locals()) def __getattr__(self, item): if not self.delegate._file: raise pymongo.errors.InvalidOperation( "You must call MotorGridOut.open() before accessing " "the %s property" % item) 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 @motor_coroutine def stream_to_handler(self, request_handler): """Write the contents of this file to a :class:`tornado.web.RequestHandler`. This method calls :meth:`~tornado.web.RequestHandler.flush` on 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 = yield motor.MotorGridFSBucket(db()) try: gridout = yield fs.open_download_stream_by_name(filename) except gridfs.NoFile: raise tornado.web.HTTPError(404) self.set_header("Content-Type", gridout.content_type) self.set_header("Content-Length", gridout.length) yield gridout.stream_to_handler(self) self.finish() .. seealso:: Tornado `RequestHandler `_ """ written = 0 while written < self.length: # Reading chunk_size at a time minimizes buffering. f = self._framework.yieldable(self.read(self.chunk_size)) yield f chunk = f.result() # write() simply appends the output to a list; flush() sends it # over the network and minimizes buffering in the handler. request_handler.write(chunk) request_handler.flush() written += len(chunk) class AgnosticGridIn(object): __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() md5 = ReadOnlyProperty() name = ReadOnlyProperty() read = DelegateMethod() readable = DelegateMethod() seekable = DelegateMethod() upload_date = ReadOnlyProperty() write = AsyncCommand().unwrap('MotorGridOut') writeable = DelegateMethod() writelines = AsyncCommand().unwrap('MotorGridOut') set = AsyncCommand(attr_name='__setattr__', doc=""" Set an arbitrary metadata attribute on the file. Stores value on the server as a key-value pair within the file document once the file is closed. If the file is already closed, calling :meth:`set` will immediately update the file 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, disable_md5=False, **kwargs): """ Class to write data to GridFS. Application developers should not generally need to instantiate this class - see :meth:`~motor.MotorGridFSBucket.open_upload_stream`. Any of the file level options specified in the `GridFS Spec `_ 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 - `disable_md5` (optional): When True, an MD5 checksum will not be computed for the uploaded file. Useful in environments where MD5 cannot be used for regulatory or other reasons. Defaults to False. - `**kwargs` (optional): file level options (see above) .. versionchanged:: 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() if delegate: # Short cut. self.delegate = delegate else: self.delegate = self.__delegate_class__( root_collection.delegate, session=session, disable_md5=disable_md5, **kwargs) if PY35: # Support "async with bucket.open_upload_stream() as f:" exec(textwrap.dedent(""" async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() """), globals(), locals()) def get_io_loop(self): return self.io_loop class AgnosticGridFSBucket(object): __motor_class_name__ = 'MotorGridFSBucket' __delegate_class__ = gridfs.GridFSBucket delete = AsyncCommand() download_to_stream = AsyncCommand() download_to_stream_by_name = AsyncCommand() open_download_stream = AsyncCommand().wrap(gridfs.GridOut) open_download_stream_by_name = AsyncCommand().wrap(gridfs.GridOut) open_upload_stream = DelegateMethod().wrap(gridfs.GridIn) open_upload_stream_with_id = DelegateMethod().wrap(gridfs.GridIn) rename = AsyncCommand() upload_from_stream = AsyncCommand() upload_from_stream_with_id = AsyncCommand() def __init__(self, database, bucket_name="fs", disable_md5=False, chunk_size_bytes=DEFAULT_CHUNK_SIZE, write_concern=None, read_preference=None, collection=None): """Create a handle to a GridFS bucket. Raises :exc:`~pymongo.errors.ConfigurationError` if `write_concern` 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. - `disable_md5` (optional): When True, MD5 checksums will not be computed for uploaded files. Useful in environments where MD5 cannot be used for regulatory or other reasons. Defaults to False. - `collection` (optional): Deprecated, an alias for `bucket_name` that exists solely to provide backwards compatibility. .. versionchanged:: 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( "First argument to %s must be MotorDatabase, not %r" % ( self.__class__, database)) self.io_loop = database.get_io_loop() self.collection = database.get_collection( bucket_name, write_concern=write_concern, read_preference=read_preference) self.delegate = self.__delegate_class__( database.delegate, bucket_name, chunk_size_bytes=chunk_size_bytes, write_concern=write_concern, read_preference=read_preference, disable_md5=disable_md5) def get_io_loop(self): return self.io_loop 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 (yield cursor.fetch_next): grid_out = cursor.next_object() data = yield grid_out.read() This iterates through all versions of "lisa.txt" stored in GridFS. Note that setting no_cursor_timeout 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) motor-2.1.0/motor/motor_py2_compat.py000066400000000000000000000020721357430361400176700ustar00rootroot00000000000000# 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 __future__ import unicode_literals, absolute_import """Python 2/3 compatibility utilities for Motor.""" import sys PY3 = False if sys.version_info[0] >= 3: PY3 = True if PY3: string_types = str, integer_types = int, text_type = str from io import BytesIO as StringIO else: string_types = basestring, integer_types = (int, long) text_type = unicode try: from cStringIO import StringIO except ImportError: from StringIO import StringIO motor-2.1.0/motor/motor_tornado.py000066400000000000000000000034631357430361400172660ustar00rootroot00000000000000# 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 __future__ import unicode_literals, absolute_import from . import core, motor_gridfs from .frameworks import tornado as tornado_framework from .metaprogramming import create_class_with_framework __all__ = ['MotorClient'] def create_motor_class(cls): 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) motor-2.1.0/motor/web.py000066400000000000000000000154421357430361400151550ustar00rootroot00000000000000# 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. from __future__ import unicode_literals """Utilities for using Motor with Tornado web applications.""" import datetime import email.utils import mimetypes import time import tornado.web from tornado import gen import gridfs import motor # TODO: this class is not a drop-in replacement for StaticFileHandler. # StaticFileHandler provides class method make_static_url, which appends # an MD5 of the static file's contents. Templates thus can do # {{ static_url('image.png') }} and get "/static/image.png?v=1234abcdef", # which is cached forever. Problem is, it calculates the MD5 synchronously. # Two options: keep a synchronous GridFS available to get each grid file's # MD5 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) @gen.coroutine def get(self, path, include_body=True): fs = motor.MotorGridFSBucket(self.database, self.root_collection) try: gridout = yield self.get_gridfs_file(fs, path, self.request) except gridfs.NoFile: raise tornado.web.HTTPError(404) # If-Modified-Since header is only good to the second. modified = gridout.upload_date.replace(microsecond=0) self.set_header("Last-Modified", modified) # MD5 is calculated on the MongoDB server when GridFS file is created self.set_header("Etag", '"%s"' % gridout.md5) 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.utcnow() + 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('"') == gridout.md5: self.set_status(304) return self.set_header("Content-Length", gridout.length) if include_body: yield 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""" pass motor-2.1.0/setup.py000066400000000000000000000145571357430361400144060ustar00rootroot00000000000000import sys from distutils.cmd import Command from distutils.errors import DistutilsOptionError try: from setuptools import setup except ImportError: from ez_setup import use_setuptools use_setuptools('28.0') from setuptools import setup classifiers = """\ Intended Audience :: Developers License :: OSI Approved :: Apache Software License Development Status :: 5 - Production/Stable Natural Language :: English Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Operating System :: MacOS :: MacOS X Operating System :: Unix Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy """ description = 'Non-blocking MongoDB driver for Tornado or asyncio' long_description = open("README.rst").read() install_requires = ['pymongo>=3.10,<4'] tests_require = ['mockupdb>=1.4.0'] if sys.version_info[0] < 3: install_requires.append('futures') class test(Command): description = "run the tests" user_options = [ ("test-module=", "m", "Discover tests in specified module"), ("test-suite=", "s", "Test suite to run (e.g. 'some_module.test_suite')"), ("failfast", "f", "Stop running tests on first failure or error"), ("tornado-warnings", "w", "Let Tornado log warnings"), ("xunit-output=", "x", "Generate a results directory with XUnit XML format")] def initialize_options(self): self.test_module = None self.test_suite = None self.failfast = False self.tornado_warnings = False self.xunit_output = None def finalize_options(self): if self.test_suite is None and self.test_module is None: self.test_module = 'test' elif self.test_module is not None and self.test_suite is not None: raise DistutilsOptionError( "You may specify a module or suite, but not both") def run(self): # Installing required packages, running egg_info and build_ext are # part of normal operation for setuptools.command.test.test. Motor # has no extensions so build_ext is a no-op. if self.distribution.install_requires: self.distribution.fetch_build_eggs( self.distribution.install_requires) if self.distribution.tests_require: self.distribution.fetch_build_eggs(self.distribution.tests_require) if self.xunit_output: self.distribution.fetch_build_eggs( ["unittest-xml-reporting>=1.14.0,<2.0.0a0"]) self.run_command('egg_info') build_ext_cmd = self.reinitialize_command('build_ext') build_ext_cmd.inplace = 1 self.run_command('build_ext') from test import (env, suppress_tornado_warnings, MotorTestLoader, test_environment as testenv) loader = MotorTestLoader() loader.avoid('high_availability', reason='Runs separately') if not (testenv.HAVE_ASYNCIO or testenv.HAVE_TORNADO): raise ImportError("No tornado nor asyncio") elif not testenv.HAVE_TORNADO: loader.avoid('tornado_tests', reason='no tornado') elif not testenv.HAVE_ASYNCIO: loader.avoid('asyncio_tests', reason='no asyncio') if not testenv.HAVE_AIOHTTP: loader.avoid('asyncio_tests.test_aiohttp_gridfs', reason='no aiohttp') if sys.version_info[:2] < (3, 5): loader.avoid('asyncio_tests.test_asyncio_await', 'asyncio_tests.test_asyncio_change_stream', 'asyncio_tests.test_examples', 'tornado_tests.test_motor_transaction', reason='python < 3.5') # Decide if we can run async / await tests with Tornado. test_motor_await = 'tornado_tests.test_motor_await' if not testenv.HAVE_TORNADO: loader.avoid(test_motor_await, reason='no tornado') elif sys.version_info[:2] < (3, 5): loader.avoid(test_motor_await, reason='python < 3.5') loader.avoid('tornado_tests.test_motor_change_stream', reason='python < 3.5') if self.test_suite is None: suite = loader.discover(self.test_module) else: suite = loader.loadTestsFromName(self.test_suite) runner_kwargs = dict(verbosity=2, failfast=self.failfast) if self.xunit_output: runner_kwargs['output'] = self.xunit_output from xmlrunner import XMLTestRunner runner_class = XMLTestRunner else: import unittest runner_class = unittest.TextTestRunner runner = runner_class(**runner_kwargs) env.setup() if not self.tornado_warnings: suppress_tornado_warnings() result = runner.run(suite) sys.exit(not result.wasSuccessful()) packages = ['motor', 'motor.frameworks', 'motor.frameworks.tornado'] if sys.version_info[0] >= 3: # Trying to install and byte-compile motor/frameworks/asyncio/__init__.py # causes SyntaxError in Python 2. packages.append('motor.frameworks.asyncio') # Install aiohttp integration - aiohttp itself need not be installed. packages.append('motor.aiohttp') setup(name='motor', version='2.1.0', packages=packages, description=description, long_description=long_description, author='A. Jesse Jiryu Davis', author_email='jesse@mongodb.com', url='https://github.com/mongodb/motor/', python_requires=', '.join(( '>=2.7', '!=3.0.*', '!=3.1.*', '!=3.2.*', '!=3.3.*', '!=3.5.0', '!=3.5.1', )), install_requires=install_requires, license='http://www.apache.org/licenses/LICENSE-2.0', classifiers=[c for c in classifiers.split('\n') if c], keywords=[ "mongo", "mongodb", "pymongo", "gridfs", "bson", "motor", "tornado", "asyncio", ], tests_require=tests_require, test_suite='test', zip_safe=False, cmdclass={'test': test}) motor-2.1.0/synchro/000077500000000000000000000000001357430361400143455ustar00rootroot00000000000000motor-2.1.0/synchro/__init__.py000066400000000000000000000636351357430361400164730ustar00rootroot00000000000000# 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. from __future__ import unicode_literals """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 inspect import unittest from tornado.ioloop import IOLoop import motor import motor.frameworks.tornado import motor.motor_tornado from motor.core import _MotorTransactionContext from motor.metaprogramming import MotorAttributeFactory # Make e.g. "from pymongo.errors import AutoReconnect" work. Note that # importing * won't pick up underscore-prefixed attrs. from gridfs import * from gridfs.errors import * from gridfs.grid_file import (DEFAULT_CHUNK_SIZE, _SEEK_CUR, _SEEK_END, _clear_entity_type_registry) from pymongo import * from pymongo import (collation, compression_support, change_stream, errors, monotonic, operations, server_selectors, server_type, son_manipulator, saslprep, ssl_match_hostname, ssl_support, write_concern) from pymongo.auth import _build_credentials_tuple from pymongo.helpers import _check_command_response from pymongo.change_stream import _NON_RESUMABLE_GETMORE_ERRORS from pymongo.client_session import TransactionOptions from pymongo.collation import * from pymongo.common import * from pymongo.common import _UUID_REPRESENTATIONS, _MAX_END_SESSIONS 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_options import * from pymongo.encryption_options import _HAVE_PYMONGOCRYPT from pymongo.errors import * 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, _Listeners, _SENSITIVE_COMMANDS from pymongo.monotonic import time from pymongo.operations import * from pymongo.pool import * from pymongo.pool import _METADATA, _PoolClosedError from pymongo.periodic_executor import * from pymongo.periodic_executor import _EXECUTORS 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.server import * from pymongo.server_selectors import * from pymongo.settings import * from pymongo.saslprep import * from pymongo.ssl_support import * from pymongo.son_manipulator 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 pymongo import auth from pymongo.auth import * from pymongo.auth import _password_digest from pymongo import GEOSPHERE, HASHED from pymongo.pool import SocketInfo, Pool # Internal classes not declared in motor_tornado.py: retrieve from class cache. from motor.core import ( AgnosticRawBatchCursor as _AgnosticRawBatchCursor, AgnosticRawBatchCommandCursor as _AgnosticRawBatchCommandCursor) 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 map_reduce() or 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): return CommandCursor(motor_obj) 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()]) return fn(*args, **kwargs) return _unwrap_synchro class SynchroAttr(object): # 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)): if attr.name is None: attr.name = name return new_class def with_metaclass(metaclass, *bases): """Python 2/3 compatible metaclass helper.""" class _metaclass(metaclass): def __new__(mcls, name, _bases, attrs): return metaclass(name, bases, attrs) return type.__new__(_metaclass, str('dummy'), (), {}) class Synchro(with_metaclass(SynchroMeta)): """ Wraps a MotorClient, MotorDatabase, MotorCollection, etc. and makes it act like the synchronous pymongo equivalent """ __metaclass__ = SynchroMeta __delegate_class__ = None def __cmp__(self, other): """Implements == and != on Python 2.""" return cmp(self.delegate, other.delegate) def __eq__(self, other): """Implements == and != on Python 3.""" if (isinstance(other, self.__class__) and hasattr(self, 'delegate') and hasattr(other, 'delegate')): return self.delegate == other.delegate return NotImplemented 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) return IOLoop.current().run_sync(partial) return synchronized_method class MongoClient(Synchro): __delegate_class__ = motor.MotorClient HOST = 'localhost' PORT = 27017 get_database = WrapOutgoing() max_pool_size = SynchroProperty() max_write_batch_size = SynchroProperty() start_session = Sync() watch = WrapOutgoing() 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) @property def is_locked(self): # MotorClient doesn't support the is_locked property. # Synchro has already synchronized current_op; use it. result = self.admin.current_op() return bool(result.get('fsyncLock', None)) 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__options = SynchroProperty() _cache_credentials = SynchroProperty() _close_cursor_now = SynchroProperty() _get_topology = SynchroProperty() _topology = SynchroProperty() _kill_cursors_executor = 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) # 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() def __init__(self, client, name, **kwargs): assert isinstance(client, MongoClient), ( "Expected MongoClient, got %s" % repr(client)) self._client = client self.delegate = kwargs.get('delegate') or motor.MotorDatabase( client.delegate, name, **kwargs) assert isinstance(self.delegate, motor.MotorDatabase), ( "synchro.Database delegate must be MotorDatabase, not " " %s" % repr(self.delegate)) def aggregate(self, *args, **kwargs): # Motor does no I/O initially in aggregate() but PyMongo does. func = wrap_synchro(unwrap_synchro(self.delegate.aggregate)) cursor = func(*args, **kwargs) self.synchronize(cursor.delegate._get_more)() return cursor @property def client(self): return self._client 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() list_indexes = WrapOutgoing() watch = 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') or 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 aggregate(self, *args, **kwargs): # Motor does no I/O initially in aggregate() but PyMongo does. func = wrap_synchro(unwrap_synchro(self.delegate.aggregate)) cursor = func(*args, **kwargs) self.synchronize(cursor.delegate._get_more)() return cursor def aggregate_raw_batches(self, *args, **kwargs): # Motor does no I/O initially in aggregate() but PyMongo does. func = wrap_synchro(unwrap_synchro(self.delegate.aggregate_raw_batches)) cursor = func(*args, **kwargs) self.synchronize(cursor.delegate._get_more)() return cursor 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]) def count(self, *args, **kwargs): raise unittest.SkipTest('count() is not supported in Motor') def group(self, *args, **kwargs): raise unittest.SkipTest('group() is not supported in Motor') 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') 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): cursor = self.delegate if cursor._buffer_size(): return cursor.next_object() elif cursor.alive: self.synchronize(cursor._get_more)() if cursor._buffer_size(): return cursor.next_object() raise StopIteration __next__ = next def __getitem__(self, index): if isinstance(index, slice): return Cursor(self.delegate[index]) else: to_list = self.synchronize(self.delegate[index].to_list) return to_list(length=10000)[0] @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 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(GridOutCursor, self).__init__(delegate) def next(self): motor_grid_out = super(GridOutCursor, self).next() if motor_grid_out: return GridOut(self.collection, delegate=motor_grid_out) __next__ = next class CursorManager(object): # Motor doesn't support cursor managers, just avoid ImportError. pass class BulkOperationBuilder(object): 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 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): self.close() class SynchroGridOutProperty(object): 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') md5 = SynchroGridOutProperty('md5') 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(GridOut, self).__setattr__(key, value) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __next__(self): if sys.version_info >= (3, 5): try: return self.synchronize(self.delegate.__anext__)() except StopAsyncIteration: raise StopIteration() else: chunk = self.readchunk() if chunk: return chunk raise StopIteration() def __iter__(self): return self motor-2.1.0/synchro/synchrotest.py000066400000000000000000000347171357430361400173200ustar00rootroot00000000000000# 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. from __future__ import unicode_literals """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 sys import nose from nose.config import Config from nose.plugins import Plugin from nose.plugins.manager import PluginManager from nose.plugins.skip import Skip from nose.plugins.xunit import Xunit from nose.selector import Selector import synchro from motor.motor_py2_compat import PY3 excluded_modules = [ # Exclude some PyMongo tests that can't be applied to Synchro. 'test.test_cursor_manager', 'test.test_examples', 'test.test_threads', 'test.test_pooling', 'test.test_legacy_api', 'test.test_monotonic', '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', 'test.test_son_manipulator', ] if sys.version_info[:2] < (3, 5): excluded_modules.extend([ # Motor's change streams need Python 3.5. 'test.test_change_stream', ]) 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', 'TestCursor.test_concurrent_close', # 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', 'TestCollection.test_reindex', # Motor doesn't support PyMongo's syntax, db.system_js['my_func'] = "code", # users should just use system.js as a regular collection. 'TestDatabase.test_system_js', 'TestDatabase.test_system_js_list', # Requires indexing / slicing cursors, which Motor doesn't do, see MOTOR-84. 'TestCollection.test_min_query', 'TestCursor.test_clone', 'TestCursor.test_count_with_limit_and_skip', '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.*', 'TestReplicaSetClientInternalIPs.*', 'TestReplicaSetClientMaxWriteBatchSize.*', 'TestSSL.test_system_certs_config_error', # Motor is correct here, it's just unreliable on slow CI servers. 'TestReplicaSetClient.test_timeout_does_not_mark_member_down', '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', 'TestCollection.test_aggregation_cursor', 'TestCommandAndReadPreference.*', 'TestCommandMonitoring.test_get_more_failure', 'TestCommandMonitoring.test_sensitive_commands', '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', 'TestMaxStaleness.test_last_write_date_absent', 'TestReplicaSetClient.test_kill_cursor_explicit_primary', 'TestReplicaSetClient.test_kill_cursor_explicit_secondary', 'TestSelections.test_bool', # Deprecated in PyMongo, removed in Motor 2.0. 'TestDatabase.test_collection_names', 'TestDatabase.test_errors', 'TestDatabase.test_eval', 'TestCollation.*', 'TestCollection.test_find_one_and_write_concern', 'TestCollection.test_parallel_scan', 'TestCollection.test_parallel_scan_max_time_ms', 'TestCollection.test_write_error_text_handling', 'TestCommandMonitoring.test_legacy_insert_many', 'TestCommandMonitoring.test_legacy_writes', 'TestClient.test_database_names', 'TestClient.test_is_locked_does_not_raise_warning', 'TestCollectionWCustomType.test_find_and_modify_w_custom_type_decoder', # Tests that use "count", deprecated in PyMongo, removed in Motor 2.0. '*.test_command_monitoring_command_A_failed_command_event', '*.test_command_monitoring_command_A_successful_command', '*.test_command_monitoring_command_A_successful_command_with_a_non-primary_read_preference', '*.test_read_count_Deprecated_count_with_a_filter', '*.test_read_count_Deprecated_count_without_a_filter', 'TestBinary.test_uuid_queries', 'TestCollection.test_count', 'TestCursor.test_comment', 'TestCursor.test_count', 'TestCursor.test_count_with_fields', 'TestCursor.test_count_with_hint', 'TestCursor.test_where', 'TestGridfs.test_gridfs_find', # Tests that use "authenticate" or "logoout", removed in Motor 2.0. 'TestSASLPlain.test_sasl_plain_bad_credentials', 'TestSCRAM.test_scram', 'TestSCRAMSHA1.test_scram_sha1', 'TestThreadedAuth.*', # Uses "collection_names", deprecated in PyMongo, removed in Motor 2.0. 'TestSingleSlaveOk.test_reads_from_secondary', # Slow. 'TestDatabase.test_collection_names_single_socket', '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-280 'TestTransactionsConvenientAPI.*', ] if sys.version_info[:2] >= (3, 5): excluded_tests.extend([ # 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', ]) excluded_modules_matched = set() excluded_tests_matched = set() class SynchroNosePlugin(Plugin): name = 'synchro' def __init__(self, *args, **kwargs): # We need a standard Nose selector in order to filter out methods that # don't match TestSuite.test_* self.selector = Selector(config=None) super(SynchroNosePlugin, self).__init__(*args, **kwargs) def configure(self, options, conf): super(SynchroNosePlugin, self).configure(options, conf) self.enabled = True def wantModule(self, 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 wantFunction(self, fn): # PyMongo's test generators run at import time; tell Nose not to run # them as unittests. if fn.__name__ in ('test_cases', 'create_test', 'create_tests', 'create_connection_string_test', 'create_document_test', 'create_selection_tests', ): return False def wantClass(self, cls): # PyMongo's test generator classes run at import time; tell Nose not # to run them as unittests. if cls.__name__ in ('TestCreator',): return False def wantMethod(self, method): # Run standard Nose checks on name, like "does it start with test_"? if not self.selector.matches(method.__name__): return False if method.__name__ in ('run_test_ops',): return False for excluded_name in excluded_tests: if PY3: classname = method.__self__.__class__.__name__ else: classname = method.im_class.__name__ # Should we exclude this method's whole TestCase? suite_name, method_name = excluded_name.split('.') suite_matches = (suite_name == classname or suite_name == '*') # Should we exclude this particular method? method_matches = ( method.__name__ == method_name or method_name == '*') if suite_matches and method_matches: excluded_tests_matched.add(excluded_name) return False return True if sys.version_info[0] < 3: # So that e.g. 'from pymongo.mongo_client import MongoClient' gets the # Synchro MongoClient, not the real one. class SynchroModuleFinder(object): def find_module(self, fullname, path=None): parts = fullname.split('.') if parts[-1] in ('gridfs', 'pymongo'): # E.g. "import pymongo" return SynchroModuleLoader(path) elif len(parts) >= 2 and parts[-2] in ('gridfs', 'pymongo'): # E.g. "import pymongo.mongo_client" return SynchroModuleLoader(path) # Let regular module search continue. return None class SynchroModuleLoader(object): def __init__(self, path): self.path = path def load_module(self, fullname): return synchro else: import importlib import importlib.abc import importlib.machinery 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 if __name__ == '__main__': 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 # 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.mongo_client', 'pymongo.database', 'pymongo.mongo_replica_set_client', 'gridfs', 'gridfs.grid_file', 'pymongo.encryption']: sys.modules.pop(n) if '--check-exclude-patterns' in sys.argv: check_exclude_patterns = True sys.argv.remove('--check-exclude-patterns') else: check_exclude_patterns = False success = nose.run( config=Config(plugins=PluginManager()), addplugins=[SynchroNosePlugin(), Skip(), Xunit()]) if not success: sys.exit(1) if check_exclude_patterns: unused_module_pats = set(excluded_modules) - excluded_modules_matched assert not unused_module_pats, "Unused module patterns: %s" % ( unused_module_pats, ) unused_test_pats = set(excluded_tests) - excluded_tests_matched assert not unused_test_pats, "Unused test patterns: %s" % ( unused_test_pats, ) motor-2.1.0/test/000077500000000000000000000000001357430361400136375ustar00rootroot00000000000000motor-2.1.0/test/__init__.py000066400000000000000000000052121357430361400157500ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import logging import unittest from unittest import SkipTest from test.test_environment import env, db_user, CLIENT_PEM 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 def suppress_tornado_warnings(): for name in [ 'tornado.general', 'tornado.access']: logger = logging.getLogger(name) logger.setLevel(logging.ERROR) class SkippedModule(object): def __init__(self, name, reason): def runTest(self): raise SkipTest(str(reason)) self.test_case = type( str(name), (unittest.TestCase, ), {'runTest': runTest}) class MotorTestLoader(unittest.TestLoader): def __init__(self, avoid=None, reason=None): super(MotorTestLoader, self).__init__() self._avoid = [] def avoid(self, *prefixes, **kwargs): """Skip a module. The usual "raise SkipTest" from a module doesn't work if the module won't even parse in Python 2, so prevent TestLoader from importing modules with the given prefix. "prefix" is a path prefix like "asyncio_tests". """ for prefix in prefixes: self._avoid.append((prefix, kwargs['reason'])) def _get_module_from_name(self, name): for prefix, reason in self._avoid: if name.startswith(prefix): return SkippedModule(name, reason) return super(MotorTestLoader, self)._get_module_from_name(name) class MockRequestHandler(object): """For testing MotorGridOut.stream_to_handler.""" def __init__(self): self.n_written = 0 def write(self, data): self.n_written += len(data) def flush(self): pass motor-2.1.0/test/assert_logs_backport.py000066400000000000000000000051601357430361400204250ustar00rootroot00000000000000"""Backport assertLogs from Python 3.4.""" import collections import logging _LoggingWatcher = collections.namedtuple("_LoggingWatcher", ["records", "output"]) class _BaseTestCaseContext(object): 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( "no logs of level {0} or higher triggered on {1}" .format(self.level_name, self.logger.name)) class AssertLogsMixin(object): def assertLogs(self, logger=None, level=None): return _AssertLogsContext(self, logger, level) motor-2.1.0/test/asyncio_tests/000077500000000000000000000000001357430361400165265ustar00rootroot00000000000000motor-2.1.0/test/asyncio_tests/__init__.py000066400000000000000000000207771357430361400206540ustar00rootroot00000000000000# 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 unittest import SkipTest from mockupdb import MockupDB from motor import motor_asyncio from test.assert_logs_backport import AssertLogsMixin from test.test_environment import env, CA_PEM, CLIENT_PEM from test.utils import get_async_test_timeout class _TestMethodWrapper(object): """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.isgenerator(result): 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(AsyncIOTestCase, self).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, **kwargs): if env.mongod_started_with_ssl: kwargs.setdefault('ssl_ca_certs', CA_PEM) kwargs.setdefault('ssl_certfile', CLIENT_PEM) kwargs.setdefault('ssl', env.mongod_started_with_ssl) kwargs.setdefault('io_loop', self.loop) return kwargs def asyncio_client(self, uri=None, *args, **kwargs): """Get an AsyncIOMotorClient. Ignores self.ssl, you must pass 'ssl' argument. """ return motor_asyncio.AsyncIOMotorClient( uri or env.uri, *args, **self.get_client_kwargs(**kwargs)) 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)) @asyncio.coroutine def make_test_data(self): yield from self.collection.delete_many({}) yield from 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() 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 ensure_future(self, coro): return ensure_future(coro, loop=self.loop) def fetch_next(self, cursor): @asyncio.coroutine def fetch_next(): return (yield from cursor.fetch_next) return self.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 coro_exc = context['exception'] # Raise CancelledError from run_until_complete below. task.cancel() self.loop.set_exception_handler(exc_handler) coro = asyncio.coroutine(f)(self, *args, **kwargs) coro = asyncio.wait_for(coro, actual_timeout, loop=self.loop) task = ensure_future(coro, loop=self.loop) try: self.loop.run_until_complete(task) except: 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 @asyncio.coroutine def get_command_line(client): command_line = yield from client.admin.command('getCmdLineOpts') assert command_line['ok'] == 1, "getCmdLineOpts() failed" return command_line @asyncio.coroutine def server_is_mongos(client): ismaster_response = yield from client.admin.command('ismaster') return ismaster_response.get('msg') == 'isdbgrid' @asyncio.coroutine def skip_if_mongos(client): is_mongos = yield from server_is_mongos(client) if is_mongos: raise unittest.SkipTest("connected to mongos") @asyncio.coroutine def remove_all_users(db): yield from db.command({"dropAllUsersFromDatabase": 1}) motor-2.1.0/test/asyncio_tests/test_aiohttp_gridfs.py000066400000000000000000000261221357430361400231500ustar00rootroot00000000000000# 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 hashlib import logging import time import aiohttp import aiohttp.web import gridfs from motor.aiohttp import AIOHTTPGridFS import test from test.asyncio_tests import AsyncIOTestCase, asyncio_test 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 cls.contents_hash = hashlib.md5(cls.contents).hexdigest() # Record when we created the file, to check the Last-Modified header cls.put_start = datetime.datetime.utcnow().replace(microsecond=0) cls.file_id = 'id' cls.fs.delete(cls.file_id) cls.fs.put(cls.contents, _id='id', filename='foo', content_type='my type') cls.put_end = datetime.datetime.utcnow().replace(microsecond=0) cls.app = cls.srv = cls.app_handler = None @classmethod def tearDownClass(cls): cls.fs.delete(cls.file_id) super().tearDownClass() @asyncio.coroutine 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, _ = yield from asyncio.gather(server, self.app.startup(), loop=self.loop) @asyncio.coroutine 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 = yield from method('http://localhost:8088%s' % path, headers=headers) yield from resp.read() return resp finally: yield from session.close() def get(self, path, **kwargs): return self.request('get', path, **kwargs) def head(self, path, **kwargs): return self.request('head', path, **kwargs) @asyncio.coroutine def stop(self): # aiohttp.rtfd.io/en/stable/web.html#aiohttp-web-graceful-shutdown self.srv.close() yield from self.srv.wait_closed() yield from self.app.shutdown() yield from self.app_handler.shutdown(timeout=1) yield from self.app.cleanup() class AIOHTTPGridFSHandlerTest(AIOHTTPGridFSHandlerTestBase): @asyncio_test def test_basic(self): yield from self.start_app() # First request response = yield from self.get('/fs/foo') self.assertEqual(200, response.status) self.assertEqual(self.contents, (yield from 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 = yield from self.get('/fs/foo', if_modified_since=ims_value) self.assertEqual(304, response.status) self.assertEqual(b'', (yield from response.read())) # If-Modified-Since in the past, get whole response back response = yield from self.get( '/fs/foo', if_modified_since=last_mod_dt - datetime.timedelta(seconds=1)) self.assertEqual(200, response.status) self.assertEqual(self.contents, (yield from response.read())) # Matching Etag response = yield from self.get('/fs/foo', headers={'If-None-Match': etag}) self.assertEqual(304, response.status) self.assertEqual(b'', (yield from response.read())) # Mismatched Etag response = yield from self.get('/fs/foo', headers={'If-None-Match': etag + 'a'}) self.assertEqual(200, response.status) self.assertEqual(self.contents, (yield from response.read())) @asyncio_test def test_404(self): yield from self.start_app() response = yield from self.get('/fs/bar') self.assertEqual(404, response.status) @asyncio_test def test_head(self): yield from self.start_app() response = yield from 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'', (yield from 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 def test_bad_route(self): handler = AIOHTTPGridFS(self.db) yield from self.start_app(extra_routes={'/x/{wrongname}': handler}) response = yield from self.get('/x/foo') self.assertEqual(500, response.status) msg = 'Bad AIOHTTPGridFS route "/x/{wrongname}"' self.assertIn(msg, (yield from response.text())) @asyncio_test def test_content_type(self): yield from 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'), ('jscr.js', 'javascript'), ]: # '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 = yield from method('/fs/' + filename) self.assertEqual(200, response.status) # mimetypes are platform-defined, be fuzzy self.assertIn( expected_type, response.headers['Content-Type'].lower()) @asyncio_test def test_post(self): # Only allow GET and HEAD, even if a POST route is added. yield from self.start_app() result = yield from self.request('post', '/fs/foo') self.assertEqual(405, result.status) class AIOHTTPTZAwareGridFSHandlerTest(AIOHTTPGridFSHandlerTestBase): @asyncio_test def test_tz_aware(self): client = self.asyncio_client(tz_aware=True) yield from self.start_app(AIOHTTPGridFS(client.motor_test)) now = datetime.datetime.utcnow() ago = now - datetime.timedelta(minutes=10) hence = now + datetime.timedelta(minutes=10) response = yield from self.get('/fs/foo', if_modified_since=ago) self.assertEqual(200, response.status) response = yield from self.get('/fs/foo', if_modified_since=hence) self.assertEqual(304, response.status) class AIOHTTPCustomHTTPGridFSTest(AIOHTTPGridFSHandlerTestBase): @asyncio_test 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' yield from 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 = yield from self.get('/fs/foo') self.assertEqual(404, response.status) response = yield from self.get('/fs/' + str(self.file_id)) self.assertEqual(200, response.status) self.assertEqual(self.contents, (yield from 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.utcnow() # 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-2.1.0/test/asyncio_tests/test_asyncio_await.py000066400000000000000000000170371357430361400230010ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import import warnings import bson from motor.motor_asyncio import (AsyncIOMotorClientSession, AsyncIOMotorGridFSBucket) import test from test import env from test.asyncio_tests import asyncio_test, AsyncIOTestCase 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 i 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-2.1.0/test/asyncio_tests/test_asyncio_basic.py000066400000000000000000000111571357430361400227520ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import from unittest import SkipTest import pymongo from pymongo import WriteConcern from pymongo.errors import ConfigurationError from pymongo.read_preferences import ReadPreference, Secondary, Nearest from motor import motor_asyncio import test from test.asyncio_tests import asyncio_test, AsyncIOTestCase from test.utils import ignore_deprecations 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 def test_write_concern(self): # Default empty dict means "w=1" self.assertEqual(WriteConcern(), self.cx.write_concern) yield from self.collection.delete_many({}) yield from self.collection.insert_one({'_id': 0}) for gle_options in [ {}, {'w': 0}, {'w': 1}, {'wtimeout': 1000}, ]: cx = self.asyncio_client(test.env.uri, **gle_options) wc = WriteConcern(**gle_options) 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): yield from collection.insert_one({'_id': 0}) else: yield from collection.insert_one({'_id': 0}) # No error # No error c = collection.with_options(write_concern=WriteConcern(w=0)) yield from c.insert_one({'_id': 0}) cx.close() @ignore_deprecations @asyncio_test 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.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): try: from abc import ABC except ImportError: # Python < 3.4. raise SkipTest() 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() motor-2.1.0/test/asyncio_tests/test_asyncio_change_stream.py000066400000000000000000000203461357430361400244710ustar00rootroot00000000000000# 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 copy import os import threading import time from pymongo.errors import InvalidOperation, OperationFailure from test import SkipTest, env from test.asyncio_tests import asyncio_test, AsyncIOTestCase from test.py35utils import wait_until from test.utils import get_async_test_timeout class TestAsyncIOChangeStream(AsyncIOTestCase): @classmethod @env.require_version_min(3, 6) def setUpClass(cls): super(TestAsyncIOChangeStream, cls).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() 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_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 motor-2.1.0/test/asyncio_tests/test_asyncio_client.py000066400000000000000000000233011357430361400231410ustar00rootroot00000000000000# 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 import pymongo from bson import CodecOptions from pymongo import ReadPreference, WriteConcern from pymongo.errors import ConnectionFailure, OperationFailure import motor from motor import motor_asyncio import test from test.asyncio_tests import (asyncio_test, AsyncIOTestCase, AsyncIOMockServerTestCase, remove_all_users) from test.test_environment import db_user, db_password, env from test.utils import get_primary_pool class TestAsyncIOClient(AsyncIOTestCase): @asyncio_test def test_client_lazy_connect(self): yield from 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'}) yield from asyncio.gather(future0, future1, loop=self.loop) resp = yield from collection.count_documents({'foo': 'bar'}) self.assertEqual(2, resp) cx.close() @asyncio_test def test_close(self): cx = self.asyncio_client() cx.close() self.assertEqual(None, get_primary_pool(cx)) @asyncio_test 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 yield from 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): yield from 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 def test_reconnect_in_case_connection_closed_by_mongo(self): cx = self.asyncio_client(maxPoolSize=1, retryReads=False) yield from 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) socket = pool.sockets.pop() socket.sock.close() pool.sockets.appendleft(socket) with self.assertRaises(pymongo.errors.AutoReconnect): yield from cx.motor_test.test_collection.find_one() @asyncio_test 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): yield from client.admin.command('ismaster') @asyncio_test(timeout=30) 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): yield from client.admin.command('ismaster') @asyncio_test 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.max_pool_size, 100) cx.close() @asyncio_test(timeout=30) def test_drop_database(self): # Make sure we can pass an AsyncIOMotorDatabase instance # to drop_database db = self.cx.test_drop_database yield from db.test_collection.insert_one({}) names = yield from self.cx.list_database_names() self.assertTrue('test_drop_database' in names) yield from self.cx.drop_database(db) names = yield from self.cx.list_database_names() self.assertFalse('test_drop_database' in names) @asyncio_test 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. yield from 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): yield from client.db.collection.find_one() client = self.asyncio_client( 'mongodb://mike:password@%s:%d/%s' % (env.host, env.port, db.name)) yield from 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 def test_list_databases(self): yield from self.collection.insert_one({}) cursor = yield from self.cx.list_databases() self.assertIsInstance(cursor, motor_asyncio.AsyncIOMotorCommandCursor) while (yield from 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 def test_list_database_names(self): yield from self.collection.insert_one({}) names = yield from self.cx.list_database_names() self.assertIsInstance(names, list) self.assertIn(self.collection.database.name, names) class TestAsyncIOClientTimeout(AsyncIOMockServerTestCase): @asyncio_test 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: yield from client.motor_test.test_collection.find_one() self.assertIn('timed out', str(context.exception)) client.close() class TestAsyncIOClientHandshake(AsyncIOMockServerTestCase): @asyncio_test 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 = yield from 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: yield from future except Exception: pass if __name__ == '__main__': unittest.main() motor-2.1.0/test/asyncio_tests/test_asyncio_collection.py000066400000000000000000000274731357430361400240340ustar00rootroot00000000000000# 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 import bson from bson import CodecOptions from bson.binary import JAVA_LEGACY from pymongo import ReadPreference, WriteConcern from pymongo.errors import BulkWriteError, DuplicateKeyError, OperationFailure from pymongo.read_concern import ReadConcern from pymongo.read_preferences import Secondary from motor import motor_asyncio from motor.motor_asyncio import (AsyncIOMotorCollection, AsyncIOMotorCommandCursor) import test from test.asyncio_tests import (asyncio_test, AsyncIOTestCase, skip_if_mongos) from test.utils import ignore_deprecations class TestAsyncIOCollection(AsyncIOTestCase): @asyncio_test 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) yield from collection.delete_many({}) yield from collection.insert_one({'_id': 1}) doc = yield from 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 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): yield from coll.delete_many({}) result = yield from coll.insert_one({'_id': 'xyzzy'}) self.assertEqual('xyzzy', result.inserted_id) result = yield from coll.find_one({'_id': 'xyzzy'}) self.assertEqual(result['_id'], 'xyzzy') yield from coll.delete_many({}) resp = yield from 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 def test_update(self): yield from self.collection.insert_one({'_id': 1}) result = yield from self.collection.update_one( {'_id': 1}, {'$set': {'foo': 'bar'}}) self.assertIsNone(result.upserted_id) self.assertEqual(1, result.modified_count) @ignore_deprecations @asyncio_test def test_update_bad(self): # Violate a unique index, make sure we handle error well coll = self.db.unique_collection yield from coll.create_index('s', unique=True) try: yield from coll.insert_many([{'s': 1}, {'s': 2}]) with self.assertRaises(DuplicateKeyError): yield from coll.update_one({'s': 2}, {'$set': {'s': 1}}) finally: yield from coll.drop() @asyncio_test def test_insert_one(self): collection = self.collection result = yield from collection.insert_one({'_id': 201}) self.assertEqual(201, result.inserted_id) @ignore_deprecations @asyncio_test def test_insert_many_one_bad(self): collection = self.collection yield from collection.insert_one({'_id': 2}) # Violate a unique index in one of many updates, handle error. with self.assertRaises(BulkWriteError): yield from collection.insert_many([ {'_id': 1}, {'_id': 2}, # Already exists {'_id': 3}]) # First insert should have succeeded, but not second or third. self.assertEqual( set([1, 2]), set((yield from collection.distinct('_id')))) @asyncio_test def test_delete_one(self): # Remove a document twice, check that we get a success responses # and n = 0 for the second time. yield from self.collection.insert_one({'_id': 1}) result = yield from 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 = yield from 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 def test_unacknowledged_insert(self): coll = self.db.test_unacknowledged_insert yield from coll.with_options(write_concern=WriteConcern(0)).insert_one( {'_id': 1}) # The insert is eventually executed. while not (yield from coll.count_documents({})): yield from asyncio.sleep(0.1, loop=self.loop) @ignore_deprecations @asyncio_test def test_unacknowledged_update(self): coll = self.collection yield from coll.insert_one({'_id': 1}) yield from coll.with_options(write_concern=WriteConcern(0)).update_one( {'_id': 1}, {'$set': {'a': 1}}) while not (yield from coll.find_one({'a': 1})): yield from asyncio.sleep(0.1, loop=self.loop) @asyncio_test def test_map_reduce(self): # Count number of documents with even and odd _id yield from self.make_test_data() expected_result = [{'_id': 0, 'value': 100}, {'_id': 1, 'value': 100}] map_fn = bson.Code('function map() { emit(this._id % 2, 1); }') reduce_fn = bson.Code(''' function reduce(key, values) { r = 0; values.forEach(function(value) { r += value; }); return r; }''') yield from self.db.tmp_mr.drop() # First do a standard mapreduce, should return AsyncIOMotorCollection collection = self.collection tmp_mr = yield from collection.map_reduce(map_fn, reduce_fn, 'tmp_mr') self.assertTrue( isinstance(tmp_mr, motor_asyncio.AsyncIOMotorCollection), 'map_reduce should return AsyncIOMotorCollection, not %s' % tmp_mr) result = yield from tmp_mr.find().sort([('_id', 1)]).to_list( length=1000) self.assertEqual(expected_result, result) # Standard mapreduce with full response yield from self.db.tmp_mr.drop() response = yield from collection.map_reduce( map_fn, reduce_fn, 'tmp_mr', full_response=True) self.assertTrue( isinstance(response, dict), 'map_reduce should return dict, not %s' % response) self.assertEqual('tmp_mr', response['result']) result = yield from tmp_mr.find().sort([('_id', 1)]).to_list( length=1000) self.assertEqual(expected_result, result) # Inline mapreduce yield from self.db.tmp_mr.drop() result = yield from collection.inline_map_reduce( map_fn, reduce_fn) result.sort(key=lambda doc: doc['_id']) self.assertEqual(expected_result, result) @ignore_deprecations @asyncio_test def test_indexes(self): test_collection = self.collection # Create an index idx_name = yield from test_collection.create_index([('foo', 1)]) index_info = yield from test_collection.index_information() self.assertEqual([('foo', 1)], index_info[idx_name]['key']) # Don't test drop_index or drop_indexes -- Synchro tests them @asyncio.coroutine def _make_test_data(self, n): yield from self.db.drop_collection("test") yield from 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) 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 = yield from self._make_test_data(collection_size) cursor = db.test.aggregate(self.pipeline) docs = yield from cursor.to_list(collection_size) self.assertEqual(expected_sum, sum(doc['_id'] for doc in docs)) @asyncio_test def test_aggregation_cursor_exc_info(self): yield from self._make_test_data(200) cursor = self.db.test.aggregate(self.pipeline) yield from cursor.to_list(length=10) yield from self.db.test.drop() try: yield from 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) 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) if __name__ == '__main__': unittest.main() motor-2.1.0/test/asyncio_tests/test_asyncio_cursor.py000066400000000000000000000555121357430361400232110ustar00rootroot00000000000000# 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 unittest import SkipTest import bson from pymongo import CursorType from pymongo.errors import InvalidOperation, ExecutionTimeout from pymongo.errors import OperationFailure from motor import motor_asyncio from test.utils import one, safe_get, get_primary_pool, TestListener from test.asyncio_tests import (asyncio_test, AsyncIOTestCase, AsyncIOMockServerTestCase, server_is_mongos, get_command_line) from test.test_environment import env 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 def test_count(self): yield from self.make_test_data() coll = self.collection self.assertEqual( 100, (yield from coll.count_documents({'_id': {'$gt': 99}}))) @asyncio_test def test_fetch_next(self): yield from 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 (yield from 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, (yield from cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) self.assertEqual(0, cursor.cursor_id) self.assertEqual(200, i) @unittest.skipUnless(sys.version_info >= (3, 4), "Python 3.4 required") @unittest.skipIf('PyPy' in sys.version, "PyPy") @asyncio_test def test_fetch_next_delete(self): client, server = self.client_server(auto_ismaster=True) cursor = client.test.coll.find() self.fetch_next(cursor) request = yield from 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 yield request = yield from self.run_thread( server.receives, "killCursors", "coll") request.ok() @asyncio_test 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, (yield from cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) # Now cursor knows it's exhausted. self.assertEqual(0, cursor.cursor_id) @asyncio_test def test_fetch_next_is_idempotent(self): # Subsequent calls to fetch_next don't do anything yield from self.make_test_data() coll = self.collection cursor = coll.find() self.assertEqual(None, cursor.cursor_id) yield from cursor.fetch_next self.assertTrue(cursor.cursor_id) self.assertEqual(101, cursor._buffer_size()) yield from cursor.fetch_next # Does nothing self.assertEqual(101, cursor._buffer_size()) yield from cursor.close() @asyncio_test def test_fetch_next_exception(self): coll = self.collection cursor = coll.find() cursor.delegate._Cursor__id = 1234 # Not valid on server. with self.assertRaises(OperationFailure): yield from cursor.fetch_next # Avoid the cursor trying to close itself when it goes out of scope. cursor.delegate._Cursor__id = None @asyncio_test(timeout=30) def test_each(self): yield from self.make_test_data() cursor = self.collection.find({}, {'_id': 1}).sort('_id') future = asyncio.Future(loop=self.loop) 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) yield from future expected = [{'_id': i} for i in range(200)] self.assertEqual(expected, results) @asyncio_test def test_to_list_argument_checking(self): # We need more than 10 documents so the cursor stays alive. yield from self.make_test_data() coll = self.collection cursor = coll.find() with self.assertRaises(ValueError): yield from cursor.to_list(-1) with self.assertRaises(TypeError): yield from cursor.to_list('foo') @asyncio_test def test_to_list_with_length(self): yield from 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), (yield from cursor.to_list(10))) self.assertEqual(expected(10, 100), (yield from 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), (yield from cursor.to_list(1))) self.assertEqual(expected(101, 102), (yield from cursor.to_list(1))) self.assertEqual(expected(102, 103), (yield from cursor.to_list(1))) self.assertEqual([], (yield from cursor.to_list(0))) self.assertEqual(expected(103, 105), (yield from cursor.to_list(2))) # Only 95 docs left, make sure length=100 doesn't error or hang self.assertEqual(expected(105, 200), (yield from cursor.to_list(100))) self.assertEqual(0, cursor.cursor_id) # Nothing left. self.assertEqual([], (yield from cursor.to_list(100))) yield from cursor.close() @asyncio_test def test_to_list_exc_info(self): yield from self.make_test_data() coll = self.collection cursor = coll.find() yield from cursor.to_list(length=10) yield from self.collection.drop() try: yield from 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) @env.require_version_min(4, 2) # failCommand @asyncio_test def test_to_list_cancelled_error(self): yield from self.make_test_data() # Cause an error on a getMore after the cursor.to_list task is # cancelled. yield from self.cx.admin.command( 'configureFailPoint', 'failCommand', mode={'times': 1}, data={'failCommands': ['getMore'], 'errorCode': 96}) try: cursor = self.collection.find(batch_size=2) task = cursor.to_list(None) task.cancel() with self.assertRaises(asyncio.CancelledError): task.result() yield from cursor.close() # Yield for some time to allow pending Cursor callbacks to run. yield from asyncio.sleep(1) finally: yield from self.cx.admin.command( 'configureFailPoint', 'failCommand', mode='off') @asyncio_test def test_to_list_with_length_of_none(self): yield from self.make_test_data() collection = self.collection cursor = collection.find() docs = yield from cursor.to_list(None) # Unlimited. count = yield from collection.count_documents({}) self.assertEqual(count, len(docs)) @asyncio_test 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): yield from cursor.to_list(10) @asyncio_test 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 = yield from self.run_thread(server.receives, "find", "coll") request.replies({"cursor": { "id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) self.assertTrue((yield from future)) self.assertEqual(123, cursor.cursor_id) future = self.ensure_future(cursor.close()) # No reply to OP_KILLCURSORS. request = yield from self.run_thread( server.receives, "killCursors", "coll") request.ok() yield from 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((yield from cursor.fetch_next)) self.assertFalse(cursor.alive) @asyncio_test def test_each_cancel(self): yield from self.make_test_data() loop = self.loop collection = self.collection results = [] future = asyncio.Future(loop=self.loop) 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: pass results.append(result) else: # Complete future.set_result(None) cursor = collection.find() cursor.each(cancel) yield from future self.assertEqual((yield from collection.count_documents({})), len(results)) @asyncio_test def test_rewind(self): yield from self.collection.insert_many([{}, {}, {}]) cursor = self.collection.find().limit(2) count = 0 while (yield from cursor.fetch_next): cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while (yield from cursor.fetch_next): cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while (yield from cursor.fetch_next): cursor.next_object() break cursor.rewind() while (yield from cursor.fetch_next): cursor.next_object() count += 1 self.assertEqual(2, count) self.assertEqual(cursor, cursor.rewind()) @unittest.skipUnless(sys.version_info >= (3, 4), "Python 3.4 required") @unittest.skipIf("PyPy" in sys.version, "PyPy") @asyncio_test def test_cursor_del(self): client, server = self.client_server(auto_ismaster=True) cursor = client.test.coll.find() future = self.fetch_next(cursor) request = yield from self.run_thread(server.receives, "find", "coll") request.replies({"cursor": { "id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) yield from 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. yield from asyncio.sleep(0, loop=self.loop) request = yield from self.run_thread( server.receives, "killCursors", "coll") request.ok() @unittest.skipUnless(sys.version_info >= (3, 4), "Python 3.4 required") @asyncio_test def test_exhaust(self): if (yield from 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) yield from self.db.drop_collection("test") # Insert enough documents to require more than one batch. yield from self.db.test.insert_many([{} for _ in range(150)]) client = self.asyncio_client(maxPoolSize=1) # Ensure a pool. yield from client.db.collection.find_one() socks = get_primary_pool(client).sockets # Make sure the socket is returned after exhaustion. cur = client[self.db.name].test.find(cursor_type=CursorType.EXHAUST) has_next = yield from cur.fetch_next self.assertTrue(has_next) self.assertEqual(0, len(socks)) while (yield from cur.fetch_next): cur.next_object() self.assertEqual(1, len(socks)) # Same as previous but with to_list instead of next_object. docs = yield from client[self.db.name].test.find( cursor_type=CursorType.EXHAUST).to_list( None) self.assertEqual(1, len(socks)) self.assertEqual( (yield from 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. sock = one(socks) cur = client[self.db.name].test.find( cursor_type=CursorType.EXHAUST).batch_size(1) has_next = yield from cur.fetch_next self.assertTrue(has_next) self.assertEqual(0, len(socks)) if 'PyPy' in sys.version: # Don't wait for GC or use gc.collect(), it's unreliable. yield from cur.close() del cur yield from asyncio.sleep(0.1, loop=self.loop) # The exhaust cursor's socket was discarded, although another may # already have been opened to send OP_KILLCURSORS. self.assertNotIn(sock, socks) self.assertTrue(sock.closed) @asyncio_test def test_close_with_docs_in_batch(self): # MOTOR-67 Killed cursor with docs batched is "alive", don't kill again. yield from self.make_test_data() # Ensure multiple batches. cursor = self.collection.find() yield from cursor.fetch_next yield from 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 def test_aggregate_batch_size(self): listener = TestListener() cx = self.asyncio_client(event_listeners=[listener]) c = cx.motor_test.collection yield from c.delete_many({}) yield from 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 (yield from 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 def test_raw_batches(self): c = self.collection yield from c.delete_many({}) yield from 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) yield from cursor.fetch_next batch = cursor.next_object() self.assertEqual([{'_id': 0}, {'_id': 1}], bson.decode_all(batch)) lst = yield from method().batch_size(2).to_list(length=1) self.assertEqual([{'_id': 0}, {'_id': 1}], bson.decode_all(lst[0])) class TestAsyncIOCursorMaxTimeMS(AsyncIOTestCase): def setUp(self): super(TestAsyncIOCursorMaxTimeMS, self).setUp() self.loop.run_until_complete(self.maybe_skip()) def tearDown(self): self.loop.run_until_complete(self.disable_timeout()) super(TestAsyncIOCursorMaxTimeMS, self).tearDown() @asyncio.coroutine def maybe_skip(self): if (yield from server_is_mongos(self.cx)): raise SkipTest("mongos has no maxTimeAlwaysTimeOut fail point") cmdline = yield from 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") @asyncio.coroutine def enable_timeout(self): yield from self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="alwaysOn") @asyncio.coroutine def disable_timeout(self): yield from self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") @asyncio_test def test_max_time_ms_query(self): # Cursor parses server timeout error in response to initial query. yield from self.enable_timeout() cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): yield from cursor.fetch_next cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): yield from cursor.to_list(10) with self.assertRaises(ExecutionTimeout): yield from self.collection.find_one(max_time_ms=100000) @asyncio_test(timeout=60) def test_max_time_ms_getmore(self): # Cursor handles server timeout during getmore, also. yield from self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) yield from cursor.fetch_next cursor.next_object() # Test getmore timeout. yield from self.enable_timeout() with self.assertRaises(ExecutionTimeout): while (yield from cursor.fetch_next): cursor.next_object() yield from cursor.close() # Send another initial query. yield from self.disable_timeout() cursor = self.collection.find().max_time_ms(100000) yield from cursor.fetch_next cursor.next_object() # Test getmore timeout. yield from self.enable_timeout() with self.assertRaises(ExecutionTimeout): yield from cursor.to_list(None) # Avoid 'IOLoop is closing' warning. yield from cursor.close() finally: # Cleanup. yield from self.disable_timeout() yield from self.collection.delete_many({}) @asyncio_test def test_max_time_ms_each_query(self): # Cursor.each() handles server timeout during initial query. yield from self.enable_timeout() cursor = self.collection.find().max_time_ms(100000) future = asyncio.Future(loop=self.loop) 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) yield from future @asyncio_test(timeout=30) def test_max_time_ms_each_getmore(self): # Cursor.each() handles server timeout during getmore. yield from self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) yield from cursor.fetch_next cursor.next_object() future = asyncio.Future(loop=self.loop) def callback(result, error): if error: future.set_exception(error) elif not result: # Done. future.set_result(None) yield from self.enable_timeout() with self.assertRaises(ExecutionTimeout): cursor.each(callback) yield from future yield from cursor.close() finally: # Cleanup. yield from self.disable_timeout() yield from 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-2.1.0/test/asyncio_tests/test_asyncio_database.py000066400000000000000000000144621357430361400234370ustar00rootroot00000000000000# 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 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) from test import env from test.asyncio_tests import (AsyncIOTestCase, asyncio_test) class TestAsyncIODatabase(AsyncIOTestCase): @asyncio_test 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) yield from db.test_collection.delete_many({}) yield from db.test_collection.insert_one({'_id': 1}) doc = yield from 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 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 = yield from cursor.to_list(10) self.assertEqual(expected, docs) @asyncio_test def test_command(self): result = yield from self.cx.admin.command("buildinfo") # Make sure we got some sane result or other. self.assertEqual(1, result['ok']) @asyncio_test def test_create_collection(self): # Test creating collection, return val is wrapped in # AsyncIOMotorCollection, creating it again raises CollectionInvalid. db = self.db yield from db.drop_collection('test_collection2') collection = yield from db.create_collection('test_collection2') self.assertTrue(isinstance(collection, AsyncIOMotorCollection)) self.assertTrue( 'test_collection2' in (yield from db.list_collection_names())) with self.assertRaises(CollectionInvalid): yield from db.create_collection('test_collection2') @asyncio_test def test_drop_collection(self): # Make sure we can pass an AsyncIOMotorCollection instance to # drop_collection. db = self.db collection = db.test_drop_collection yield from collection.insert_one({}) names = yield from db.list_collection_names() self.assertTrue('test_drop_collection' in names) yield from db.drop_collection(collection) names = yield from db.list_collection_names() self.assertFalse('test_drop_collection' in names) @asyncio_test def test_validate_collection(self): db = self.db with self.assertRaises(TypeError): yield from db.validate_collection(5) with self.assertRaises(TypeError): yield from db.validate_collection(None) with self.assertRaises(OperationFailure): yield from db.validate_collection("test.doesnotexist") with self.assertRaises(OperationFailure): yield from db.validate_collection(db.test.doesnotexist) yield from db.test.insert_one({"dummy": "object"}) self.assertTrue((yield from db.validate_collection("test"))) self.assertTrue((yield from 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) if __name__ == '__main__': unittest.main() motor-2.1.0/test/asyncio_tests/test_asyncio_gridfsbucket.py000066400000000000000000000065621357430361400243510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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.""" import asyncio from io import BytesIO from gridfs.errors import NoFile from pymongo.write_concern import WriteConcern from pymongo.read_preferences import ReadPreference from motor.motor_asyncio import AsyncIOMotorGridFSBucket from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.utils import ignore_deprecations class TestAsyncIOGridFSBucket(AsyncIOTestCase): @asyncio.coroutine def _reset(self): yield from self.db.drop_collection("fs.files") yield from self.db.drop_collection("fs.chunks") yield from self.db.drop_collection("alt.files") yield from self.db.drop_collection("alt.chunks") def setUp(self): super(TestAsyncIOGridFSBucket, self).setUp() self.loop.run_until_complete(self._reset()) self.bucket = AsyncIOMotorGridFSBucket(self.db) def tearDown(self): self.loop.run_until_complete(self._reset()) super(TestAsyncIOGridFSBucket, self).tearDown() @asyncio_test def test_basic(self): oid = yield from self.bucket.upload_from_stream("test_filename", b"hello world") gout = yield from self.bucket.open_download_stream(oid) self.assertEqual(b"hello world", (yield from gout.read())) self.assertEqual(1, (yield from self.db.fs.files.count_documents({}))) self.assertEqual(1, (yield from self.db.fs.chunks.count_documents({}))) dst = BytesIO() yield from self.bucket.download_to_stream(gout._id, dst) self.assertEqual(b"hello world", dst.getvalue()) yield from self.bucket.delete(oid) with self.assertRaises(NoFile): yield from self.bucket.open_download_stream(oid) self.assertEqual(0, (yield from self.db.fs.files.count_documents({}))) self.assertEqual(0, (yield from 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, disable_md5=True, 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-2.1.0/test/asyncio_tests/test_asyncio_ipv6.py000066400000000000000000000037471357430361400225630ustar00rootroot00000000000000# 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 unittest from pymongo import MongoClient from pymongo.errors import ConnectionFailure import test from test import SkipTest from test.asyncio_tests import asyncio_test, AsyncIOTestCase from test.test_environment import db_user, db_password, connected, env class MotorIPv6Test(AsyncIOTestCase): @asyncio_test 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 yield from collection.insert_one({"dummy": "object"}) self.assertTrue((yield from collection.find_one({"dummy": "object"}))) if __name__ == '__main__': unittest.main() motor-2.1.0/test/asyncio_tests/test_asyncio_replica_set.py000066400000000000000000000044511357430361400241620ustar00rootroot00000000000000# 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 unittest import pymongo import pymongo.errors import pymongo.mongo_replica_set_client import test from motor import motor_asyncio from test import env, SkipTest from test.asyncio_tests import AsyncIOTestCase, asyncio_test from test.test_environment import env 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 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): yield from 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(TestReplicaSetClientAgainstStandalone, self).setUp() if test.env.is_replica_set: raise SkipTest( "Connected to a replica set, not a standalone mongod") @asyncio_test 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): yield from client.test.test.find_one() if __name__ == '__main__': unittest.main() motor-2.1.0/test/asyncio_tests/test_asyncio_session.py000066400000000000000000000170621357430361400233550ustar00rootroot00000000000000# 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. from __future__ import unicode_literals """Test ClientSession support with asyncio.""" import asyncio import copy import sys import unittest from pymongo import InsertOne, IndexModel from pymongo.errors import InvalidOperation 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 class TestAsyncIOSession(AsyncIOTestCase): @classmethod def setUpClass(cls): super(TestAsyncIOSession, cls).setUpClass() if not env.sessions_enabled: raise SkipTest("Sessions not supported") @asyncio.coroutine def _test_ops(self, client, *ops): listener = client.event_listeners()[0][0] for f, args, kw in ops: s = yield from 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 yield from 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: yield from s.end_session() with self.assertRaisesRegex(InvalidOperation, "ended session"): yield from f(*args2, **kw2) # No explicit session. for f, args, kw in ops: listener.results.clear() yield from 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 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, [], {}), ] yield from self._test_ops(client, *ops) @asyncio_test(timeout=30) def test_collection(self): listener = TestListener() client = self.asyncio_client(event_listeners=[listener]) yield from client.drop_database('motor_test') coll = client.motor_test.test_collection @asyncio.coroutine def list_indexes(session=None): yield from coll.list_indexes(session=session).to_list(length=None) @asyncio.coroutine def aggregate(session=None): yield from coll.aggregate([], session=session).to_list(length=None) # Test some collection methods - the rest are in test_cursor. yield from 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 def test_cursor(self): listener = TestListener() client = self.asyncio_client(event_listeners=[listener]) yield from self.make_test_data() coll = client.motor_test.test_collection s = yield from client.start_session() # Simulate "async with" on Python 3.4. try: listener.results.clear() cursor = coll.find(session=s) yield from 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: yield from s.end_session() with self.assertRaisesRegex(InvalidOperation, "ended session"): yield from coll.find(session=s).to_list(length=None) # No explicit session. listener.results.clear() cursor = coll.find() yield from 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-2.1.0/test/asyncio_tests/test_asyncio_ssl.py000066400000000000000000000154501357430361400224720ustar00rootroot00000000000000# 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 ssl import unittest from unittest import SkipTest from pymongo.errors import ConfigurationError, ConnectionFailure import test from motor.motor_asyncio import AsyncIOMotorClient from test.asyncio_tests import asyncio_test from test.test_environment import CA_PEM, CLIENT_PEM, env # 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): 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, ssl='foo') self.assertRaises(ConfigurationError, AsyncIOMotorClient, io_loop=self.loop, ssl=False, ssl_certfile=CLIENT_PEM) self.assertRaises(IOError, AsyncIOMotorClient, io_loop=self.loop, ssl_certfile="NoFile") self.assertRaises(TypeError, AsyncIOMotorClient, io_loop=self.loop, ssl_certfile=True) self.assertRaises(IOError, AsyncIOMotorClient, io_loop=self.loop, ssl_keyfile="NoFile") self.assertRaises(TypeError, AsyncIOMotorClient, io_loop=self.loop, ssl_keyfile=True) @asyncio_test 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, ssl_certfile=CLIENT_PEM, ssl_ca_certs=CA_PEM, io_loop=self.loop) yield from client.db.collection.find_one() response = yield from client.admin.command('ismaster') if 'setName' in response: client = AsyncIOMotorClient( env.host, env.port, ssl=True, ssl_certfile=CLIENT_PEM, ssl_ca_certs=CA_PEM, replicaSet=response['setName'], io_loop=self.loop) yield from client.db.collection.find_one() @asyncio_test 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, ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.loop) yield from client.db.collection.find_one() response = yield from client.admin.command('ismaster') if 'setName' in response: client = AsyncIOMotorClient( env.host, env.port, replicaSet=response['setName'], ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.loop) yield from client.db.collection.find_one() @asyncio_test 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, ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_NONE, ssl_ca_certs=CA_PEM, io_loop=self.loop) yield from client.admin.command('ismaster') @asyncio_test 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, ssl=True, ssl_certfile=CLIENT_PEM, ssl_ca_certs=CA_PEM, io_loop=self.loop) response = yield from 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, ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.loop) yield from 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'], ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.loop) yield from client.db.collection.find_one() motor-2.1.0/test/asyncio_tests/test_asyncio_tests.py000066400000000000000000000145671357430361400230430ustar00rootroot00000000000000# 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 contextlib import io import os import unittest import concurrent.futures from test.asyncio_tests import AsyncIOTestCase, asyncio_test def run_test_case(case, suppress_output=True): suite = unittest.defaultTestLoader.loadTestsFromTestCase(case) if suppress_output: stream = io.StringIO() else: stream = 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 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() def test_decorated_with_no_args(self): pass result = run_test_case(TestPasses) self.assertEqual(0, len(result.errors)) class TestFails(AsyncIOTestCase): @asyncio_test() def test_decorated_with_no_args(self): assert False 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) def test_that_is_too_slow(self): yield from self.middle() @asyncio.coroutine def middle(self): yield from self.inner() @asyncio.coroutine def inner(self): yield from asyncio.sleep(1, loop=self.loop) 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 def default_timeout(self): yield from asyncio.sleep(0.1, loop=self.loop) 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) def custom_timeout(self): yield from asyncio.sleep(0.2, loop=self.loop) 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 def test_that_fails(self): yield from self.middle() @asyncio.coroutine def middle(self): yield from self.inner() @asyncio.coroutine 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): def test_that_should_be_decorated(self): yield from asyncio.sleep(0.01, loop=self.loop) 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-2.1.0/test/asyncio_tests/test_examples.py000066400000000000000000001132251357430361400217610ustar00rootroot00000000000000# 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 datetime from io import StringIO from unittest.mock import patch import pymongo from pymongo import WriteConcern from pymongo.errors import ConnectionFailure, OperationFailure from pymongo.read_concern import ReadConcern from pymongo.read_preferences import ReadPreference from test import env from test.asyncio_tests import AsyncIOTestCase, asyncio_test 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 stats = await db.command("collStats", "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, retrying" "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. CRUD operations in transactions must be on existing 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 motor-2.1.0/test/certificates/000077500000000000000000000000001357430361400163045ustar00rootroot00000000000000motor-2.1.0/test/certificates/ca.pem000066400000000000000000000024011357430361400173670ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDhTCCAm2gAwIBAgIDBWMSMA0GCSqGSIb3DQEBBQUAMG4xEzARBgNVBAMTCkNE cml2ZXIgQ0ExEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01vbmdvREIxEjAQ BgNVBAcTCVJleWtqYXZpazESMBAGA1UECBMJUmV5a2phdmlrMQswCQYDVQQGEwJJ UzAeFw0xNjA0MTQxODM2MDVaFw0zNjA0MTQxODM2MDVaMG4xEzARBgNVBAMTCkNE cml2ZXIgQ0ExEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01vbmdvREIxEjAQ BgNVBAcTCVJleWtqYXZpazESMBAGA1UECBMJUmV5a2phdmlrMQswCQYDVQQGEwJJ UzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPALNK5y00nCB3RZcQoz STimEel5xQZo3egezDDawYDY75IIZMCHZNmVTiTC1CDqrzmImA+QhkoRZPXZgZpz 7wkVzM822uJ5oj4QWLo1B9xLh5XyHixm9iFwngoPuT2tm29ynAxYmLAZb7D2gFql 5zlMoUeShyDJMb+ATCg6Bp5wXN5rRsyKBzcwJX2N31BeSZUaVNQ8/Ns2L8ngKvZD rDXnNIQyVG0B7v+sOSht75VcaTm8xMA6FT0nDLp6x/v/g7szTj+Dh/uXDEv5QWQ+ G8Ct74PC9Vs/pv5uBvj55Cx34G1rXpIkXW39Qod0RhYs6MjNoBT1yi9vkguY7Izd xT0CAwEAAaMsMCowDAYDVR0TBAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH BH8AAAEwDQYJKoZIhvcNAQEFBQADggEBABEkLGA6fkMc8PNZKR0cqqFMWdRq+bn/ fq73Oa/MgUphmg/oWB7QniW/RueNG5+vOS11gLj8Kk5eldfY98Uey8cido50CKcy wq4mjq5jMx9xFjOudssZW4cRc7ov+FoiAYSl8ZyH/rPx/u2ECPS076oZ/wAovevk kftIbh8FPWVKTfe4nXSb/KDI6X+DGM4eQL3/IkC1qHq0j/xKpIEf7zU3niO76Pjv 8KBOJn79kPlxu4vQJv7hu3jgucm9Js+ok15Lw4iVcxOel452X+xkykAtKUx8uyZ2 8TkXV6lqo66SJVtEUMlMX4E9V1uKUKQjYd/DYsyHZmw6eadaKntf8gI= -----END CERTIFICATE----- motor-2.1.0/test/certificates/client.pem000066400000000000000000000056101357430361400202670ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAu/KmDvhqb6wXMMc3GfvHfzS/r6fFz1TbU0VJZpsi4U/Np4AK w13oPNN796nD4MKRn6Puv8vMbFOT5kNVnLI8YWI7BZc7kWM8rYW1Sh9T/87vAeZl oobP0u8RTEzhWDUzJBgeX69fvMh+7FpjfRbwDXIVQDl+mIUQXybj0JaFb3S2r4Nm dD84OeJ0KzjIsitJhEY9x2o5ihEz1is+lG8baprU9KLIa3sH/P1ZAtcmoTavbcTP X5QNnRRYBDzqCyOy+kYkjH0XnlQQBzXw85sEkfsIF0wBFDAQFciliG4KhCeFCfs2 yimY/So13378JmCgEBQQ9HsMYhoSjfyi5AoKyQIDAQABAoIBAB63lqpFsWeU8alY Nco6Wy2QGnAD9JFhJgNjBI8PddDrMQpaI+gwNd+TFOSyS0A1rIUxzrhr/3oI4tg0 Uw6sFrA31LuiLushE+6sNqfpnNnnISrnU98PN3Ia/nHA8udusnAIvRYYXIZHIm9Q iogMMqmP5qc7flRBEx7qBTu3DhHb8lYfnlulaDP1T4CFUiFHBjSYC2Ug8csn3fkE MkedIPgaDBYPplcwJ0FBzkETsZ5PxEE9fx1GzVc1LcfaGtUUeECzWdF1pH5bASdj BxIl/vqIu8ykWBgZS9I4TJkrmQgr/C0bX+FcVmT7W9H5Gzv1f5U2/e3LAs6ZZMgE Edo2d80CgYEA80FBKqtefkwZCCarWH9k17lpOgdInKbKNqpWu39IEh6WhnbxR3n5 HgY3a1SVA8JEbZxaxifJhnIxtboS/Ii5Gu9kL2rvD+YzzAfCimIl0nnc76445qg3 +p+hTAOlY1YkM6r6rFh2wsy1F6Dvq4wR1N4ps0rOZN3g/S1X18nU5E8CgYEAxcuR Z6jzwTvVh1D9tUHH9m0kktDUamIgUIodPBQLFR9MXNQsGr9+2BOSbydExccVkZko cbFyjw3z/HE4Xw6cRoNmNh8bHexKDkKC/zGRFbADXc8aZjM+VG1tVc2IYRvGvslA VPwUBcqxwkRGGifOXM3/OkuB25r4u9wYHgHMIWcCgYEAwNtwhhrdzPYaNnsqB/zg f6PxSF9z8zbNRW/hWb/87NixcKQmgQu0NViqYX7qsdM4m0qGMWpj/SLWRITk/rCL SU6YuHtoU8uoL0/LrhlIcUnKn4AafszJrPsYT4LoggxOoWQiX7e3WJQh0zz2J6Ti kkkc+JgLcK707OUhKuVK00MCgYEAgwkWnj7epWBXWJr+SUt4BrhNp3nzjzrMv66Q KZH5uZePhlwFj/7XMJzewQaZ2BDJKDj4A0IUa9NJlS8IsmjCfTBl0WsQEuXMNeA4 Woni3SRLRqWCjrxaCaKo8VQFipfVNeWr2eFZ/nBHDTUf4AoI+5B5CoylTfA/4myw CYkVS/cCgYAnMSM89onsgAvnlh/mmQTEbgeYwDV0rJCXP6uhMZO7C9S1xh6LaxGs Vw3t4aJrpZ7uzv/+YtMwmR7zXmWBAOkXQghuefSXVSoGzHjhOQPeI4Od3LHWpLS6 O18kLPMFvpVsefz/EgadQn0FmlHuwATXJ0dmWxCZu6sUAV3jfxVrIA== -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIIDfjCCAmagAwIBAgIDBIcQMA0GCSqGSIb3DQEBBQUAMG4xEzARBgNVBAMTCkNE cml2ZXIgQ0ExEDAOBgNVBAsTB0RyaXZlcnMxEDAOBgNVBAoTB01vbmdvREIxEjAQ BgNVBAcTCVJleWtqYXZpazESMBAGA1UECBMJUmV5a2phdmlrMQswCQYDVQQGEwJJ UzAeFw0xNjA0MTQxODU3MzJaFw0zNjA0MTQxODU3MzJaMGAxDzANBgNVBAMTBmNs aWVudDEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9uZ29EQjENMAsGA1UE BxMET3NsbzENMAsGA1UECBMET3NsbzELMAkGA1UEBhMCTk8wggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQC78qYO+GpvrBcwxzcZ+8d/NL+vp8XPVNtTRUlm myLhT82ngArDXeg803v3qcPgwpGfo+6/y8xsU5PmQ1WcsjxhYjsFlzuRYzythbVK H1P/zu8B5mWihs/S7xFMTOFYNTMkGB5fr1+8yH7sWmN9FvANchVAOX6YhRBfJuPQ loVvdLavg2Z0Pzg54nQrOMiyK0mERj3HajmKETPWKz6UbxtqmtT0oshrewf8/VkC 1yahNq9txM9flA2dFFgEPOoLI7L6RiSMfReeVBAHNfDzmwSR+wgXTAEUMBAVyKWI bgqEJ4UJ+zbKKZj9KjXffvwmYKAQFBD0ewxiGhKN/KLkCgrJAgMBAAGjMzAxMBMG A1UdJQQMMAoGCCsGAQUFBwMCMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAN BgkqhkiG9w0BAQUFAAOCAQEAy00IOu2h7G9psRmZATgu4zSK8gAIzw5QHNGbcGBd T4xCblOskWwDphXpmaoeX+BNDrpzprISn6F0GXz4v0kLj595zNoDsmhug2/Rg8Aq ERDOLVZcxp4FlJtcs4lBFHtjl9cnDxA2Usj/J0IFJHuFyMvy/zWqFNFmBnj19iul 7Hs1t8zRcDmtlW+2LKS8hQO9hL2v5jOXENmmyJ628WielArutVYRRnEme6/XpkO7 LFM0zItA+/yGDByoszivWhpiB6/uHONGH2fFMaal80OIrgJeffblqHoNUUsI3A72 v/gqsJ+D8bjPLGd7gLuNOnZuIoGI0md/t9XWeiLMu/fceA== -----END CERTIFICATE----- motor-2.1.0/test/certificates/server.pem000066400000000000000000000056201357430361400203200ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAhFDrxHa8DDVZfjc5p3i2700AKw3BPrFi1du1W9BVUS1s4G1T eytonDR+kqi3lPJ1nU/tPHCveKyt5u1/76mAiT6FqdKM5bjm76Hq67sXCQ+FQZ+C aDZILUVldMS1wIi21MK9b3yYCnZD+Oh+yjsHToOo+8XzvGkbLAh5/THqp6DEw4Mq J1nbIKrC3qlBZJHIe7cblxgFPy2Uucrr22VvwnI5uYlEQdZD4udNRc/pijeDPVZT 7/QhDDG5vVK16dzZhPZxaCLnOEKocuLJMEmM5jKctTgrGdDi5Wn86VNr1zqzTTLa rpTwniJGT0UsHsn6OqoB0+x8MOcweJ/JJAE3cQIDAQABAoIBABnaScawQFvOTLcZ iT1Mm8mz7NV8sYZTWHFapVvUH+dljcFn+wi02ZKzuwtixcPnf82F4V+O0OouKyhU vUuRfdjTC4C77+WutAicRW9btcuCadIBnZRJDt8xkPo4uR5uV/LIPgU7feWFaBCJ NMTEtc/Eb88JxW7JxpJa8aLkDupDecD8q40F65MjraV88wZ1uyS8e7m/Qj0gx3GT KNtuFFuXRM/L9RdKuhCLCxc88uCTsgBFbrsl/ftK4mqVStUiEKUwmWRr5U9PQt6z SpyjMfTYtf2CgOaUmK0HyhgOM8v7yXHJphsxqseeQp6YMNelgSxRfswSMjZ5wJjV s5/Fk0ECgYEA0h1fVbT4pLoS0qh/n6q0uclIq1JZe+LX6Ur++fxpHkA7vjV4wthC fA/3eSZcgF/zwyFVJZkO0I6n3bv7VnknZXbRUm6vr/z6nBblYKcilHVzUXyGJOkc XsCiexNMWzpAntk4ZhiDYoGb54bDWKrdI+YUERogJjvqVmVtPo+XMNkCgYEAoTYn vNJ5B/9opB4zN70/PzqLsqhyFjKrqiVu6Zo71nu5Cx5ULmt67FMGTqEej8eNQApD S45wiZUXjMRPm4c99uYn3kd+aq/25Xms0EbSbn5UHnRcurK0VMUHBRZ/GdiKvB21 zeoZrCdredsGEMrIxmFTzcnAiOI3OkQDK6/1nFkCgYATSMzaMezsOYPGv95Oyj0i HB5GUx2RaEWGzPJQAWGofB6/m2rE/1JCzrhY7zNoOOZHj4SgGccpTx6LFFjCHAaI onodxbjsPxzFPLi6wTxEQe9emD47nwWSkL2Y7DGC4frSWX96p19PqDOl8VF2juAT DmfjFfHDcJZWbaK0LjujKQKBgFFWa5I/d0VrvjYf7qOO3lxeg6Fuj7FXe3lrTZyb btl2rxRHBx4sqGaYNTRy4mKUwlWvV+jFQ06HZMxnN+s2vzLVSQRDeisGAUIis+iZ 8w5AutxqxWaFR6sFhdcxVIuLz+O98b6lALL1aKqgsVzDk61T6d6IjpYB3O+O5TKj ov5xAoGAU8yD4Qxa5FgSsFMot8ofc2J5NlSpnXBg3DOVD/ljKDNZAQX0nThAmyOu O3oTRQrT7lDJ4Q2W8Xvy1pWDokY0tJOVdf7R3b80Pmk/tdRpXgmtIoC+jQDZlGaO Nls9O0wy4l63LyIXmum13i+T2dWtkUDdN0lLvkJwVuKq0TzYaHA= -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIIDiDCCAnCgAwIBAgICZXIwDQYJKoZIhvcNAQEFBQAwbjETMBEGA1UEAxMKQ0Ry aXZlciBDQTEQMA4GA1UECxMHRHJpdmVyczEQMA4GA1UEChMHTW9uZ29EQjESMBAG A1UEBxMJUmV5a2phdmlrMRIwEAYDVQQIEwlSZXlramF2aWsxCzAJBgNVBAYTAklT MB4XDTE3MDEyNzIyNDg0N1oXDTM3MDEyNzIyNDg0N1owbjESMBAGA1UEAxMJbG9j YWxob3N0MRAwDgYDVQQLEwdEcml2ZXJzMRAwDgYDVQQKEwdNb25nb0RCMRIwEAYD VQQHEwlQYWxvIEFsdG8xEzARBgNVBAgTCkNhbGlmb3JuaWExCzAJBgNVBAYTAlVT MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhFDrxHa8DDVZfjc5p3i2 700AKw3BPrFi1du1W9BVUS1s4G1TeytonDR+kqi3lPJ1nU/tPHCveKyt5u1/76mA iT6FqdKM5bjm76Hq67sXCQ+FQZ+CaDZILUVldMS1wIi21MK9b3yYCnZD+Oh+yjsH ToOo+8XzvGkbLAh5/THqp6DEw4MqJ1nbIKrC3qlBZJHIe7cblxgFPy2Uucrr22Vv wnI5uYlEQdZD4udNRc/pijeDPVZT7/QhDDG5vVK16dzZhPZxaCLnOEKocuLJMEmM 5jKctTgrGdDi5Wn86VNr1zqzTTLarpTwniJGT0UsHsn6OqoB0+x8MOcweJ/JJAE3 cQIDAQABozAwLjAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAA AAAAAAAAAAEwDQYJKoZIhvcNAQEFBQADggEBALbwRmXw2JmJzygVXJp5GM7QAM+G rAmyE5ai3tHQRyRdqWOfGuT78+qIdSMc+RS1fwdAlroYxkOwSD5pL5qm0AEsofrc 5LfinyCfhnE8gzBh7zu8mP3agW39BHiYy4LaER708r4NRiJ12jSgEXgytDgscH5V 3CrzkYh4JxrIMbKzIJs5EQjHVwwgShd5ns6odfgcEoaS5B79zDH7tNx12xhs3SPD e4Fo+wjsoUC7LA3seq5qUSCL3pqT4Pu2pQDuVd9ryInGTwrX3NAv+VgKOgTBHabb ZXJ7zTCY70v5arCdddPMFsDpcr0YMROqOC6wXLgGX/rX4zV5YgFJP9bSlu4= -----END CERTIFICATE----- motor-2.1.0/test/py35utils.py000066400000000000000000000023241357430361400160730ustar00rootroot00000000000000# Copyright 2019-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 time """Utilities for testing Motor on Python 3.5+.""" 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) motor-2.1.0/test/test_environment.py000066400000000000000000000265061357430361400176250ustar00rootroot00000000000000# 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 import warnings from functools import wraps import pymongo.errors from test import SkipTest from test.utils import create_user from test.version import Version 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: import aiohttp except ImportError: HAVE_AIOHTTP = False aiohttp = None # 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.""" with warnings.catch_warnings(): # Ignore warning that "ismaster" is always routed to primary even # if client's read preference isn't PRIMARY. warnings.simplefilter("ignore", UserWarning) client.admin.command('ismaster') # 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 socket.error: return False finally: socket.setdefaulttimeout(socket_timeout) class TestEnvironment(object): 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, connectTimeoutMS=connectTimeoutMS, socketTimeoutMS=socketTimeoutMS, serverSelectionTimeoutMS=serverSelectionTimeoutMS, ssl_ca_certs=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, connectTimeoutMS=connectTimeoutMS, socketTimeoutMS=socketTimeoutMS, serverSelectionTimeoutMS=serverSelectionTimeoutMS, ssl_ca_certs=CA_PEM, ssl_certfile=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, 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, ssl_ca_certs=CA_PEM, ssl_certfile=CLIENT_PEM)) else: client = connected(pymongo.MongoClient(host, port, username=db_user, password=db_password, 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): 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_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 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() motor-2.1.0/test/tornado_tests/000077500000000000000000000000001357430361400165275ustar00rootroot00000000000000motor-2.1.0/test/tornado_tests/__init__.py000066400000000000000000000121751357430361400206460ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Utilities for testing Motor with Tornado.""" import concurrent.futures from unittest import SkipTest from mockupdb import MockupDB from bson import SON from tornado import gen, testing import motor from test.test_environment import env, CA_PEM, CLIENT_PEM from test.version import Version @gen.coroutine def get_command_line(client): command_line = yield client.admin.command('getCmdLineOpts') assert command_line['ok'] == 1, "getCmdLineOpts() failed" raise gen.Return(command_line) @gen.coroutine def server_is_mongos(client): ismaster_response = yield client.admin.command('ismaster') raise gen.Return(ismaster_response.get('msg') == 'isdbgrid') @gen.coroutine def skip_if_mongos(client): is_mongos = yield server_is_mongos(client) if is_mongos: raise SkipTest("connected to mongos") @gen.coroutine def remove_all_users(db): yield db.command({"dropAllUsersFromDatabase": 1}) @gen.coroutine def skip_if_mongos(client): is_mongos = yield server_is_mongos(client) if is_mongos: raise SkipTest("connected to mongos") @gen.coroutine def remove_all_users(db): yield 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(MotorTest, self).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 @gen.coroutine def make_test_data(self): yield self.collection.delete_many({}) yield self.collection.insert_many([{'_id': i} for i in range(200)]) make_test_data.__test__ = False @gen.coroutine def set_fail_point(self, client, command_args): cmd = SON([('configureFailPoint', 'failCommand')]) cmd.update(command_args) yield client.admin.command(cmd) def get_client_kwargs(self, **kwargs): if env.mongod_started_with_ssl: kwargs.setdefault('ssl_certfile', CLIENT_PEM) kwargs.setdefault('ssl_ca_certs', CA_PEM) kwargs.setdefault('ssl', 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({}) self.cx.close() super(MotorTest, self).tearDown() class MotorReplicaSetTestBase(MotorTest): def setUp(self): super(MotorReplicaSetTestBase, self).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 def run_thread(self, fn, *args, **kwargs): return self.executor.submit(fn, *args, **kwargs) class AsyncVersion(Version): """Version class that can be instantiated with an async client from within a coroutine.""" @classmethod @gen.coroutine def from_client(cls, client): info = yield client.server_info() if 'versionArray' in info: raise gen.Return(cls.from_version_array(info['versionArray'])) raise gen.Return(cls.from_string(info['version'])) motor-2.1.0/test/tornado_tests/test_motor_auth.py000066400000000000000000000076021357430361400223260ustar00rootroot00000000000000# 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 __future__ import unicode_literals from pymongo.errors import OperationFailure from tornado.testing import gen_test from test.test_environment import env from test.tornado_tests import MotorTest """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(MotorAuthTest, self).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 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: yield client.scramtestdb.collection.insert_one({}) else: with self.assertRaises(OperationFailure): yield client.scramtestdb.collection.insert_one({}) # No mechanism specified, always works. for user, mechanism, should_work in [('sha1', None, True), ('sha256', None, False), ('both', None, True)]: client = self.motor_client(username=user, password='pwd', authsource='scramtestdb') yield client.scramtestdb.collection.insert_one({}) @gen_test 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', u'\u2163', roles=['dbOwner'], mechanisms=['SCRAM-SHA-256']) client = self.motor_client(username='saslprep-test-user', password='IV', authsource='scramtestdb') yield client.scramtestdb.collection.insert_one({}) motor-2.1.0/test/tornado_tests/test_motor_await.py000066400000000000000000000167471357430361400225040ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import import warnings import bson from test import env """Test Motor, an asynchronous driver for MongoDB and Tornado.""" from tornado.testing import gen_test from motor.motor_tornado import MotorClientSession, MotorGridFSBucket import test from test.tornado_tests import MotorTest 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 i 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-2.1.0/test/tornado_tests/test_motor_basic.py000066400000000000000000000110321357430361400224360ustar00rootroot00000000000000# 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. from __future__ import unicode_literals, absolute_import """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import pymongo from pymongo import WriteConcern from pymongo.errors import ConfigurationError from pymongo.read_preferences import ReadPreference, Secondary, Nearest from tornado.testing import gen_test import motor import test from test import SkipTest from test.tornado_tests import MotorTest from test.utils import ignore_deprecations 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 def test_write_concern(self): # Default empty dict means "w=1" self.assertEqual(WriteConcern(), self.cx.write_concern) yield self.collection.delete_many({}) yield self.collection.insert_one({'_id': 0}) for gle_options in [ {}, {'w': 0}, {'w': 1}, {'wtimeout': 1000}, ]: cx = self.motor_client(test.env.uri, **gle_options) wc = WriteConcern(**gle_options) 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): yield collection.insert_one({'_id': 0}) else: yield collection.insert_one({'_id': 0}) # No error # No error c = collection.with_options(write_concern=WriteConcern(w=0)) yield c.insert_one({'_id': 0}) cx.close() @ignore_deprecations @gen_test 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.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): try: from abc import ABC except ImportError: # Python < 3.4. raise SkipTest() 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() motor-2.1.0/test/tornado_tests/test_motor_change_stream.py000066400000000000000000000177311357430361400241710ustar00rootroot00000000000000# 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. from __future__ import absolute_import, unicode_literals """Test MotorChangeStream.""" import copy import os import threading import time from pymongo.errors import InvalidOperation, OperationFailure from tornado.testing import gen_test from test import SkipTest, env from test.tornado_tests import MotorTest from test.py35utils import wait_until from test.utils import get_async_test_timeout class MotorChangeStreamTest(MotorTest): @classmethod @env.require_version_min(3, 6) def setUpClass(cls): super(MotorChangeStreamTest, cls).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() 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 motor-2.1.0/test/tornado_tests/test_motor_client.py000066400000000000000000000266261357430361400226520ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import os import unittest import pymongo from pymongo import CursorType import pymongo.mongo_client from bson import CodecOptions from mockupdb import OpQuery from pymongo import ReadPreference, WriteConcern from pymongo.errors import ConfigurationError, OperationFailure from pymongo.errors import ConnectionFailure from tornado import gen from tornado.concurrent import Future from tornado.testing import gen_test import motor import test from test import SkipTest from test.test_environment import db_user, db_password, env from test.tornado_tests import (MotorMockServerTest, MotorTest, remove_all_users) from test.utils import one, get_primary_pool class MotorClientTest(MotorTest): @gen_test def test_client_lazy_connect(self): yield 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'}) yield [future0, future1] self.assertEqual(2, (yield collection.count_documents({'foo': 'bar'}))) cx.close() @gen_test 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) yield 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): yield 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 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): yield client.admin.command('ismaster') @gen_test(timeout=30) 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): yield client.admin.command('ismaster') @gen_test 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.max_pool_size, 100) cx.close() @gen_test(timeout=30) def test_drop_database(self): # Make sure we can pass a MotorDatabase instance to drop_database db = self.cx.test_drop_database yield db.test_collection.insert_one({}) names = yield self.cx.list_database_names() self.assertTrue('test_drop_database' in names) yield self.cx.drop_database(db) names = yield self.cx.list_database_names() self.assertFalse('test_drop_database' in names) @gen_test 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. yield 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): yield client.db.collection.find_one() client = self.motor_client( 'mongodb://mike:password@%s:%d/%s' % (env.host, env.port, db.name)) yield 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 def test_list_databases(self): yield self.collection.insert_one({}) cursor = yield self.cx.list_databases() self.assertIsInstance(cursor, motor.motor_tornado.MotorCommandCursor) # Make sure the cursor works, by searching for "local" database. while (yield 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 def test_list_database_names(self): yield self.collection.insert_one({}) names = yield self.cx.list_database_names() self.assertIsInstance(names, list) self.assertIn(self.collection.database.name, names) class MotorClientTimeoutTest(MotorMockServerTest): @gen_test 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: yield 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) @gen.coroutine 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) yield client.admin.command('ismaster') pool = get_primary_pool(client) sock_info = one(pool.sockets) cursor = client.db.collection.find(cursor_type=CursorType.EXHAUST) # With Tornado, simply accessing fetch_next starts the fetch. fetch_next = cursor.fetch_next request = yield self.run_thread(server.receives, OpQuery) request.fail() with self.assertRaises(pymongo.errors.OperationFailure): yield fetch_next self.assertFalse(sock_info.closed) self.assertEqual(sock_info, one(pool.sockets)) @gen_test def test_exhaust_query_server_error_standalone(self): yield self._test_exhaust_query_server_error(rs=False) @gen_test def test_exhaust_query_server_error_rs(self): yield self._test_exhaust_query_server_error(rs=True) @gen.coroutine 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) yield client.admin.command('ismaster') pool = get_primary_pool(client) pool._check_interval_seconds = None # Never check. sock_info = one(pool.sockets) cursor = client.db.collection.find(cursor_type=CursorType.EXHAUST) # With Tornado, simply accessing fetch_next starts the fetch. fetch_next = cursor.fetch_next request = yield self.run_thread(server.receives, OpQuery) request.hangs_up() with self.assertRaises(pymongo.errors.ConnectionFailure): yield fetch_next self.assertTrue(sock_info.closed) del cursor self.assertNotIn(sock_info, pool.sockets) @gen_test def test_exhaust_query_network_error_standalone(self): yield self._test_exhaust_query_network_error(rs=False) @gen_test def test_exhaust_query_network_error_rs(self): yield self._test_exhaust_query_network_error(rs=True) class MotorClientHandshakeTest(MotorMockServerTest): @gen_test def test_handshake(self): server = self.server() client = motor.MotorClient(server.uri, connectTimeoutMS=100, serverSelectionTimeoutMS=100) # Trigger connection. future = client.db.command('ping') ismaster = yield 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: yield future except Exception: pass if __name__ == '__main__': unittest.main() motor-2.1.0/test/tornado_tests/test_motor_collection.py000066400000000000000000000271351357430361400235230ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import sys import traceback import unittest import bson from bson import CodecOptions from bson.binary import JAVA_LEGACY from pymongo import ReadPreference, WriteConcern from pymongo.read_concern import ReadConcern from pymongo.read_preferences import Secondary from pymongo.errors import BulkWriteError, DuplicateKeyError, OperationFailure from tornado import gen from tornado.concurrent import Future from tornado.testing import gen_test import motor import motor.motor_tornado import test from test import SkipTest from test.tornado_tests import MotorTest, skip_if_mongos from test.utils import ignore_deprecations class MotorCollectionTest(MotorTest): @gen_test 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) yield collection.insert_one({'_id': 1}) doc = yield 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 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): yield coll.delete_many({}) result = yield coll.insert_one({'_id': 'xyzzy'}) self.assertEqual('xyzzy', result.inserted_id) result = yield coll.find_one({'_id': 'xyzzy'}) self.assertEqual(result['_id'], 'xyzzy') yield coll.delete_many({}) self.assertEqual(None, (yield 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 def test_update(self): yield self.collection.insert_one({'_id': 1}) result = yield self.collection.update_one( {'_id': 1}, {'$set': {'foo': 'bar'}}) self.assertIsNone(result.upserted_id) self.assertEqual(1, result.modified_count) @ignore_deprecations @gen_test def test_update_bad(self): # Violate a unique index, make sure we handle error well coll = self.db.unique_collection yield coll.create_index('s', unique=True) try: yield coll.insert_many([{'s': 1}, {'s': 2}]) with self.assertRaises(DuplicateKeyError): yield coll.update_one({'s': 2}, {'$set': {'s': 1}}) finally: yield coll.drop() @gen_test def test_insert_one(self): collection = self.collection result = yield collection.insert_one({'_id': 201}) self.assertEqual(201, result.inserted_id) @ignore_deprecations @gen_test def test_insert_many_one_bad(self): collection = self.collection yield collection.insert_one({'_id': 2}) # Violate a unique index in one of many updates, handle error. with self.assertRaises(BulkWriteError): yield collection.insert_many([ {'_id': 1}, {'_id': 2}, # Already exists {'_id': 3}]) # First insert should have succeeded, but not second or third. self.assertEqual( set([1, 2]), set((yield collection.distinct('_id')))) @gen_test def test_delete_one(self): # Remove a document twice, check that we get a success responses # and n = 0 for the second time. yield self.collection.insert_one({'_id': 1}) result = yield 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 = yield 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 def test_unacknowledged_insert(self): # Test that unacknowledged inserts complete eventually. coll = self.db.test_unacknowledged_insert yield coll.with_options( write_concern=WriteConcern(0)).insert_one({'_id': 1}) # The insert is eventually executed. while not (yield coll.count_documents({})): yield gen.sleep(0.1) @gen_test def test_unacknowledged_update(self): # Test that unacknowledged updates complete eventually. coll = self.collection yield coll.insert_one({'_id': 1}) yield coll.with_options(write_concern=WriteConcern(0)).update_one( {'_id': 1}, {'$set': {'a': 1}}) while not (yield coll.find_one({'a': 1})): yield gen.sleep(0.1) @gen_test def test_map_reduce(self): # Count number of documents with even and odd _id yield self.make_test_data() expected_result = [{'_id': 0, 'value': 100}, {'_id': 1, 'value': 100}] map_fn = bson.Code('function map() { emit(this._id % 2, 1); }') reduce_fn = bson.Code(''' function reduce(key, values) { r = 0; values.forEach(function(value) { r += value; }); return r; }''') yield self.db.tmp_mr.drop() # First do a standard mapreduce, should return MotorCollection collection = self.collection tmp_mr = yield collection.map_reduce(map_fn, reduce_fn, 'tmp_mr') self.assertTrue( isinstance(tmp_mr, motor.MotorCollection), 'map_reduce should return MotorCollection, not %s' % tmp_mr) result = yield tmp_mr.find().sort([('_id', 1)]).to_list(length=1000) self.assertEqual(expected_result, result) # Standard mapreduce with full response yield self.db.tmp_mr.drop() response = yield collection.map_reduce( map_fn, reduce_fn, 'tmp_mr', full_response=True) self.assertTrue( isinstance(response, dict), 'map_reduce should return dict, not %s' % response) self.assertEqual('tmp_mr', response['result']) result = yield tmp_mr.find().sort([('_id', 1)]).to_list(length=1000) self.assertEqual(expected_result, result) # Inline mapreduce yield self.db.tmp_mr.drop() result = yield collection.inline_map_reduce( map_fn, reduce_fn) result.sort(key=lambda doc: doc['_id']) self.assertEqual(expected_result, result) @ignore_deprecations @gen_test def test_indexes(self): test_collection = self.collection # Create an index idx_name = yield test_collection.create_index([('foo', 1)]) index_info = yield test_collection.index_information() self.assertEqual([('foo', 1)], index_info[idx_name]['key']) # Don't test drop_index or drop_indexes -- Synchro tests them @gen.coroutine def _make_test_data(self, n): yield self.db.drop_collection("test") yield self.db.test.insert_many([{'_id': i} for i in range(n)]) expected_sum = sum(range(n)) raise gen.Return(expected_sum) 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) 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 = yield self._make_test_data(collection_size) cursor = db.test.aggregate(self.pipeline) docs = yield cursor.to_list(collection_size) self.assertAllDocs(expected_sum, docs) @gen_test def test_aggregation_cursor_exc_info(self): if sys.version_info < (3,): raise SkipTest("Requires Python 3") yield self._make_test_data(200) cursor = self.db.test.aggregate(self.pipeline) yield cursor.to_list(length=10) yield self.db.test.drop() try: yield 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) 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) if __name__ == '__main__': unittest.main() motor-2.1.0/test/tornado_tests/test_motor_core.py000066400000000000000000000127371357430361400223220ustar00rootroot00000000000000# 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. from __future__ import unicode_literals """Validate list of PyMongo attributes wrapped by Motor.""" from tornado.testing import gen_test, unittest from gridfs import GridFSBucket, GridIn from motor import MotorGridFSBucket, MotorGridIn from motor.core import PY35 from test import env from test.tornado_tests import MotorTest 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([ 'close_cursor', 'database_names', 'is_locked', 'set_cursor_manager', 'kill_cursors']).union(pymongo_only) if PY35: pymongo_client_session_only = set([]) else: pymongo_client_session_only = set(['with_transaction']) pymongo_database_only = set([ 'add_user', 'collection_names', 'remove_user', 'system_js', 'last_status', 'reset_error_history', 'eval', 'add_son_manipulator', 'logout', 'error', 'authenticate', 'previous_error']).union(pymongo_only) pymongo_collection_only = set([ 'count', 'ensure_index', 'group', 'initialize_ordered_bulk_op', 'initialize_unordered_bulk_op', 'save', 'remove', 'insert', 'update', 'find_and_modify', 'parallel_scan']).union(pymongo_only) motor_cursor_only = set([ 'fetch_next', 'to_list', 'each', 'started', 'next_object', 'closed']).union(motor_only) pymongo_cursor_only = set([ 'count', 'retrieved']).union(pymongo_only) class MotorCoreTest(MotorTest): def test_client_attrs(self): self.assertEqual( attrs(env.sync_cx) - pymongo_client_only, attrs(self.cx) - motor_client_only) @unittest.skip("Skip until MOTOR-444 is complete") @env.require_version_min(3, 6) @env.require_replica_set @gen_test def test_client_session_attrs(self): self.assertEqual( attrs(env.sync_cx.start_session()) - pymongo_client_session_only, attrs((yield 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(MotorCoreTestGridFS, self).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(MotorCoreTestGridFS, self).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) self.assertEqual( attrs(GridIn(env.sync_cx.test.fs)), attrs(MotorGridIn(self.cx.test.fs)) - motor_gridin_only) @gen_test def test_gridout_attrs(self): motor_gridout_only = set([ 'open', 'stream_to_handler' ]).union(motor_only) fs = MotorGridFSBucket(self.cx.test) motor_gridout = yield fs.open_download_stream(1) self.assertEqual( attrs(self.sync_fs.open_download_stream(1)), 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-2.1.0/test/tornado_tests/test_motor_cursor.py000066400000000000000000000541451357430361400227060ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import sys import traceback import unittest import warnings from functools import partial import bson import pymongo from tornado import gen from tornado.concurrent import Future from tornado.testing import gen_test from pymongo import CursorType from pymongo.collation import Collation from pymongo.errors import InvalidOperation, ExecutionTimeout from pymongo.errors import OperationFailure import motor import motor.motor_tornado from test import SkipTest, env from test.tornado_tests import (get_command_line, MotorTest, MotorMockServerTest, server_is_mongos) from test.utils import one, safe_get, get_primary_pool, TestListener 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 def test_count(self): yield self.make_test_data() coll = self.collection self.assertEqual(100, (yield coll.count_documents({'_id': {'$gt': 99}}))) @gen_test def test_fetch_next(self): yield 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 (yield 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, (yield cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) self.assertEqual(0, cursor.cursor_id) self.assertEqual(200, i) @gen_test def test_fetch_next_delete(self): if sys.version_info < (3, 4): raise SkipTest("requires Python 3.4") 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 = yield 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. yield gen.moment request = yield self.run_thread(server.receives, "killCursors", "coll") request.ok() @gen_test 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, (yield cursor.fetch_next)) self.assertEqual(None, cursor.next_object()) # Now cursor knows it's exhausted self.assertEqual(0, cursor.cursor_id) @gen_test def test_fetch_next_is_idempotent(self): # Subsequent calls to fetch_next don't do anything yield self.make_test_data() coll = self.collection cursor = coll.find() self.assertEqual(None, cursor.cursor_id) yield cursor.fetch_next self.assertTrue(cursor.cursor_id) self.assertEqual(101, cursor._buffer_size()) yield cursor.fetch_next # Does nothing self.assertEqual(101, cursor._buffer_size()) yield cursor.close() @gen_test def test_fetch_next_exception(self): coll = self.collection cursor = coll.find() cursor.delegate._Cursor__id = 1234 # Not valid on server with self.assertRaises(OperationFailure): yield cursor.fetch_next # Avoid the cursor trying to close itself when it goes out of scope cursor.delegate._Cursor__id = None @gen_test 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) def test_each(self): yield 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) yield future expected = [{'_id': i} for i in range(200)] self.assertEqual(expected, results) @gen_test def test_to_list_argument_checking(self): # We need more than 10 documents so the cursor stays alive. yield self.make_test_data() coll = self.collection cursor = coll.find() with self.assertRaises(ValueError): yield cursor.to_list(-1) with self.assertRaises(TypeError): yield cursor.to_list('foo') @gen_test def test_to_list_with_length(self): yield self.make_test_data() coll = self.collection cursor = coll.find().sort('_id') self.assertEqual([], (yield cursor.to_list(0))) def expected(start, stop): return [{'_id': i} for i in range(start, stop)] self.assertEqual(expected(0, 10), (yield cursor.to_list(10))) self.assertEqual(expected(10, 100), (yield 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), (yield cursor.to_list(1))) self.assertEqual(expected(101, 102), (yield cursor.to_list(1))) self.assertEqual(expected(102, 103), (yield cursor.to_list(1))) self.assertEqual([], (yield cursor.to_list(0))) self.assertEqual(expected(103, 105), (yield cursor.to_list(2))) # Only 95 docs left, make sure length=100 doesn't error or hang self.assertEqual(expected(105, 200), (yield cursor.to_list(100))) self.assertEqual(0, cursor.cursor_id) # Nothing left. self.assertEqual([], (yield cursor.to_list(100))) @gen_test def test_to_list_exc_info(self): if sys.version_info < (3,): raise SkipTest("Requires Python 3") yield self.make_test_data() coll = self.collection cursor = coll.find() yield cursor.to_list(length=10) yield self.collection.drop() try: yield 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 def test_to_list_with_length_of_none(self): yield self.make_test_data() collection = self.collection cursor = collection.find() docs = yield cursor.to_list(None) # Unlimited. count = yield collection.count_documents({}) self.assertEqual(count, len(docs)) @gen_test 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): yield cursor.to_list(10) @env.require_version_min(3, 4) @gen_test def test_to_list_with_chained_collation(self): yield 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 = yield cursor.to_list(length=1000) self.assertEqual(expected, result) @gen_test 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 = yield self.run_thread(server.receives, "find", "coll") request.replies({"cursor": { "id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) self.assertTrue((yield fetch_next)) close_future = cursor.close() request = yield self.run_thread(server.receives, "killCursors", "coll") request.ok() yield close_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((yield cursor.fetch_next)) self.assertFalse(cursor.alive) @gen_test def test_each_cancel(self): yield 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: pass results.append(result) else: # Complete future.set_result(None) cursor = collection.find() cursor.each(cancel) yield future self.assertEqual((yield collection.count_documents({})), len(results)) @gen_test def test_rewind(self): yield self.collection.insert_many([{}, {}, {}]) cursor = self.collection.find().limit(2) count = 0 while (yield cursor.fetch_next): cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while (yield cursor.fetch_next): cursor.next_object() count += 1 self.assertEqual(2, count) cursor.rewind() count = 0 while (yield cursor.fetch_next): cursor.next_object() break cursor.rewind() while (yield cursor.fetch_next): cursor.next_object() count += 1 self.assertEqual(2, count) self.assertEqual(cursor, cursor.rewind()) @gen_test def test_cursor_del(self): if sys.version_info < (3, 4): raise SkipTest("requires Python 3.4") 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 = yield self.run_thread(server.receives, "find", "coll") request.replies({"cursor": { "id": 123, "ns": "db.coll", "firstBatch": [{"_id": 1}]}}) yield 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. yield gen.sleep(0.1) request = yield self.run_thread(server.receives, "killCursors", "coll") request.ok() @gen_test def test_exhaust(self): if sys.version_info < (3, 4): raise SkipTest("requires Python 3.4") if (yield 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) yield self.db.drop_collection("test") # Insert enough documents to require more than one batch. yield self.db.test.insert_many([{} for _ in range(150)]) client = self.motor_client(maxPoolSize=1) # Ensure a pool. yield client.db.collection.find_one() socks = get_primary_pool(client).sockets # Make sure the socket is returned after exhaustion. cur = client[self.db.name].test.find(cursor_type=CursorType.EXHAUST) has_next = yield cur.fetch_next self.assertTrue(has_next) self.assertEqual(0, len(socks)) while (yield cur.fetch_next): cur.next_object() self.assertEqual(1, len(socks)) # Same as previous but with to_list instead of next_object. docs = yield client[self.db.name].test.find( cursor_type=CursorType.EXHAUST).to_list(None) self.assertEqual(1, len(socks)) self.assertEqual( (yield 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. sock = one(socks) cur = client[self.db.name].test.find( cursor_type=CursorType.EXHAUST).batch_size(1) has_next = yield cur.fetch_next self.assertTrue(has_next) self.assertEqual(0, len(socks)) if 'PyPy' in sys.version: # Don't wait for GC or use gc.collect(), it's unreliable. yield cur.close() del cur yield gen.sleep(0.1) # The exhaust cursor's socket was discarded, although another may # already have been opened to send OP_KILLCURSORS. self.assertNotIn(sock, socks) self.assertTrue(sock.closed) def test_iter(self): # Iteration should be prohibited. with self.assertRaises(TypeError): for _ in self.db.test.find(): pass @gen_test def test_close_with_docs_in_batch(self): # MOTOR-67 Killed cursor with docs batched is "alive", don't kill again. yield self.make_test_data() # Ensure multiple batches. cursor = self.collection.find() yield cursor.fetch_next yield 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 def test_aggregate_batch_size(self): listener = TestListener() cx = self.motor_client(event_listeners=[listener]) c = cx.motor_test.collection yield c.delete_many({}) yield 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 (yield 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 def test_raw_batches(self): c = self.collection yield c.delete_many({}) yield 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) yield cursor.fetch_next batch = cursor.next_object() self.assertEqual([{'_id': 0}, {'_id': 1}], bson.decode_all(batch)) lst = yield method().batch_size(2).to_list(length=1) self.assertEqual([{'_id': 0}, {'_id': 1}], bson.decode_all(lst[0])) class MotorCursorMaxTimeMSTest(MotorTest): def setUp(self): super(MotorCursorMaxTimeMSTest, self).setUp() self.io_loop.run_sync(self.maybe_skip) def tearDown(self): self.io_loop.run_sync(self.disable_timeout) super(MotorCursorMaxTimeMSTest, self).tearDown() @gen.coroutine def maybe_skip(self): if (yield server_is_mongos(self.cx)): raise SkipTest("mongos has no maxTimeAlwaysTimeOut fail point") cmdline = yield 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") @gen.coroutine def enable_timeout(self): yield self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="alwaysOn") @gen.coroutine def disable_timeout(self): yield self.cx.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") @gen_test def test_max_time_ms_query(self): # Cursor parses server timeout error in response to initial query. yield self.enable_timeout() cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): yield cursor.fetch_next cursor = self.collection.find().max_time_ms(100000) with self.assertRaises(ExecutionTimeout): yield cursor.to_list(10) with self.assertRaises(ExecutionTimeout): yield self.collection.find_one(max_time_ms=100000) @gen_test(timeout=60) def test_max_time_ms_getmore(self): # Cursor handles server timeout during getmore, also. yield self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) yield cursor.fetch_next cursor.next_object() # Test getmore timeout. yield self.enable_timeout() with self.assertRaises(ExecutionTimeout): while (yield cursor.fetch_next): cursor.next_object() yield cursor.close() # Send another initial query. yield self.disable_timeout() cursor = self.collection.find().max_time_ms(100000) yield cursor.fetch_next cursor.next_object() # Test getmore timeout. yield self.enable_timeout() with self.assertRaises(ExecutionTimeout): yield cursor.to_list(None) # Avoid 'IOLoop is closing' warning. yield cursor.close() finally: # Cleanup. yield self.disable_timeout() yield self.collection.delete_many({}) @gen_test def test_max_time_ms_each_query(self): # Cursor.each() handles server timeout during initial query. yield 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) yield future @gen_test(timeout=30) def test_max_time_ms_each_getmore(self): # Cursor.each() handles server timeout during getmore. yield self.collection.insert_many({} for _ in range(200)) try: # Send initial query. cursor = self.collection.find().max_time_ms(100000) yield 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) yield self.enable_timeout() with self.assertRaises(ExecutionTimeout): cursor.each(callback) yield future yield cursor.close() finally: # Cleanup. yield self.disable_timeout() yield self.collection.delete_many({}) if __name__ == '__main__': unittest.main() motor-2.1.0/test/tornado_tests/test_motor_database.py000066400000000000000000000150771357430361400231360ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import unittest import pymongo.database from bson import CodecOptions from bson.binary import JAVA_LEGACY from pymongo import ReadPreference, WriteConcern from pymongo.read_preferences import Secondary from pymongo.errors import OperationFailure, CollectionInvalid from tornado import gen from tornado.testing import gen_test import motor from test import env from test.tornado_tests import MotorTest class MotorDatabaseTest(MotorTest): @gen_test 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) yield db.test_collection.insert_one({'_id': 1}) doc = yield 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 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 = yield cursor.to_list(10) self.assertEqual(expected, docs) @gen_test def test_command(self): result = yield self.cx.admin.command("buildinfo") # Make sure we got some sane result or other. self.assertEqual(1, result['ok']) @gen_test def test_create_collection(self): # Test creating collection, return val is wrapped in MotorCollection, # creating it again raises CollectionInvalid. db = self.db yield db.drop_collection('test_collection2') collection = yield db.create_collection('test_collection2') self.assertTrue(isinstance(collection, motor.MotorCollection)) self.assertTrue( 'test_collection2' in (yield db.list_collection_names())) with self.assertRaises(CollectionInvalid): yield db.create_collection('test_collection2') yield db.drop_collection('test_collection2') # Test creating capped collection collection = yield db.create_collection( 'test_capped', capped=True, size=4096) self.assertTrue(isinstance(collection, motor.MotorCollection)) self.assertEqual( {"capped": True, 'size': 4096}, (yield db.test_capped.options())) yield db.drop_collection('test_capped') @gen_test def test_drop_collection(self): # Make sure we can pass a MotorCollection instance to drop_collection db = self.db collection = db.test_drop_collection yield collection.insert_one({}) names = yield db.list_collection_names() self.assertTrue('test_drop_collection' in names) yield db.drop_collection(collection) names = yield db.list_collection_names() self.assertFalse('test_drop_collection' in names) @gen_test def test_validate_collection(self): db = self.db with self.assertRaises(TypeError): yield db.validate_collection(5) with self.assertRaises(TypeError): yield db.validate_collection(None) with self.assertRaises(OperationFailure): yield db.validate_collection("test.doesnotexist") with self.assertRaises(OperationFailure): yield db.validate_collection(db.test.doesnotexist) yield db.test.insert_one({"dummy": "object"}) self.assertTrue((yield db.validate_collection("test"))) self.assertTrue((yield 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) if __name__ == '__main__': unittest.main() motor-2.1.0/test/tornado_tests/test_motor_grid_file.py000066400000000000000000000311331357430361400233050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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 __future__ import unicode_literals """Test GridFS with Motor, an asynchronous driver for MongoDB and Tornado.""" import datetime import sys import traceback import unittest from bson.objectid import ObjectId from gridfs.errors import NoFile from tornado import gen from tornado.testing import gen_test from pymongo.errors import InvalidOperation import motor from test import MockRequestHandler, SkipTest from test.tornado_tests import MotorTest class MotorGridFileTest(MotorTest): @gen.coroutine def _reset(self): yield self.db.drop_collection("fs.files") yield self.db.drop_collection("fs.chunks") yield self.db.drop_collection("alt.files") yield self.db.drop_collection("alt.chunks") def tearDown(self): self.io_loop.run_sync(self._reset) super(MotorGridFileTest, self).tearDown() @gen_test def test_attributes(self): f = motor.MotorGridIn( self.db.fs, filename="test", foo="bar", content_type="text") yield 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', 'md5') for attr_name in attr_names: self.assertRaises(InvalidOperation, getattr, g, attr_name) yield g.open() for attr_name in attr_names: getattr(g, attr_name) @gen_test def test_iteration(self): fs = motor.MotorGridFSBucket(self.db) _id = yield fs.upload_from_stream('filename', b'foo') g = motor.MotorGridOut(self.db.fs, _id) # Iteration is prohibited. self.assertRaises(TypeError, iter, g) @gen_test def test_basic(self): f = motor.MotorGridIn(self.db.fs, filename="test") yield f.write(b"hello world") yield f.close() self.assertEqual(1, (yield self.db.fs.files.count_documents({}))) self.assertEqual(1, (yield self.db.fs.chunks.count_documents({}))) g = motor.MotorGridOut(self.db.fs, f._id) self.assertEqual(b"hello world", (yield g.read())) f = motor.MotorGridIn(self.db.fs, filename="test") yield f.close() self.assertEqual(2, (yield self.db.fs.files.count_documents({}))) self.assertEqual(1, (yield self.db.fs.chunks.count_documents({}))) g = motor.MotorGridOut(self.db.fs, f._id) self.assertEqual(b"", (yield g.read())) @gen_test def test_readchunk(self): in_data = b'a' * 10 f = motor.MotorGridIn(self.db.fs, chunkSize=3) yield f.write(in_data) yield f.close() g = motor.MotorGridOut(self.db.fs, f._id) # This is starting to look like Lisp. self.assertEqual(3, len((yield g.readchunk()))) self.assertEqual(2, len((yield g.read(2)))) self.assertEqual(1, len((yield g.readchunk()))) self.assertEqual(3, len((yield g.read(3)))) self.assertEqual(1, len((yield g.readchunk()))) self.assertEqual(0, len((yield g.readchunk()))) @gen_test def test_gridout_open_exc_info(self): if sys.version_info < (3, ): raise SkipTest("Requires Python 3") g = motor.MotorGridOut(self.db.fs, "_id that doesn't exist") try: yield 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 def test_alternate_collection(self): yield self.db.alt.files.delete_many({}) yield self.db.alt.chunks.delete_many({}) f = motor.MotorGridIn(self.db.alt) yield f.write(b"hello world") yield f.close() self.assertEqual(1, (yield self.db.alt.files.count_documents({}))) self.assertEqual(1, (yield self.db.alt.chunks.count_documents({}))) g = motor.MotorGridOut(self.db.alt, f._id) self.assertEqual(b"hello world", (yield g.read())) # test that md5 still works... self.assertEqual("5eb63bbbe01eeed093cb22bb8f5acdc3", g.md5) @gen_test 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 yield a.set("filename", "my_file") self.assertEqual("my_file", a.filename) self.assertEqual(None, a.content_type) yield 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") yield a.set("aliases", ["foo"]) self.assertEqual(["foo"], a.aliases) self.assertRaises(AttributeError, getattr, a, "metadata") yield a.set("metadata", {"foo": 1}) self.assertEqual({"foo": 1}, a.metadata) self.assertRaises(AttributeError, setattr, a, "md5", 5) yield 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) self.assertEqual("d41d8cd98f00b204e9800998ecf8427e", a.md5) self.assertRaises(AttributeError, setattr, a, "md5", 5) @gen_test 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 def test_grid_out_default_opts(self): self.assertRaises(TypeError, motor.MotorGridOut, "foo") gout = motor.MotorGridOut(self.db.fs, 5) with self.assertRaises(NoFile): yield gout.open() a = motor.MotorGridIn(self.db.fs) yield a.close() b = yield 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) self.assertEqual("d41d8cd98f00b204e9800998ecf8427e", b.md5) @gen_test 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") yield one.write(b"hello world") yield one.close() two = yield 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) self.assertEqual("5eb63bbbe01eeed093cb22bb8f5acdc3", two.md5) @gen_test def test_grid_out_file_document(self): one = motor.MotorGridIn(self.db.fs) yield one.write(b"foo bar") yield one.close() file_document = yield self.db.fs.files.find_one() two = motor.MotorGridOut( self.db.fs, file_document=file_document) self.assertEqual(b"foo bar", (yield two.read())) file_document = yield self.db.fs.files.find_one() three = motor.MotorGridOut(self.db.fs, 5, file_document) self.assertEqual(b"foo bar", (yield three.read())) gridout = motor.MotorGridOut(self.db.fs, file_document={}) with self.assertRaises(NoFile): yield gridout.open() @gen_test def test_write_file_like(self): one = motor.MotorGridIn(self.db.fs) yield one.write(b"hello world") yield one.close() two = motor.MotorGridOut(self.db.fs, one._id) three = motor.MotorGridIn(self.db.fs) yield three.write(two) yield three.close() four = motor.MotorGridOut(self.db.fs, three._id) self.assertEqual(b"hello world", (yield four.read())) @gen_test 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") yield 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) yield f.set("bar", "a") yield f.set("baz", "b") self.assertRaises(AttributeError, setattr, f, "upload_date", 5) g = yield motor.MotorGridOut(self.db.fs, f._id).open() self.assertEqual("a", g.bar) self.assertEqual("b", g.baz) @gen_test def test_stream_to_handler(self): fs = motor.MotorGridFSBucket(self.db) for content_length in (0, 1, 100, 100 * 1000): _id = yield fs.upload_from_stream('filename', b'a' * content_length) gridout = yield fs.open_download_stream(_id) handler = MockRequestHandler() yield gridout.stream_to_handler(handler) self.assertEqual(content_length, handler.n_written) yield fs.delete(_id) if __name__ == "__main__": unittest.main() motor-2.1.0/test/tornado_tests/test_motor_gridfsbucket.py000066400000000000000000000045701357430361400240420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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. from __future__ import unicode_literals """Test MotorGridFSBucket.""" from io import BytesIO from gridfs.errors import NoFile from tornado import gen from tornado.testing import gen_test import motor from test.tornado_tests import MotorTest class MotorGridFSBucketTest(MotorTest): @gen.coroutine def _reset(self): yield self.db.drop_collection("fs.files") yield self.db.drop_collection("fs.chunks") yield self.db.drop_collection("alt.files") yield self.db.drop_collection("alt.chunks") def setUp(self): super(MotorGridFSBucketTest, self).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(MotorGridFSBucketTest, self).tearDown() @gen_test def test_basic(self): oid = yield self.bucket.upload_from_stream("test_filename", b"hello world") gout = yield self.bucket.open_download_stream(oid) self.assertEqual(b"hello world", (yield gout.read())) self.assertEqual(1, (yield self.db.fs.files.count_documents({}))) self.assertEqual(1, (yield self.db.fs.chunks.count_documents({}))) yield self.bucket.delete(oid) with self.assertRaises(NoFile): yield self.bucket.open_download_stream(oid) self.assertEqual(0, (yield self.db.fs.files.count_documents({}))) self.assertEqual(0, (yield self.db.fs.chunks.count_documents({}))) gin = self.bucket.open_upload_stream("test_filename") yield gin.write(b"hello world") yield gin.close() dst = BytesIO() yield self.bucket.download_to_stream(gin._id, dst) self.assertEqual(b"hello world", dst.getvalue()) motor-2.1.0/test/tornado_tests/test_motor_ipv6.py000066400000000000000000000041051357430361400222440ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import unittest from pymongo import MongoClient from pymongo.errors import ConnectionFailure from tornado.testing import gen_test import motor import test from test import SkipTest from test.test_environment import connected, db_user, db_password, env from test.tornado_tests import MotorTest class MotorIPv6Test(MotorTest): @gen_test 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 yield collection.insert_one({"dummy": "object"}) self.assertTrue((yield collection.find_one({"dummy": "object"}))) if __name__ == '__main__': unittest.main() motor-2.1.0/test/tornado_tests/test_motor_replica_set.py000066400000000000000000000102131357430361400236470ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test replica set MotorClient.""" import unittest import pymongo import pymongo.auth import pymongo.errors import pymongo.mongo_replica_set_client from tornado import gen from tornado.testing import gen_test import motor import motor.core import test from test import SkipTest from test.test_environment import db_user, db_password, env from test.tornado_tests import MotorReplicaSetTestBase, MotorTest from test.utils import one, get_primary_pool from motor.motor_py2_compat import text_type class MotorReplicaSetTest(MotorReplicaSetTestBase): def test_io_loop(self): with self.assertRaises(TypeError): motor.MotorClient(test.env.rs_uri, io_loop='foo') @gen_test 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): yield client.admin.command('ismaster') @gen_test def test_auth_network_error(self): if not test.env.auth: raise SkipTest('Authentication is not enabled on server') # Make sure there's no semaphore leak if we get a network error # when authenticating a new socket with cached credentials. # Get a client with one socket so we detect if it's leaked. c = self.motor_rsc(maxPoolSize=1, waitQueueTimeoutMS=1, retryReads=False) yield c.admin.command('ismaster') # Simulate an authenticate() call on a different socket. credentials = pymongo.auth._build_credentials_tuple( 'DEFAULT', 'admin', text_type(db_user), text_type(db_password), {}, 'admin') c.delegate._cache_credentials('test', credentials, connect=False) # Cause a network error on the actual socket. pool = get_primary_pool(c) socket_info = one(pool.sockets) socket_info.sock.close() # In __check_auth, the client authenticates its socket with the # new credential, but gets a socket.error. Should be reraised as # AutoReconnect. with self.assertRaises(pymongo.errors.AutoReconnect): yield c.test.collection.find_one() # No semaphore leak, the pool is allowed to make a new socket. yield c.test.collection.find_one() @gen_test 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() yield [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(TestReplicaSetClientAgainstStandalone, self).setUp() if test.env.is_replica_set: raise SkipTest( "Connected to a replica set, not a standalone mongod") @gen_test def test_connect(self): with self.assertRaises(pymongo.errors.ServerSelectionTimeoutError): yield 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-2.1.0/test/tornado_tests/test_motor_session.py000066400000000000000000000200501357430361400230400ustar00rootroot00000000000000# 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. from __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import copy import sys import unittest from pymongo import InsertOne, IndexModel from tornado import gen from pymongo.errors import InvalidOperation from tornado.testing import gen_test from test import SkipTest from test.test_environment import env from test.tornado_tests import MotorTest from test.utils import TestListener, session_ids class MotorSessionTest(MotorTest): @classmethod def setUpClass(cls): super(MotorSessionTest, cls).setUpClass() if not env.sessions_enabled: raise SkipTest("Sessions not supported") @gen.coroutine def _test_ops(self, client, *ops): listener = client.event_listeners()[0][0] for f, args, kw in ops: # Simulate "async with" on all Pythons. s = yield client.start_session() try: listener.results.clear() # In case "f" modifies its inputs. args2 = copy.copy(args) kw2 = copy.copy(kw) kw2['session'] = s yield 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: yield s.end_session() with self.assertRaises(InvalidOperation) as ctx: yield f(*args2, **kw2) self.assertIn("ended session", str(ctx.exception)) # No explicit session. for f, args, kw in ops: listener.results.clear() yield 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 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, [], {}), ] yield self._test_ops(client, *ops) @gen_test(timeout=30) def test_collection(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) yield client.drop_database('motor_test') coll = client.motor_test.test_collection @gen.coroutine def list_indexes(session=None): yield coll.list_indexes(session=session).to_list(length=None) @gen.coroutine def aggregate(session=None): yield coll.aggregate([], session=session).to_list(length=None) # Test some collection methods - the rest are in test_cursor. yield 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 def test_cursor(self): listener = TestListener() client = self.motor_client(event_listeners=[listener]) yield self.make_test_data() coll = client.motor_test.test_collection s = yield client.start_session() # Simulate "async with" on all Pythons. try: listener.results.clear() cursor = coll.find(session=s) yield 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: yield s.end_session() with self.assertRaises(InvalidOperation) as ctx: yield coll.find(session=s).to_list(length=None) self.assertIn("ended session", str(ctx.exception)) # No explicit session. listener.results.clear() cursor = coll.find() yield 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 def test_options(self): s = yield self.cx.start_session() self.assertTrue(s.options.causal_consistency) s = yield self.cx.start_session(False) self.assertFalse(s.options.causal_consistency) s = yield self.cx.start_session(causal_consistency=True) self.assertTrue(s.options.causal_consistency) s = yield self.cx.start_session(causal_consistency=False) self.assertFalse(s.options.causal_consistency) if __name__ == '__main__': unittest.main() motor-2.1.0/test/tornado_tests/test_motor_ssl.py000066400000000000000000000135561357430361400221730ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test Motor, an asynchronous driver for MongoDB and Tornado.""" try: import ssl except ImportError: ssl = None try: # Python 2. from urllib import quote_plus except ImportError: # Python 3. from urllib.parse import quote_plus from pymongo.errors import ConfigurationError, ConnectionFailure from tornado.testing import gen_test import motor import test from test import SkipTest from test.tornado_tests import MotorTest from test.test_environment import CA_PEM, CLIENT_PEM, env # 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(MotorSSLTest, self).setUp() def test_config_ssl(self): self.assertRaises(ValueError, motor.MotorClient, ssl='foo') self.assertRaises(ConfigurationError, motor.MotorClient, ssl=False, ssl_certfile=CLIENT_PEM) self.assertRaises(IOError, motor.MotorClient, ssl_certfile="NoFile") self.assertRaises(TypeError, motor.MotorClient, ssl_certfile=True) self.assertRaises(IOError, motor.MotorClient, ssl_keyfile="NoFile") self.assertRaises(TypeError, motor.MotorClient, ssl_keyfile=True) @gen_test 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, ssl_certfile=CLIENT_PEM, ssl_ca_certs=CA_PEM, io_loop=self.io_loop) yield client.db.collection.find_one() response = yield client.admin.command('ismaster') if 'setName' in response: client = self.motor_rsc(ssl_certfile=CLIENT_PEM, ssl_ca_certs=CA_PEM) yield client.db.collection.find_one() @gen_test 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, ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.io_loop) yield client.db.collection.find_one() response = yield client.admin.command('ismaster') if 'setName' in response: client = motor.MotorClient( env.host, env.port, replicaSet=response['setName'], ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.io_loop) yield client.db.collection.find_one() @gen_test 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, ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_NONE, ssl_ca_certs=CA_PEM, io_loop=self.io_loop) yield client.admin.command('ismaster') @gen_test 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, ssl_certfile=CLIENT_PEM, ssl_ca_certs=CA_PEM, io_loop=self.io_loop) response = yield 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, ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.io_loop) yield 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'], ssl_certfile=CLIENT_PEM, ssl_cert_reqs=ssl.CERT_REQUIRED, ssl_ca_certs=CA_PEM, io_loop=self.io_loop) yield client.db.collection.find_one() motor-2.1.0/test/tornado_tests/test_motor_transaction.py000066400000000000000000000162411357430361400237110ustar00rootroot00000000000000# 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 __future__ import unicode_literals import collections import os import re from bson import json_util from bson.json_util import JSONOptions from pymongo.read_concern import ReadConcern from pymongo.results import (BulkWriteResult, InsertManyResult, InsertOneResult, UpdateResult, DeleteResult) from motor.motor_tornado import (MotorCommandCursor, MotorCursor, MotorLatentCommandCursor) from test.utils import TestListener from test.version import Version """Test Motor, an asynchronous driver for MongoDB and Tornado.""" import unittest from pymongo import (ReadPreference, WriteConcern) from pymongo.errors import ConnectionFailure, OperationFailure from tornado.testing import gen_test from motor import core from test.test_environment import env from test.tornado_tests import MotorTest class PatchSessionTimeout(object): """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']) self.set_fail_point(client, { 'configureFailPoint': 'failCommand', 'mode': 'off'}) if __name__ == '__main__': unittest.main() motor-2.1.0/test/tornado_tests/test_motor_web.py000066400000000000000000000214511357430361400221400ustar00rootroot00000000000000# 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 __future__ import unicode_literals """Test utilities for using Motor with Tornado web applications.""" import datetime import email import hashlib import time import re import unittest import gridfs from tornado.testing import AsyncHTTPTestCase from tornado.web import Application import motor import motor.web import test from test.test_environment import env, CA_PEM, CLIENT_PEM # We're using Tornado's AsyncHTTPTestCase instead of our own MotorTestCase for # the convenience of self.fetch(). class GridFSHandlerTestBase(AsyncHTTPTestCase): def setUp(self): super(GridFSHandlerTestBase, self).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 self.contents_hash = hashlib.md5(self.contents).hexdigest() # Record when we created the file, to check the Last-Modified header self.put_start = datetime.datetime.utcnow().replace(microsecond=0) self.file_id = 'id' self.fs.delete(self.file_id) self.fs.put( self.contents, _id='id', filename='foo', content_type='my type') self.put_end = datetime.datetime.utcnow().replace(microsecond=0) self.assertTrue(self.fs.get_last_version('foo')) def motor_db(self, **kwargs): if env.mongod_started_with_ssl: kwargs.setdefault('ssl_certfile', CLIENT_PEM) kwargs.setdefault('ssl_ca_certs', CA_PEM) kwargs.setdefault('ssl', 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(GridFSHandlerTestBase, self).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'), ('jscr.js', 'javascript'), ]: # '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.assertTrue( response.headers['Content-Type'].lower().endswith( expected_type)) class TZAwareGridFSHandlerTest(GridFSHandlerTestBase): def motor_db(self): return super(TZAwareGridFSHandlerTest, self).motor_db(tz_aware=True) def test_tz_aware(self): now = datetime.datetime.utcnow() 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.utcnow() < datetime.timedelta(seconds=12)) self.assertEqual('fizzledy', response.headers['quux']) if __name__ == '__main__': unittest.main() motor-2.1.0/test/utils.py000066400000000000000000000070771357430361400153640ustar00rootroot00000000000000# 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 __future__ import unicode_literals 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 warnings 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): 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 motor-2.1.0/test/version.py000066400000000000000000000057511357430361400157060ustar00rootroot00000000000000# 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.""" class Version(tuple): """Copied from PyMongo's test.version submodule.""" def __new__(cls, *version): padded_version = cls._padded(version, 4) return super(Version, cls).__new__(cls, tuple(padded_version)) @classmethod def _padded(cls, iter, length, padding=0): l = list(iter) if len(l) < length: for _ in range(length - len(l)): l.append(padding) return l @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-2.1.0/tox.ini000066400000000000000000000061111357430361400141720ustar00rootroot00000000000000# Tox (https://tox.readthedocs.io) is a tool for running tests in multiple # virtualenvs. "pip install tox" and run "tox" from this directory. # Adapted from Tornado's tox.ini. [tox] envlist = # Minimal Tornado 4 compatibility check. tornado4-{py27,py36,py37}, # Tornado 5 supports Python 2.7.9+ and 3.4+. tornado5-{py27,pypy,pypy3,py34,py35,py36,py37}, # Tornado 6 supports Python 3.5+. tornado6-{pypy3,py35,py36,py37,py38}, # Test Tornado on master in a few configurations. tornado_git-{py27,py34,py35,py36,py37}, # Ensure the sphinx build has no errors or warnings. py3-sphinx-docs, # Run the doctests, include examples and tutorial, via Sphinx. py3-sphinx-doctest, # asyncio without Tornado. asyncio-{py34,py35,py36,py37,py38}, # Test PyMongo HEAD, not the latest release. py3-pymongo-master, # Apply PyMongo's test suite to Motor via Synchro. synchro27, synchro37 [testenv] passenv = DB_IP DB_PORT DB_USER DB_PASSWORD CERT_DIR ASYNC_TEST_TIMEOUT basepython = py27,synchro27: {env:PYTHON_BINARY:python2.7} py34: {env:PYTHON_BINARY:python3.4} py35: {env:PYTHON_BINARY:python3.5} py36: {env:PYTHON_BINARY:python3.6} py37,synchro37: {env:PYTHON_BINARY:python3.7} py38: {env:PYTHON_BINARY:python3.8} pypy: {env:PYTHON_BINARY:pypy} pypy3: {env:PYTHON_BINARY:pypy3} # Default Python 3 when we don't care about minor version. py3: {env:PYTHON_BINARY:python3.6} deps = tornado4: tornado>=4,<5 tornado5: tornado>=5,<6 tornado6: tornado>=6,<7 tornado_git: git+https://github.com/tornadoweb/tornado.git {py27,pypy}: futures {py35,py36,py37,py38}: aiohttp sphinx: sphinx sphinx: aiohttp sphinx: git+https://github.com/tornadoweb/tornado.git py3-pymongo-master: git+https://github.com/mongodb/mongo-python-driver.git py3-pymongo-master: tornado>=5,<6 synchro27: tornado>=5,<6 synchro27: nose synchro27: futures synchro37: tornado>=6,<7 synchro37: nose commands = python --version python setup.py test --xunit-output=xunit-results {posargs} [testenv:py3-sphinx-docs] changedir = doc commands = sphinx-build -q -E -W -b html . {envtmpdir}/html {posargs} [testenv:py3-sphinx-doctest] setenv = PYTHONHASHSEED=0 changedir = doc commands = sphinx-build -q -E -b doctest . {envtmpdir}/doctest {posargs} [testenv:synchro27] whitelist_externals = git setenv = PYTHONPATH = {envtmpdir}/mongo-python-driver commands = git clone --depth 1 https://github.com/mongodb/mongo-python-driver.git {envtmpdir}/mongo-python-driver python -m synchro.synchrotest --with-xunit --xunit-file=xunit-synchro-results -v -w {envtmpdir}/mongo-python-driver {posargs} [testenv:synchro37] whitelist_externals = git setenv = PYTHONPATH = {envtmpdir}/mongo-python-driver commands = git clone --depth 1 https://github.com/mongodb/mongo-python-driver.git {envtmpdir}/mongo-python-driver python3 -m synchro.synchrotest --with-xunit --xunit-file=xunit-synchro-results -v -w {envtmpdir}/mongo-python-driver {posargs}