Compare commits
1 Commits
master
...
spec-resyn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b520e39ccc |
@ -1,4 +0,0 @@
|
||||
# do not notify until at least 100 builds have been uploaded from the CI pipeline
|
||||
# you can also set after_n_builds on comments independently
|
||||
comment:
|
||||
after_n_builds: 100
|
||||
@ -38,7 +38,6 @@ post:
|
||||
# Disabled, causing timeouts
|
||||
# - func: "upload working dir"
|
||||
- func: "teardown system"
|
||||
- func: "upload codecov"
|
||||
- func: "upload coverage"
|
||||
- func: "upload mo artifacts"
|
||||
- func: "upload test results"
|
||||
|
||||
@ -239,40 +239,6 @@ functions:
|
||||
working_dir: src
|
||||
type: test
|
||||
|
||||
# Test numpy
|
||||
test numpy:
|
||||
- command: subprocess.exec
|
||||
params:
|
||||
binary: bash
|
||||
args:
|
||||
- .evergreen/just.sh
|
||||
- test-numpy
|
||||
working_dir: src
|
||||
include_expansions_in_env:
|
||||
- TOOLCHAIN_VERSION
|
||||
- COVERAGE
|
||||
type: test
|
||||
|
||||
# Upload coverage codecov
|
||||
upload codecov:
|
||||
- command: subprocess.exec
|
||||
params:
|
||||
binary: bash
|
||||
args:
|
||||
- .evergreen/scripts/upload-codecov.sh
|
||||
working_dir: src
|
||||
include_expansions_in_env:
|
||||
- CODECOV_TOKEN
|
||||
- build_variant
|
||||
- task_name
|
||||
- github_commit
|
||||
- github_pr_number
|
||||
- github_pr_head_branch
|
||||
- github_author
|
||||
- requester
|
||||
- branch_name
|
||||
type: test
|
||||
|
||||
# Upload coverage
|
||||
upload coverage:
|
||||
- command: ec2.assume_role
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -70,34 +70,27 @@ buildvariants:
|
||||
tags: [pr]
|
||||
|
||||
# Aws auth tests
|
||||
- name: auth-aws-rhel8
|
||||
- name: auth-aws-ubuntu-20
|
||||
tasks:
|
||||
- name: .auth-aws
|
||||
display_name: Auth AWS RHEL8
|
||||
display_name: Auth AWS Ubuntu-20
|
||||
run_on:
|
||||
- rhel87-small
|
||||
- ubuntu2004-small
|
||||
tags: []
|
||||
- name: auth-aws-win64
|
||||
tasks:
|
||||
- name: .auth-aws
|
||||
- name: .auth-aws !.auth-aws-ecs
|
||||
display_name: Auth AWS Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- windows-64-vsMulti-small
|
||||
tags: []
|
||||
- name: auth-aws-macos
|
||||
tasks:
|
||||
- name: .auth-aws !.auth-aws-web-identity !.auth-aws-ec2
|
||||
- name: .auth-aws !.auth-aws-web-identity !.auth-aws-ecs !.auth-aws-ec2
|
||||
display_name: Auth AWS macOS
|
||||
run_on:
|
||||
- macos-14
|
||||
tags: [pr]
|
||||
- name: auth-aws-ecs-macos
|
||||
tasks:
|
||||
- name: .auth-aws-ecs
|
||||
display_name: Auth AWS ECS macOS
|
||||
run_on:
|
||||
- ubuntu2404-small
|
||||
tags: [pr]
|
||||
|
||||
# Aws lambda tests
|
||||
- name: faas-lambda
|
||||
@ -140,15 +133,6 @@ buildvariants:
|
||||
- rhel87-small
|
||||
expansions:
|
||||
COMPRESSOR: zstd
|
||||
- name: compression-zstd-ubuntu-22
|
||||
tasks:
|
||||
- name: .test-standard !.server-4.2 !.server-4.4 !.server-5.0 .python-3.14
|
||||
- name: .test-standard !.server-4.2 !.server-4.4 !.server-5.0 .python-3.14t
|
||||
display_name: Compression zstd Ubuntu-22
|
||||
run_on:
|
||||
- ubuntu2204-small
|
||||
expansions:
|
||||
COMPRESSOR: ztsd
|
||||
|
||||
# Coverage report tests
|
||||
- name: coverage-report
|
||||
@ -184,6 +168,7 @@ buildvariants:
|
||||
- name: encryption-rhel8
|
||||
tasks:
|
||||
- name: .test-non-standard
|
||||
- name: .test-min-deps
|
||||
display_name: Encryption RHEL8
|
||||
run_on:
|
||||
- rhel87-small
|
||||
@ -206,7 +191,7 @@ buildvariants:
|
||||
- name: .test-non-standard !.pypy
|
||||
display_name: Encryption Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- windows-64-vsMulti-small
|
||||
batchtime: 1440
|
||||
expansions:
|
||||
TEST_NAME: encryption
|
||||
@ -214,6 +199,7 @@ buildvariants:
|
||||
- name: encryption-crypt_shared-rhel8
|
||||
tasks:
|
||||
- name: .test-non-standard
|
||||
- name: .test-min-deps
|
||||
display_name: Encryption crypt_shared RHEL8
|
||||
run_on:
|
||||
- rhel87-small
|
||||
@ -238,7 +224,7 @@ buildvariants:
|
||||
- name: .test-non-standard !.pypy
|
||||
display_name: Encryption crypt_shared Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- windows-64-vsMulti-small
|
||||
batchtime: 1440
|
||||
expansions:
|
||||
TEST_NAME: encryption
|
||||
@ -277,10 +263,10 @@ buildvariants:
|
||||
AUTH: auth
|
||||
- name: auth-enterprise-win64
|
||||
tasks:
|
||||
- name: .test-standard-auth !.pypy .auth !.free-threaded
|
||||
- name: .test-standard-auth !.pypy .auth
|
||||
display_name: Auth Enterprise Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- windows-64-vsMulti-small
|
||||
expansions:
|
||||
TEST_NAME: enterprise_auth
|
||||
AUTH: auth
|
||||
@ -331,14 +317,6 @@ buildvariants:
|
||||
expansions:
|
||||
TEST_NAME: load_balancer
|
||||
|
||||
# Min support tests
|
||||
- name: min-support-rhel8
|
||||
tasks:
|
||||
- name: .test-min-support
|
||||
display_name: Min Support RHEL8
|
||||
run_on:
|
||||
- rhel87-small
|
||||
|
||||
# Mockupdb tests
|
||||
- name: mockupdb-rhel8
|
||||
tasks:
|
||||
@ -367,8 +345,6 @@ buildvariants:
|
||||
display_name: No C Ext RHEL8
|
||||
run_on:
|
||||
- rhel87-small
|
||||
expansions:
|
||||
NO_EXT: "1"
|
||||
|
||||
# No server tests
|
||||
- name: no-server-rhel8
|
||||
@ -393,7 +369,7 @@ buildvariants:
|
||||
- name: .ocsp-rsa !.ocsp-staple .4.4
|
||||
display_name: OCSP Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- windows-64-vsMulti-small
|
||||
batchtime: 10080
|
||||
- name: ocsp-macos
|
||||
tasks:
|
||||
@ -419,8 +395,6 @@ buildvariants:
|
||||
run_on:
|
||||
- ubuntu2204-small
|
||||
batchtime: 1440
|
||||
expansions:
|
||||
COVERAGE: "1"
|
||||
tags: [pr]
|
||||
- name: auth-oidc-macos
|
||||
tasks:
|
||||
@ -434,7 +408,7 @@ buildvariants:
|
||||
- name: "!.auth_oidc_remote .auth_oidc"
|
||||
display_name: Auth OIDC Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- windows-64-vsMulti-small
|
||||
batchtime: 1440
|
||||
|
||||
# Perf tests
|
||||
@ -473,7 +447,7 @@ buildvariants:
|
||||
- name: .test-standard !.pypy .async .replica_set-noauth-ssl
|
||||
display_name: PyOpenSSL Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- rhel87-small
|
||||
batchtime: 1440
|
||||
expansions:
|
||||
SUB_TEST_NAME: pyopenssl
|
||||
@ -615,10 +589,9 @@ buildvariants:
|
||||
- name: test-win64
|
||||
tasks:
|
||||
- name: .test-standard !.pypy
|
||||
- name: .test-no-orchestration !.pypy
|
||||
display_name: "* Test Win64"
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
- windows-64-vsMulti-small
|
||||
tags: [standard-non-linux]
|
||||
- name: test-win32
|
||||
tasks:
|
||||
@ -639,42 +612,3 @@ buildvariants:
|
||||
- rhel87-small
|
||||
expansions:
|
||||
STORAGE_ENGINE: inmemory
|
||||
|
||||
# Test numpy tests
|
||||
- name: test-numpy-rhel8
|
||||
tasks:
|
||||
- name: .test-numpy
|
||||
display_name: Test Numpy RHEL8
|
||||
run_on:
|
||||
- rhel87-small
|
||||
tags: [binary, vector, pr]
|
||||
- name: test-numpy-macos
|
||||
tasks:
|
||||
- name: .test-numpy
|
||||
display_name: Test Numpy macOS
|
||||
run_on:
|
||||
- macos-14
|
||||
tags: [binary, vector]
|
||||
- name: test-numpy-macos-arm64
|
||||
tasks:
|
||||
- name: .test-numpy
|
||||
display_name: Test Numpy macOS Arm64
|
||||
run_on:
|
||||
- macos-14-arm64
|
||||
tags: [binary, vector]
|
||||
- name: test-numpy-win64
|
||||
tasks:
|
||||
- name: .test-numpy
|
||||
display_name: Test Numpy Win64
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
tags: [binary, vector]
|
||||
- name: test-numpy-win32
|
||||
tasks:
|
||||
- name: .test-numpy
|
||||
display_name: Test Numpy Win32
|
||||
run_on:
|
||||
- windows-64-vsMulti-small
|
||||
expansions:
|
||||
IS_WIN32: "1"
|
||||
tags: [binary, vector]
|
||||
|
||||
@ -41,7 +41,7 @@ rm $PYMONGO/test/index_management/index-rawdata.json
|
||||
rm $PYMONGO/test/collection_management/modifyCollection-*.json
|
||||
|
||||
# PYTHON-5248 - Remove support for MongoDB 4.0
|
||||
find /$PYMONGO/test -type f -name 'pre-42-*.json' -delete
|
||||
find /$PYMONGO /test -type f -name 'pre-42-*.json' -delete
|
||||
|
||||
# PYTHON-3359 - Remove Database and Collection level timeout override
|
||||
rm $PYMONGO/test/csot/override-collection-timeoutMS.json
|
||||
@ -50,7 +50,4 @@ rm $PYMONGO/test/csot/override-database-timeoutMS.json
|
||||
# PYTHON-2943 - Socks5 Proxy Support
|
||||
rm $PYMONGO/test/uri_options/proxy-options.json
|
||||
|
||||
# PYTHON-5517 - Avoid clearing the connection pool when the server connection rate limiter triggers
|
||||
rm $PYMONGO/test/discovery_and_monitoring/unified/backpressure-*.json
|
||||
|
||||
echo "Done removing unimplemented tests"
|
||||
|
||||
@ -94,9 +94,6 @@ do
|
||||
change-streams|change_streams)
|
||||
cpjson change-streams/tests/ change_streams/
|
||||
;;
|
||||
client-backpressure|client_backpressure)
|
||||
cpjson client-backpressure/tests client-backpressure
|
||||
;;
|
||||
client-side-encryption|csfle|fle)
|
||||
cpjson client-side-encryption/tests/ client-side-encryption/spec
|
||||
cpjson client-side-encryption/corpus/ client-side-encryption/corpus
|
||||
|
||||
@ -38,7 +38,6 @@ trap "cleanup_tests" SIGINT ERR
|
||||
|
||||
# Start the test runner.
|
||||
echo "Running tests with UV_PYTHON=${UV_PYTHON:-}..."
|
||||
echo "UV_ARGS=${UV_ARGS}"
|
||||
uv run ${UV_ARGS} --reinstall-package pymongo .evergreen/scripts/run_tests.py "$@"
|
||||
echo "Running tests with UV_PYTHON=${UV_PYTHON:-}... done."
|
||||
|
||||
|
||||
@ -97,8 +97,6 @@ def create_standard_nonlinux_variants() -> list[BuildVariant]:
|
||||
tasks = [
|
||||
f".test-standard !.pypy .server-{version}" for version in get_versions_from("6.0")
|
||||
]
|
||||
if host_name == "win64":
|
||||
tasks.append(".test-no-orchestration !.pypy")
|
||||
host = HOSTS[host_name]
|
||||
tags = ["standard-non-linux"]
|
||||
expansions = dict()
|
||||
@ -130,7 +128,7 @@ def create_encryption_variants() -> list[BuildVariant]:
|
||||
):
|
||||
expansions = get_encryption_expansions(encryption)
|
||||
display_name = get_variant_name(encryption, host, **expansions)
|
||||
tasks = [".test-non-standard"]
|
||||
tasks = [".test-non-standard", ".test-min-deps"]
|
||||
if host != "rhel8":
|
||||
tasks = [".test-non-standard !.pypy"]
|
||||
variant = create_variant(
|
||||
@ -196,22 +194,6 @@ def create_compression_variants():
|
||||
expansions=expansions,
|
||||
)
|
||||
)
|
||||
# Add explicit tests with compression.zstd support on linux.
|
||||
host = HOSTS["ubuntu22"]
|
||||
expansions = dict(COMPRESSOR="ztsd")
|
||||
tasks = [
|
||||
".test-standard !.server-4.2 !.server-4.4 !.server-5.0 .python-3.14",
|
||||
".test-standard !.server-4.2 !.server-4.4 !.server-5.0 .python-3.14t",
|
||||
]
|
||||
display_name = get_variant_name(f"Compression {compressor}", host)
|
||||
variants.append(
|
||||
create_variant(
|
||||
tasks,
|
||||
display_name,
|
||||
host=host,
|
||||
expansions=expansions,
|
||||
)
|
||||
)
|
||||
return variants
|
||||
|
||||
|
||||
@ -225,8 +207,7 @@ def create_enterprise_auth_variants():
|
||||
if host == "macos":
|
||||
tasks = [".test-standard-auth !.pypy .auth !.free-threaded"]
|
||||
if host == "win64":
|
||||
# https://jira.mongodb.org/browse/PYTHON-5704
|
||||
tasks = [".test-standard-auth !.pypy .auth !.free-threaded"]
|
||||
tasks = [".test-standard-auth !.pypy .auth"]
|
||||
variant = create_variant(tasks, display_name, host=host, expansions=expansions)
|
||||
variants.append(variant)
|
||||
return variants
|
||||
@ -323,7 +304,7 @@ def create_no_c_ext_variants():
|
||||
expansions = dict()
|
||||
handle_c_ext(C_EXTS[0], expansions)
|
||||
display_name = get_variant_name("No C Ext", host)
|
||||
return [create_variant(tasks, display_name, host=host, expansions=expansions)]
|
||||
return [create_variant(tasks, display_name, host=host)]
|
||||
|
||||
|
||||
def create_mod_wsgi_variants():
|
||||
@ -342,41 +323,6 @@ def create_disable_test_commands_variants():
|
||||
return [create_variant(tasks, display_name, host=host, expansions=expansions)]
|
||||
|
||||
|
||||
def create_test_numpy_tasks():
|
||||
tasks = []
|
||||
for python in MIN_MAX_PYTHON:
|
||||
tags = ["binary", "vector", f"python-{python}", "test-numpy"]
|
||||
vars = dict(TOOLCHAIN_VERSION=python)
|
||||
if python == MIN_MAX_PYTHON[-1]:
|
||||
tags.append("pr")
|
||||
vars["COVERAGE"] = "1"
|
||||
task_name = get_task_name("test-numpy", python=python, **vars)
|
||||
test_func = FunctionCall(func="test numpy", vars=vars)
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||
return tasks
|
||||
|
||||
|
||||
def create_test_numpy_variants() -> list[BuildVariant]:
|
||||
variants = []
|
||||
base_display_name = "Test Numpy"
|
||||
|
||||
# Test a subset on each of the other platforms.
|
||||
for host_name in ("rhel8", "macos", "macos-arm64", "win64", "win32"):
|
||||
tasks = [".test-numpy"]
|
||||
host = HOSTS[host_name]
|
||||
tags = ["binary", "vector"]
|
||||
if host_name == "rhel8":
|
||||
tags.append("pr")
|
||||
expansions = dict()
|
||||
if host_name == "win32":
|
||||
expansions["IS_WIN32"] = "1"
|
||||
display_name = get_variant_name(base_display_name, host)
|
||||
variant = create_variant(tasks, display_name, host=host, tags=tags, expansions=expansions)
|
||||
variants.append(variant)
|
||||
|
||||
return variants
|
||||
|
||||
|
||||
def create_oidc_auth_variants():
|
||||
variants = []
|
||||
for host_name in ["ubuntu22", "macos", "win64"]:
|
||||
@ -403,7 +349,6 @@ def create_oidc_auth_variants():
|
||||
tags=["pr"],
|
||||
host=host,
|
||||
batchtime=BATCHTIME_DAY,
|
||||
expansions=dict(COVERAGE="1"),
|
||||
)
|
||||
)
|
||||
return variants
|
||||
@ -489,15 +434,15 @@ def create_perf_variants():
|
||||
def create_aws_auth_variants():
|
||||
variants = []
|
||||
|
||||
for host_name in ["rhel8", "win64", "macos"]:
|
||||
for host_name in ["ubuntu20", "win64", "macos"]:
|
||||
expansions = dict()
|
||||
tasks = [".auth-aws"]
|
||||
tags = []
|
||||
if host_name == "macos":
|
||||
tasks = [".auth-aws !.auth-aws-web-identity !.auth-aws-ec2"]
|
||||
tasks = [".auth-aws !.auth-aws-web-identity !.auth-aws-ecs !.auth-aws-ec2"]
|
||||
tags = ["pr"]
|
||||
elif host_name == "win64":
|
||||
tasks = [".auth-aws"]
|
||||
tasks = [".auth-aws !.auth-aws-ecs"]
|
||||
host = HOSTS[host_name]
|
||||
variant = create_variant(
|
||||
tasks,
|
||||
@ -507,25 +452,9 @@ def create_aws_auth_variants():
|
||||
expansions=expansions,
|
||||
)
|
||||
variants.append(variant)
|
||||
|
||||
# The ECS test must be run on Ubuntu 24 to match the Fargate Config.
|
||||
variant = create_variant(
|
||||
[".auth-aws-ecs"],
|
||||
get_variant_name("Auth AWS ECS", host),
|
||||
host=HOSTS["ubuntu24"],
|
||||
tags=tags,
|
||||
expansions=expansions,
|
||||
)
|
||||
variants.append(variant)
|
||||
return variants
|
||||
|
||||
|
||||
def create_min_support_variants():
|
||||
host = HOSTS["rhel8"]
|
||||
name = get_variant_name("Min Support", host=host)
|
||||
return [create_variant([".test-min-support"], name, host=host)]
|
||||
|
||||
|
||||
def create_no_server_variants():
|
||||
host = HOSTS["rhel8"]
|
||||
name = get_variant_name("No server", host=host)
|
||||
@ -599,11 +528,9 @@ def create_server_version_tasks():
|
||||
seen.add(combo)
|
||||
tags.append("pr")
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
if "t" in python:
|
||||
tags.append("free-threaded")
|
||||
if "pr" in tags:
|
||||
if python not in PYPYS and "t" not in python:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name(
|
||||
"test-server-version",
|
||||
@ -666,10 +593,6 @@ def create_test_non_standard_tasks():
|
||||
if pr:
|
||||
tags.append("pr")
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
elif pr:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name("test-non-standard", python=python, **expansions)
|
||||
server_func = FunctionCall(func="run server", vars=expansions)
|
||||
test_vars = expansions.copy()
|
||||
@ -710,10 +633,6 @@ def create_test_standard_auth_tasks():
|
||||
if pr:
|
||||
tags.append("pr")
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
elif pr:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name("test-standard-auth", python=python, **expansions)
|
||||
server_func = FunctionCall(func="run server", vars=expansions)
|
||||
test_vars = expansions.copy()
|
||||
@ -723,6 +642,22 @@ def create_test_standard_auth_tasks():
|
||||
return tasks
|
||||
|
||||
|
||||
def create_min_deps_tasks():
|
||||
"""For variants that support testing with minimum dependencies."""
|
||||
tasks = []
|
||||
for topology in TOPOLOGIES:
|
||||
auth, ssl = get_standard_auth_ssl(topology)
|
||||
tags = ["test-min-deps", f"{topology}-{auth}-{ssl}"]
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
|
||||
server_func = FunctionCall(func="run server", vars=expansions)
|
||||
test_vars = expansions.copy()
|
||||
test_vars["TEST_MIN_DEPS"] = "1"
|
||||
name = get_task_name("test-min-deps", python=CPYTHONS[0], sync="sync", **test_vars)
|
||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
|
||||
return tasks
|
||||
|
||||
|
||||
def create_standard_tasks():
|
||||
"""For variants that do not set a TEST_NAME."""
|
||||
tasks = []
|
||||
@ -750,10 +685,6 @@ def create_standard_tasks():
|
||||
if pr:
|
||||
tags.append("pr")
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
elif pr:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name("test-standard", python=python, sync=sync, **expansions)
|
||||
server_func = FunctionCall(func="run server", vars=expansions)
|
||||
test_vars = expansions.copy()
|
||||
@ -771,11 +702,9 @@ def create_no_orchestration_tasks():
|
||||
"test-no-orchestration",
|
||||
f"python-{python}",
|
||||
]
|
||||
name = get_task_name("test-no-orchestration", python=python)
|
||||
assume_func = FunctionCall(func="assume ec2 role")
|
||||
test_vars = dict(TOOLCHAIN_VERSION=python)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
test_vars["TEST_MIN_DEPS"] = "1"
|
||||
name = get_task_name("test-no-orchestration", **test_vars)
|
||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||
commands = [assume_func, test_func]
|
||||
tasks.append(EvgTask(name=name, tags=tags, commands=commands))
|
||||
@ -812,23 +741,19 @@ def create_aws_tasks():
|
||||
"env-creds",
|
||||
"session-creds",
|
||||
"web-identity",
|
||||
"ecs",
|
||||
]
|
||||
assume_func = FunctionCall(func="assume ec2 role")
|
||||
for version, test_type, python in zip_cycle(get_versions_from("4.4"), aws_test_types, CPYTHONS):
|
||||
base_name = f"test-auth-aws-{version}"
|
||||
base_tags = ["auth-aws"]
|
||||
server_vars = dict(AUTH_AWS="1", VERSION=version)
|
||||
server_func = FunctionCall(func="run server", vars=server_vars)
|
||||
assume_func = FunctionCall(func="assume ec2 role")
|
||||
tags = [*base_tags, f"auth-aws-{test_type}"]
|
||||
if "t" in python:
|
||||
tags.append("free-threaded")
|
||||
name = get_task_name(f"{base_name}-{test_type}", python=python)
|
||||
test_vars = dict(TEST_NAME="auth_aws", SUB_TEST_NAME=test_type, TOOLCHAIN_VERSION=python)
|
||||
if python == MIN_MAX_PYTHON[0]:
|
||||
test_vars["TEST_MIN_DEPS"] = "1"
|
||||
elif python == MIN_MAX_PYTHON[-1]:
|
||||
tags.append("pr")
|
||||
test_vars["COVERAGE"] = "1"
|
||||
name = get_task_name(f"{base_name}-{test_type}", **test_vars)
|
||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||
funcs = [server_func, assume_func, test_func]
|
||||
tasks.append(EvgTask(name=name, tags=tags, commands=funcs))
|
||||
@ -848,16 +773,6 @@ def create_aws_tasks():
|
||||
funcs = [server_func, assume_func, test_func]
|
||||
tasks.append(EvgTask(name=name, tags=tags, commands=funcs))
|
||||
|
||||
# Add the ECS task. This will run on Ubuntu 24 to match the
|
||||
# Fargate environment.
|
||||
tags = ["auth-aws-ecs"]
|
||||
test_vars = dict(TEST_NAME="auth_aws", SUB_TEST_NAME="ecs")
|
||||
name = get_task_name("test-auth-aws-ecs", **test_vars)
|
||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||
server_func = FunctionCall(func="run server", vars=dict(VERSION="8.0"))
|
||||
funcs = [assume_func, server_func, test_func]
|
||||
tasks.append(EvgTask(name=name, tags=tags, commands=funcs))
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
@ -865,11 +780,11 @@ def create_oidc_tasks():
|
||||
tasks = []
|
||||
for sub_test in ["default", "azure", "gcp", "eks", "aks", "gke"]:
|
||||
vars = dict(TEST_NAME="auth_oidc", SUB_TEST_NAME=sub_test)
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
task_name = f"test-auth-oidc-{sub_test}"
|
||||
tags = ["auth_oidc"]
|
||||
if sub_test != "default":
|
||||
tags.append("auth_oidc_remote")
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
task_name = get_task_name(f"test-auth-oidc-{sub_test}", **vars)
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||
|
||||
return tasks
|
||||
@ -917,40 +832,24 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
|
||||
TOOLCHAIN_VERSION=python,
|
||||
VERSION=version,
|
||||
)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
vars["TEST_MIN_DEPS"] = "1"
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
|
||||
tags = ["ocsp", f"ocsp-{algo}", version]
|
||||
if "disableStapling" not in variant:
|
||||
tags.append("ocsp-staple")
|
||||
if base_task_name == "valid-cert-server-staples" and version == "latest":
|
||||
if algo == "valid-cert-server-staples" and version == "latest":
|
||||
tags.append("pr")
|
||||
if "TEST_MIN_DEPS" not in vars:
|
||||
vars["COVERAGE"] = "1"
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
task_name = get_task_name(f"test-ocsp-{algo}-{base_task_name}", **vars)
|
||||
|
||||
task_name = get_task_name(
|
||||
f"test-ocsp-{algo}-{base_task_name}",
|
||||
python=python,
|
||||
version=version,
|
||||
)
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def create_min_support_tasks():
|
||||
server_func = FunctionCall(func="run server")
|
||||
from generate_config_utils import MIN_SUPPORT_VERSIONS
|
||||
|
||||
tasks = []
|
||||
for python, topology in product(MIN_SUPPORT_VERSIONS, TOPOLOGIES):
|
||||
auth, ssl = get_standard_auth_ssl(topology)
|
||||
vars = dict(UV_PYTHON=python, AUTH=auth, SSL=ssl, TOPOLOGY=topology)
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
task_name = get_task_name(
|
||||
"test-min-support", python=python, topology=topology, auth=auth, ssl=ssl
|
||||
)
|
||||
tags = ["test-min-support"]
|
||||
commands = [server_func, test_func]
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=commands))
|
||||
return tasks
|
||||
|
||||
|
||||
def create_aws_lambda_tasks():
|
||||
assume_func = FunctionCall(func="assume ec2 role")
|
||||
vars = dict(TEST_NAME="aws_lambda")
|
||||
@ -1093,26 +992,6 @@ def create_upload_coverage_func():
|
||||
return "upload coverage", [get_assume_role(), cmd]
|
||||
|
||||
|
||||
def create_upload_coverage_codecov_func():
|
||||
# Upload the coverage xml report to codecov.
|
||||
include_expansions = [
|
||||
"CODECOV_TOKEN",
|
||||
"build_variant",
|
||||
"task_name",
|
||||
"github_commit",
|
||||
"github_pr_number",
|
||||
"github_pr_head_branch",
|
||||
"github_author",
|
||||
"requester",
|
||||
"branch_name",
|
||||
]
|
||||
args = [
|
||||
".evergreen/scripts/upload-codecov.sh",
|
||||
]
|
||||
upload_cmd = get_subprocess_exec(include_expansions_in_env=include_expansions, args=args)
|
||||
return "upload codecov", [upload_cmd]
|
||||
|
||||
|
||||
def create_download_and_merge_coverage_func():
|
||||
include_expansions = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
|
||||
args = [
|
||||
@ -1245,14 +1124,6 @@ def create_run_tests_func():
|
||||
return "run tests", [setup_cmd, test_cmd]
|
||||
|
||||
|
||||
def create_test_numpy_func():
|
||||
includes = ["TOOLCHAIN_VERSION", "COVERAGE"]
|
||||
test_cmd = get_subprocess_exec(
|
||||
include_expansions_in_env=includes, args=[".evergreen/just.sh", "test-numpy"]
|
||||
)
|
||||
return "test numpy", [test_cmd]
|
||||
|
||||
|
||||
def create_cleanup_func():
|
||||
cmd = get_subprocess_exec(args=[".evergreen/scripts/cleanup.sh"])
|
||||
return "cleanup", [cmd]
|
||||
|
||||
@ -24,7 +24,6 @@ from shrub.v3.shrub_service import ShrubService
|
||||
ALL_VERSIONS = ["4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
|
||||
CPYTHONS = ["3.10", "3.11", "3.12", "3.13", "3.14t", "3.14"]
|
||||
PYPYS = ["pypy3.11"]
|
||||
MIN_SUPPORT_VERSIONS = ["3.9", "pypy3.9", "pypy3.10"]
|
||||
ALL_PYTHONS = CPYTHONS + PYPYS
|
||||
MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]]
|
||||
BATCHTIME_WEEK = 10080
|
||||
@ -43,7 +42,7 @@ DISPLAY_LOOKUP = dict(
|
||||
sync={"sync": "Sync", "async": "Async"},
|
||||
coverage={"1": "cov"},
|
||||
no_ext={"1": "No C"},
|
||||
test_min_deps={"1": "Min Deps"},
|
||||
test_min_deps={True: "Min Deps"},
|
||||
)
|
||||
HOSTS = dict()
|
||||
|
||||
@ -59,12 +58,12 @@ class Host:
|
||||
# Hosts with toolchains.
|
||||
HOSTS["rhel8"] = Host("rhel8", "rhel87-small", "RHEL8", dict())
|
||||
HOSTS["win64"] = Host("win64", "windows-64-vsMulti-small", "Win64", dict())
|
||||
HOSTS["win-latest"] = Host("win-latest", "windows-2022-latest-small", "WinLatest", dict())
|
||||
HOSTS["win32"] = Host("win32", "windows-64-vsMulti-small", "Win32", dict())
|
||||
HOSTS["macos"] = Host("macos", "macos-14", "macOS", dict())
|
||||
HOSTS["macos-arm64"] = Host("macos-arm64", "macos-14-arm64", "macOS Arm64", dict())
|
||||
HOSTS["ubuntu20"] = Host("ubuntu20", "ubuntu2004-small", "Ubuntu-20", dict())
|
||||
HOSTS["ubuntu22"] = Host("ubuntu22", "ubuntu2204-small", "Ubuntu-22", dict())
|
||||
HOSTS["ubuntu24"] = Host("ubuntu24", "ubuntu2404-small", "Ubuntu-24", dict())
|
||||
HOSTS["rhel7"] = Host("rhel7", "rhel79-small", "RHEL7", dict())
|
||||
HOSTS["perf"] = Host("perf", "rhel90-dbx-perf-large", "", dict())
|
||||
HOSTS["debian11"] = Host("debian11", "debian11-small", "Debian11", dict())
|
||||
DEFAULT_HOST = HOSTS["rhel8"]
|
||||
@ -140,14 +139,6 @@ def create_variant(
|
||||
expansions = expansions and expansions.copy() or dict()
|
||||
if version:
|
||||
expansions["VERSION"] = version
|
||||
# 8.0+ Windows builds must run on win-latest
|
||||
if (
|
||||
"win64" in display_name.lower()
|
||||
or "win32" in display_name.lower()
|
||||
and version
|
||||
and version >= "8.0"
|
||||
):
|
||||
kwargs["run_on"] = HOSTS["win-latest"].run_on
|
||||
return create_variant_generic(
|
||||
tasks, display_name, version=version, host=host, expansions=expansions, **kwargs
|
||||
)
|
||||
@ -181,7 +172,7 @@ def get_common_name(base: str, sep: str, **kwargs) -> str:
|
||||
display_name = f"{display_name}{sep}{version}"
|
||||
for key, value in kwargs.items():
|
||||
name = value
|
||||
if key.lower() in ["python", "toolchain_version"]:
|
||||
if key.lower() == "python":
|
||||
if not value.startswith("pypy"):
|
||||
name = f"Python{value}"
|
||||
else:
|
||||
|
||||
@ -6,8 +6,7 @@ import pathlib
|
||||
import subprocess
|
||||
from argparse import Namespace
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
JIRA_FILTER = "https://jira.mongodb.org/issues/?jql=labels%20%3D%20automated-sync%20AND%20status%20!%3D%20Closed"
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def resync_specs(directory: pathlib.Path, errored: dict[str, str]) -> None:
|
||||
@ -33,27 +32,14 @@ def resync_specs(directory: pathlib.Path, errored: dict[str, str]) -> None:
|
||||
|
||||
def apply_patches(errored):
|
||||
print("Beginning to apply patches")
|
||||
subprocess.run(
|
||||
["bash", "./.evergreen/remove-unimplemented-tests.sh"], # noqa: S603, S607
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["bash", "./.evergreen/remove-unimplemented-tests.sh"], check=True) # noqa: S603, S607
|
||||
try:
|
||||
# Avoid shell=True by passing arguments as a list.
|
||||
# Note: glob expansion doesn't work in shell=False, so we use a list of files.
|
||||
patches = [str(p) for p in pathlib.Path("./.evergreen/spec-patch/").glob("*")]
|
||||
if patches:
|
||||
subprocess.run(
|
||||
[ # noqa: S603, S607
|
||||
"git",
|
||||
"apply",
|
||||
"-R",
|
||||
"--allow-empty",
|
||||
"--whitespace=fix",
|
||||
*patches,
|
||||
],
|
||||
check=True,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
subprocess.run(
|
||||
["git apply -R --allow-empty --whitespace=fix ./.evergreen/spec-patch/*"], # noqa: S607
|
||||
shell=True, # noqa: S602
|
||||
check=True,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except CalledProcessError as exc:
|
||||
errored["applying patches"] = exc.stderr
|
||||
|
||||
@ -87,24 +73,17 @@ def check_new_spec_directories(directory: pathlib.Path) -> list[str]:
|
||||
return list(spec_set - test_set)
|
||||
|
||||
|
||||
def write_summary(errored: dict[str, str], new: list[str], filename: str | None) -> None:
|
||||
def write_summary(errored: dict[str, str], new: list[str], filename: Optional[str]) -> None:
|
||||
"""Generate the PR description"""
|
||||
pr_body = ""
|
||||
# Avoid shell=True and complex pipes by using Python to process git output
|
||||
process = subprocess.run(
|
||||
["git", "diff", "--name-only"], # noqa: S603, S607
|
||||
["git diff --name-only | awk -F'/' '{print $2}' | sort | uniq"], # noqa: S607
|
||||
shell=True, # noqa: S602
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
changed_files = process.stdout.strip().splitlines()
|
||||
succeeded_set = set()
|
||||
for f in changed_files:
|
||||
parts = f.split("/")
|
||||
if len(parts) > 1:
|
||||
succeeded_set.add(parts[1])
|
||||
succeeded = sorted(succeeded_set)
|
||||
|
||||
succeeded = process.stdout.strip().split()
|
||||
if len(succeeded) > 0:
|
||||
pr_body += "The following specs were changed:\n -"
|
||||
pr_body += "\n -".join(succeeded)
|
||||
@ -119,7 +98,6 @@ def write_summary(errored: dict[str, str], new: list[str], filename: str | None)
|
||||
pr_body += "\n -".join(new)
|
||||
pr_body += "\n"
|
||||
if pr_body != "":
|
||||
pr_body = f"Jira tickets: {JIRA_FILTER}\n\n" + pr_body
|
||||
if filename is None:
|
||||
print(f"\n{pr_body}")
|
||||
else:
|
||||
@ -142,9 +120,7 @@ if __name__ == "__main__":
|
||||
description="Python Script to resync all specs and generate summary for PR."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--filename",
|
||||
help="Name of file for the summary to be written into.",
|
||||
default=None,
|
||||
"--filename", help="Name of file for the summary to be written into.", default=None
|
||||
)
|
||||
args = parser.parse_args()
|
||||
main(args)
|
||||
|
||||
@ -12,7 +12,7 @@ def set_env(name: str, value: Any = "1") -> None:
|
||||
|
||||
def start_server():
|
||||
opts, extra_opts = get_test_options(
|
||||
"Run a MongoDB server. All given flags will be passed to run-mongodb.sh in DRIVERS_TOOLS.",
|
||||
"Run a MongoDB server. All given flags will be passed to run-orchestration.sh in DRIVERS_TOOLS.",
|
||||
require_sub_test_name=False,
|
||||
allow_extra_opts=True,
|
||||
)
|
||||
@ -51,7 +51,7 @@ def start_server():
|
||||
elif opts.quiet:
|
||||
extra_opts.append("-q")
|
||||
|
||||
cmd = ["bash", f"{DRIVERS_TOOLS}/.evergreen/run-mongodb.sh", "start", *extra_opts]
|
||||
cmd = ["bash", f"{DRIVERS_TOOLS}/.evergreen/run-orchestration.sh", *extra_opts]
|
||||
run_command(cmd, cwd=DRIVERS_TOOLS)
|
||||
|
||||
|
||||
|
||||
@ -4,9 +4,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@ -204,16 +202,6 @@ def run() -> None:
|
||||
if os.environ.get("DEBUG_LOG"):
|
||||
TEST_ARGS.extend(f"-o log_cli_level={logging.DEBUG}".split())
|
||||
|
||||
if os.environ.get("COVERAGE"):
|
||||
binary = sys.executable.replace(os.sep, "/")
|
||||
cmd = f"{binary} -m coverage run -m pytest {' '.join(TEST_ARGS)} {' '.join(sys.argv[1:])}"
|
||||
result = subprocess.run(shlex.split(cmd), check=False) # noqa: S603
|
||||
cmd = f"{binary} -m coverage report"
|
||||
subprocess.run(shlex.split(cmd), check=False) # noqa: S603
|
||||
if result.returncode != 0:
|
||||
print(result.stderr)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
# Run local tests.
|
||||
ret = pytest.main(TEST_ARGS + sys.argv[1:])
|
||||
if ret != 0:
|
||||
|
||||
@ -12,7 +12,6 @@ set -eu
|
||||
# TEST_CRYPT_SHARED If non-empty, install crypt_shared lib.
|
||||
# MONGODB_API_VERSION The mongodb api version to use in tests.
|
||||
# MONGODB_URI If non-empty, use as the MONGODB_URI in tests.
|
||||
# USE_ACTIVE_VENV If non-empty, use the active virtual environment.
|
||||
|
||||
SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0})
|
||||
|
||||
@ -22,5 +21,5 @@ if [ -f $SCRIPT_DIR/env.sh ]; then
|
||||
fi
|
||||
|
||||
echo "Setting up tests with args \"$*\"..."
|
||||
uv run ${USE_ACTIVE_VENV:+--active} "$SCRIPT_DIR/setup_tests.py" "$@"
|
||||
uv run $SCRIPT_DIR/setup_tests.py "$@"
|
||||
echo "Setting up tests with args \"$*\"... done."
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import stat
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from urllib import request
|
||||
|
||||
@ -115,10 +117,9 @@ def setup_libmongocrypt():
|
||||
LOGGER.info(f"Fetching {url}...")
|
||||
with request.urlopen(request.Request(url), timeout=15.0) as response: # noqa: S310
|
||||
if response.status == 200:
|
||||
with Path("libmongocrypt.tar.gz").open("wb") as f:
|
||||
f.write(response.read())
|
||||
Path("libmongocrypt").mkdir()
|
||||
run_command("tar -xzf libmongocrypt.tar.gz -C libmongocrypt")
|
||||
fileobj = io.BytesIO(response.read())
|
||||
with tarfile.open("libmongocrypt.tar.gz", fileobj=fileobj) as fid:
|
||||
fid.extractall(Path.cwd() / "libmongocrypt")
|
||||
LOGGER.info(f"Fetching {url}... done.")
|
||||
|
||||
run_command("ls -la libmongocrypt")
|
||||
@ -153,10 +154,6 @@ def handle_test_env() -> None:
|
||||
# Start compiling the args we'll pass to uv.
|
||||
UV_ARGS = ["--extra test --no-group dev"]
|
||||
|
||||
# If USE_ACTIVE_VENV is set, add --active to UV_ARGS so run-tests.sh uses the active venv.
|
||||
if is_set("USE_ACTIVE_VENV"):
|
||||
UV_ARGS.append("--active")
|
||||
|
||||
test_title = test_name
|
||||
if sub_test_name:
|
||||
test_title += f" {sub_test_name}"
|
||||
@ -328,8 +325,7 @@ def handle_test_env() -> None:
|
||||
version = os.environ.get("VERSION", "latest")
|
||||
cmd = [
|
||||
"bash",
|
||||
f"{DRIVERS_TOOLS}/.evergreen/run-mongodb.sh",
|
||||
"start",
|
||||
f"{DRIVERS_TOOLS}/.evergreen/run-orchestration.sh",
|
||||
"--ssl",
|
||||
"--version",
|
||||
version,
|
||||
@ -436,9 +432,6 @@ def handle_test_env() -> None:
|
||||
# We do not want the default client_context to be initialized.
|
||||
write_env("DISABLE_CONTEXT")
|
||||
|
||||
if test_name == "numpy":
|
||||
UV_ARGS.append("--with numpy")
|
||||
|
||||
if test_name == "perf":
|
||||
data_dir = ROOT / "specifications/source/benchmarking/data"
|
||||
if not data_dir.exists():
|
||||
@ -466,14 +459,12 @@ def handle_test_env() -> None:
|
||||
# Keep in sync with combine-coverage.sh.
|
||||
# coverage >=5 is needed for relative_files=true.
|
||||
UV_ARGS.append("--group coverage")
|
||||
TEST_ARGS = f"{TEST_ARGS} --cov"
|
||||
write_env("COVERAGE")
|
||||
|
||||
if opts.green_framework:
|
||||
framework = opts.green_framework or os.environ["GREEN_FRAMEWORK"]
|
||||
UV_ARGS.append(f"--group {framework}")
|
||||
if framework == "gevent" and opts.test_min_deps:
|
||||
# PYTHON-5729. This can be removed when the min supported gevent is moved to 25.9.1.
|
||||
UV_ARGS.append('--with "setuptools==81.0"')
|
||||
|
||||
else:
|
||||
TEST_ARGS = f"-v --durations=5 {TEST_ARGS}"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Stop a server that was started using run-mongodb.sh in DRIVERS_TOOLS.
|
||||
# Stop a server that was started using run-orchestration.sh in DRIVERS_TOOLS.
|
||||
set -eu
|
||||
|
||||
HERE=$(dirname ${BASH_SOURCE:-$0})
|
||||
@ -11,4 +11,4 @@ if [ -f $HERE/env.sh ]; then
|
||||
source $HERE/env.sh
|
||||
fi
|
||||
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/run-mongodb.sh stop
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/stop-orchestration.sh
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2154
|
||||
# Upload a coverate report to codecov.
|
||||
set -eu
|
||||
|
||||
HERE=$(dirname ${BASH_SOURCE:-$0})
|
||||
ROOT=$(dirname "$(dirname $HERE)")
|
||||
|
||||
pushd $ROOT > /dev/null
|
||||
export FNAME=coverage.xml
|
||||
REQUESTER=${requester:-}
|
||||
|
||||
if [ ! -f ".coverage" ]; then
|
||||
echo "There are no coverage results, not running codecov"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${REQUESTER}" == "github_pr" || "${REQUESTER}" == "commit" ]]; then
|
||||
echo "Uploading codecov for $REQUESTER..."
|
||||
else
|
||||
echo "Error: requester must be 'github_pr' or 'commit', got '${REQUESTER}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'sha: %s\n' "$github_commit"
|
||||
printf 'flag: %s-%s\n' "$build_variant" "$task_name"
|
||||
printf 'file: %s\n' "$FNAME"
|
||||
uv tool run --with "coverage[toml]" coverage xml
|
||||
|
||||
codecov_args=(
|
||||
upload-process
|
||||
--report-type coverage
|
||||
--disable-search
|
||||
--fail-on-error
|
||||
--git-service github
|
||||
--token "${CODECOV_TOKEN}"
|
||||
--sha "${github_commit}"
|
||||
--flag "${build_variant}-${task_name}"
|
||||
--file "${FNAME}"
|
||||
)
|
||||
|
||||
if [ -n "${github_pr_number:-}" ]; then
|
||||
printf 'branch: %s:%s\n' "$github_author" "$github_pr_head_branch"
|
||||
printf 'pr: %s\n' "$github_pr_number"
|
||||
uv tool run --from codecov-cli codecovcli \
|
||||
"${codecov_args[@]}" \
|
||||
--pr "${github_pr_number}" \
|
||||
--branch "${github_author}:${github_pr_head_branch}"
|
||||
else
|
||||
printf 'branch: %s\n' "$branch_name"
|
||||
uv tool run --from codecov-cli codecovcli \
|
||||
"${codecov_args[@]}" \
|
||||
--branch "${branch_name}"
|
||||
fi
|
||||
echo "Uploading codecov for $REQUESTER... done."
|
||||
|
||||
popd > /dev/null
|
||||
@ -44,7 +44,6 @@ TEST_SUITE_MAP = {
|
||||
"mockupdb": "mockupdb",
|
||||
"ocsp": "ocsp",
|
||||
"perf": "perf",
|
||||
"numpy": "",
|
||||
}
|
||||
|
||||
# Tests that require a sub test suite.
|
||||
@ -52,7 +51,7 @@ SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi", "perf"]
|
||||
|
||||
EXTRA_TESTS = ["mod_wsgi", "aws_lambda", "doctest"]
|
||||
|
||||
# Tests that do not use run-mongodb directly.
|
||||
# Tests that do not use run-orchestration directly.
|
||||
NO_RUN_ORCHESTRATION = [
|
||||
"auth_oidc",
|
||||
"atlas_connect",
|
||||
|
||||
64
.evergreen/spec-patch/PYTHON-2673.patch
Normal file
64
.evergreen/spec-patch/PYTHON-2673.patch
Normal file
@ -0,0 +1,64 @@
|
||||
diff --git a/test/load_balancer/cursors.json b/test/load_balancer/cursors.json
|
||||
index 43e4fbb4f..4e2a55fd4 100644
|
||||
--- a/test/load_balancer/cursors.json
|
||||
+++ b/test/load_balancer/cursors.json
|
||||
@@ -376,7 +376,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
+ "description": "pinned connections are not returned after an network error during getMore",
|
||||
- "description": "pinned connections are returned after an network error during getMore",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
@@ -440,7 +440,7 @@
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
+ "connections": 1
|
||||
- "connections": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -659,7 +659,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
+ "description": "pinned connections are not returned to the pool after a non-network error on getMore",
|
||||
- "description": "pinned connections are returned to the pool after a non-network error on getMore",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
@@ -715,7 +715,7 @@
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
+ "connections": 1
|
||||
- "connections": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
diff --git a/test/load_balancer/sdam-error-handling.json b/test/load_balancer/sdam-error-handling.json
|
||||
index 63aabc04d..462fa0aac 100644
|
||||
--- a/test/load_balancer/sdam-error-handling.json
|
||||
+++ b/test/load_balancer/sdam-error-handling.json
|
||||
@@ -366,6 +366,9 @@
|
||||
{
|
||||
"connectionCreatedEvent": {}
|
||||
},
|
||||
+ {
|
||||
+ "poolClearedEvent": {}
|
||||
+ },
|
||||
{
|
||||
"connectionClosedEvent": {
|
||||
"reason": "error"
|
||||
@@ -378,9 +375,6 @@
|
||||
"connectionCheckOutFailedEvent": {
|
||||
"reason": "connectionError"
|
||||
}
|
||||
- },
|
||||
- {
|
||||
- "poolClearedEvent": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
.evergreen/spec-patch/PYTHON-3712.patch
Normal file
14
.evergreen/spec-patch/PYTHON-3712.patch
Normal file
@ -0,0 +1,14 @@
|
||||
diff --git a/test/discovery_and_monitoring/unified/serverMonitoringMode.json b/test/discovery_and_monitoring/unified/serverMonitoringMode.json
|
||||
index e44fad1b..4b492f7d 100644
|
||||
--- a/test/discovery_and_monitoring/unified/serverMonitoringMode.json
|
||||
+++ b/test/discovery_and_monitoring/unified/serverMonitoringMode.json
|
||||
@@ -5,7 +5,8 @@
|
||||
{
|
||||
"topologies": [
|
||||
"single",
|
||||
- "sharded"
|
||||
+ "sharded",
|
||||
+ "sharded-replicaset"
|
||||
],
|
||||
"serverless": "forbid"
|
||||
}
|
||||
61
.evergreen/spec-patch/PYTHON-4261.patch
Normal file
61
.evergreen/spec-patch/PYTHON-4261.patch
Normal file
@ -0,0 +1,61 @@
|
||||
diff --git a/test/server_selection_logging/replica-set.json b/test/server_selection_logging/replica-set.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/replica-set.json
|
||||
+++ b/test/server_selection_logging/replica-set.json
|
||||
@@ -184,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
diff --git a/test/server_selection_logging/standalone.json b/test/server_selection_logging/standalone.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/standalone.json
|
||||
+++ b/test/server_selection_logging/standalone.json
|
||||
@@ -191,7 +191,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
diff --git a/test/server_selection_logging/sharded.json b/test/server_selection_logging/sharded.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/sharded.json
|
||||
+++ b/test/server_selection_logging/sharded.json
|
||||
@@ -193,7 +193,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
diff --git a/test/server_selection_logging/sharded.json b/test/server_selection_logging/operation-id.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/operation-id.json
|
||||
+++ b/test/server_selection_logging/operation-id.json
|
||||
@@ -197,7 +197,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
@@ -383,7 +383,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
@ -1,26 +0,0 @@
|
||||
diff --git a/test/auth/legacy/connection-string.json b/test/auth/legacy/connection-string.json
|
||||
index 3a099c813..8982b61d5 100644
|
||||
--- a/test/auth/legacy/connection-string.json
|
||||
+++ b/test/auth/legacy/connection-string.json
|
||||
@@ -440,6 +440,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
+ {
|
||||
+ "description": "should throw an exception if username provided (MONGODB-AWS)",
|
||||
+ "uri": "mongodb://user@localhost.com/?authMechanism=MONGODB-AWS",
|
||||
+ "valid": false
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "should throw an exception if username and password provided (MONGODB-AWS)",
|
||||
+ "uri": "mongodb://user:pass@localhost.com/?authMechanism=MONGODB-AWS",
|
||||
+ "valid": false
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "should throw an exception if AWS_SESSION_TOKEN provided (MONGODB-AWS)",
|
||||
+ "uri": "mongodb://localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token",
|
||||
+ "valid": false
|
||||
+ },
|
||||
{
|
||||
"description": "should recognise the mechanism with test environment (MONGODB-OIDC)",
|
||||
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test",
|
||||
587
.evergreen/spec-patch/PYTHON-5529.patch
Normal file
587
.evergreen/spec-patch/PYTHON-5529.patch
Normal file
@ -0,0 +1,587 @@
|
||||
diff --git a/test/csot/command-execution.json b/test/csot/command-execution.json
|
||||
index aa9c3eb2..212cd410 100644
|
||||
--- a/test/csot/command-execution.json
|
||||
+++ b/test/csot/command-execution.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "timeoutMS behaves correctly during command execution",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4.7",
|
||||
@@ -69,8 +69,10 @@
|
||||
"appName": "reduceMaxTimeMSTest",
|
||||
"w": 1,
|
||||
"timeoutMS": 500,
|
||||
- "heartbeatFrequencyMS": 500
|
||||
+ "heartbeatFrequencyMS": 500,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
]
|
||||
@@ -185,8 +187,10 @@
|
||||
"appName": "rttTooHighTest",
|
||||
"w": 1,
|
||||
"timeoutMS": 10,
|
||||
- "heartbeatFrequencyMS": 500
|
||||
+ "heartbeatFrequencyMS": 500,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
]
|
||||
@@ -316,8 +320,10 @@
|
||||
"appName": "reduceMaxTimeMSTest",
|
||||
"w": 1,
|
||||
"timeoutMS": 90,
|
||||
- "heartbeatFrequencyMS": 100000
|
||||
+ "heartbeatFrequencyMS": 100000,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
]
|
||||
diff --git a/test/csot/convenient-transactions.json b/test/csot/convenient-transactions.json
|
||||
index 3868b302..f9d03429 100644
|
||||
--- a/test/csot/convenient-transactions.json
|
||||
+++ b/test/csot/convenient-transactions.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "timeoutMS behaves correctly for the withTransaction API",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4",
|
||||
@@ -21,8 +21,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 500
|
||||
+ "timeoutMS": 500,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
diff --git a/test/csot/error-transformations.json b/test/csot/error-transformations.json
|
||||
index 4889e395..89be49f0 100644
|
||||
--- a/test/csot/error-transformations.json
|
||||
+++ b/test/csot/error-transformations.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "MaxTimeMSExpired server errors are transformed into a custom timeout error",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.0",
|
||||
@@ -26,8 +26,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
diff --git a/test/csot/global-timeoutMS.json b/test/csot/global-timeoutMS.json
|
||||
index f1edbe68..9d8046d1 100644
|
||||
--- a/test/csot/global-timeoutMS.json
|
||||
+++ b/test/csot/global-timeoutMS.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "timeoutMS can be configured on a MongoClient",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4",
|
||||
@@ -38,8 +38,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -217,8 +219,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -390,8 +394,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -569,8 +575,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -762,8 +770,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -941,8 +951,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -1120,8 +1132,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -1305,8 +1319,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -1484,8 +1500,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -1663,8 +1681,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -1842,8 +1862,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -2021,8 +2043,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -2194,8 +2218,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -2375,8 +2401,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -2554,8 +2582,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -2733,8 +2763,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -2906,8 +2938,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -3079,8 +3113,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -3258,8 +3294,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -3441,8 +3479,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -3628,8 +3668,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -3807,8 +3849,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -3986,8 +4030,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -4171,8 +4217,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -4360,8 +4408,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -4549,8 +4599,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -4728,8 +4780,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -4913,8 +4967,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -5102,8 +5158,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -5297,8 +5355,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -5482,8 +5542,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
@@ -5677,8 +5739,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 250
|
||||
+ "timeoutMS": 250,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
diff --git a/test/csot/non-tailable-cursors.json b/test/csot/non-tailable-cursors.json
|
||||
index 291c6e72..58c59cb3 100644
|
||||
--- a/test/csot/non-tailable-cursors.json
|
||||
+++ b/test/csot/non-tailable-cursors.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "timeoutMS behaves correctly for non-tailable cursors",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4"
|
||||
@@ -17,8 +17,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 200
|
||||
+ "timeoutMS": 200,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
diff --git a/test/csot/retryability-timeoutMS.json b/test/csot/retryability-timeoutMS.json
|
||||
index 9daad260..5a0c9f36 100644
|
||||
--- a/test/csot/retryability-timeoutMS.json
|
||||
+++ b/test/csot/retryability-timeoutMS.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "timeoutMS behaves correctly for retryable operations",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.0",
|
||||
@@ -26,8 +26,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 100
|
||||
+ "timeoutMS": 100,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
diff --git a/test/csot/runCursorCommand.json b/test/csot/runCursorCommand.json
|
||||
index 36f774fb..e5182e33 100644
|
||||
--- a/test/csot/runCursorCommand.json
|
||||
+++ b/test/csot/runCursorCommand.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "runCursorCommand",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4"
|
||||
@@ -16,6 +16,10 @@
|
||||
{
|
||||
"client": {
|
||||
"id": "commandClient",
|
||||
+ "uriOptions": {
|
||||
+ "minPoolSize": 1
|
||||
+ },
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent",
|
||||
diff --git a/test/csot/sessions-inherit-timeoutMS.json b/test/csot/sessions-inherit-timeoutMS.json
|
||||
index 13ea91c7..dbf163e4 100644
|
||||
--- a/test/csot/sessions-inherit-timeoutMS.json
|
||||
+++ b/test/csot/sessions-inherit-timeoutMS.json
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "sessions inherit timeoutMS from their parent MongoClient",
|
||||
- "schemaVersion": "1.9",
|
||||
+ "schemaVersion": "1.26",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4",
|
||||
@@ -21,8 +21,10 @@
|
||||
"client": {
|
||||
"id": "client",
|
||||
"uriOptions": {
|
||||
- "timeoutMS": 500
|
||||
+ "timeoutMS": 500,
|
||||
+ "minPoolSize": 1
|
||||
},
|
||||
+ "awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent",
|
||||
@ -1,815 +0,0 @@
|
||||
diff --git a/test/sessions/snapshot-sessions.json b/test/sessions/snapshot-sessions.json
|
||||
index 260f8b6f4..8f806ea75 100644
|
||||
--- a/test/sessions/snapshot-sessions.json
|
||||
+++ b/test/sessions/snapshot-sessions.json
|
||||
@@ -988,6 +988,810 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Find operation with snapshot and snapshot time",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "find",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session0",
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ },
|
||||
+ {
|
||||
+ "_id": 2,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "getSnapshotTime",
|
||||
+ "object": "session0",
|
||||
+ "saveResultAsEntity": "savedSnapshotTime"
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "insertOne",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "document": {
|
||||
+ "_id": 3,
|
||||
+ "x": 33
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "createEntities",
|
||||
+ "object": "testRunner",
|
||||
+ "arguments": {
|
||||
+ "entities": [
|
||||
+ {
|
||||
+ "session": {
|
||||
+ "id": "session2",
|
||||
+ "client": "client0",
|
||||
+ "sessionOptions": {
|
||||
+ "snapshot": true,
|
||||
+ "snapshotTime": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "find",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ },
|
||||
+ {
|
||||
+ "_id": 2,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "find",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ },
|
||||
+ {
|
||||
+ "_id": 2,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "find",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ },
|
||||
+ {
|
||||
+ "_id": 2,
|
||||
+ "x": 11
|
||||
+ },
|
||||
+ {
|
||||
+ "_id": 3,
|
||||
+ "x": 33
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "expectEvents": [
|
||||
+ {
|
||||
+ "client": "client0",
|
||||
+ "events": [
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "find": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "find": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "find": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "find": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Distinct operation with snapshot and snapshot time",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "distinct",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session0",
|
||||
+ "filter": {},
|
||||
+ "fieldName": "x"
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ 11
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "getSnapshotTime",
|
||||
+ "object": "session0",
|
||||
+ "saveResultAsEntity": "savedSnapshotTime"
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "insertOne",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "document": {
|
||||
+ "_id": 3,
|
||||
+ "x": 33
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "createEntities",
|
||||
+ "object": "testRunner",
|
||||
+ "arguments": {
|
||||
+ "entities": [
|
||||
+ {
|
||||
+ "session": {
|
||||
+ "id": "session2",
|
||||
+ "client": "client0",
|
||||
+ "sessionOptions": {
|
||||
+ "snapshot": true,
|
||||
+ "snapshotTime": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "distinct",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "filter": {},
|
||||
+ "fieldName": "x"
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ 11
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "distinct",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "filter": {},
|
||||
+ "fieldName": "x"
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ 11
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "distinct",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "filter": {},
|
||||
+ "fieldName": "x"
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ 11,
|
||||
+ 33
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "expectEvents": [
|
||||
+ {
|
||||
+ "client": "client0",
|
||||
+ "events": [
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "distinct": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "distinct": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "distinct": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "distinct": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Aggregate operation with snapshot and snapshot time",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "aggregate",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session0",
|
||||
+ "pipeline": [
|
||||
+ {
|
||||
+ "$match": {
|
||||
+ "_id": 1
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "getSnapshotTime",
|
||||
+ "object": "session0",
|
||||
+ "saveResultAsEntity": "savedSnapshotTime"
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "findOneAndUpdate",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "filter": {
|
||||
+ "_id": 1
|
||||
+ },
|
||||
+ "update": {
|
||||
+ "$inc": {
|
||||
+ "x": 1
|
||||
+ }
|
||||
+ },
|
||||
+ "returnDocument": "After"
|
||||
+ },
|
||||
+ "expectResult": {
|
||||
+ "_id": 1,
|
||||
+ "x": 12
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "createEntities",
|
||||
+ "object": "testRunner",
|
||||
+ "arguments": {
|
||||
+ "entities": [
|
||||
+ {
|
||||
+ "session": {
|
||||
+ "id": "session2",
|
||||
+ "client": "client0",
|
||||
+ "sessionOptions": {
|
||||
+ "snapshot": true,
|
||||
+ "snapshotTime": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "aggregate",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "pipeline": [
|
||||
+ {
|
||||
+ "$match": {
|
||||
+ "_id": 1
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "aggregate",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "pipeline": [
|
||||
+ {
|
||||
+ "$match": {
|
||||
+ "_id": 1
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "aggregate",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "pipeline": [
|
||||
+ {
|
||||
+ "$match": {
|
||||
+ "_id": 1
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 12
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "expectEvents": [
|
||||
+ {
|
||||
+ "client": "client0",
|
||||
+ "events": [
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "countDocuments operation with snapshot and snapshot time",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "countDocuments",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session0",
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": 2
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "getSnapshotTime",
|
||||
+ "object": "session0",
|
||||
+ "saveResultAsEntity": "savedSnapshotTime"
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "insertOne",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "document": {
|
||||
+ "_id": 3,
|
||||
+ "x": 33
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "createEntities",
|
||||
+ "object": "testRunner",
|
||||
+ "arguments": {
|
||||
+ "entities": [
|
||||
+ {
|
||||
+ "session": {
|
||||
+ "id": "session2",
|
||||
+ "client": "client0",
|
||||
+ "sessionOptions": {
|
||||
+ "snapshot": true,
|
||||
+ "snapshotTime": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "countDocuments",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": 2
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "countDocuments",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session2",
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": 2
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "countDocuments",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "filter": {}
|
||||
+ },
|
||||
+ "expectResult": 3
|
||||
+ }
|
||||
+ ],
|
||||
+ "expectEvents": [
|
||||
+ {
|
||||
+ "client": "client0",
|
||||
+ "events": [
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ },
|
||||
+ "databaseName": "database0"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Mixed operation with snapshot and snapshotTime",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "find",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "session": "session0",
|
||||
+ "filter": {
|
||||
+ "_id": 1
|
||||
+ }
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "getSnapshotTime",
|
||||
+ "object": "session0",
|
||||
+ "saveResultAsEntity": "savedSnapshotTime"
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "findOneAndUpdate",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "filter": {
|
||||
+ "_id": 1
|
||||
+ },
|
||||
+ "update": {
|
||||
+ "$inc": {
|
||||
+ "x": 1
|
||||
+ }
|
||||
+ },
|
||||
+ "returnDocument": "After"
|
||||
+ },
|
||||
+ "expectResult": {
|
||||
+ "_id": 1,
|
||||
+ "x": 12
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "createEntities",
|
||||
+ "object": "testRunner",
|
||||
+ "arguments": {
|
||||
+ "entities": [
|
||||
+ {
|
||||
+ "session": {
|
||||
+ "id": "session2",
|
||||
+ "client": "client0",
|
||||
+ "sessionOptions": {
|
||||
+ "snapshot": true,
|
||||
+ "snapshotTime": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "find",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "filter": {
|
||||
+ "_id": 1
|
||||
+ }
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 12
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "aggregate",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "pipeline": [
|
||||
+ {
|
||||
+ "$match": {
|
||||
+ "_id": 1
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "session": "session2"
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "x": 11
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "name": "distinct",
|
||||
+ "object": "collection0",
|
||||
+ "arguments": {
|
||||
+ "fieldName": "x",
|
||||
+ "filter": {},
|
||||
+ "session": "session2"
|
||||
+ },
|
||||
+ "expectResult": [
|
||||
+ 11
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "expectEvents": [
|
||||
+ {
|
||||
+ "client": "client0",
|
||||
+ "events": [
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "find": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "find": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "$$exists": false
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "aggregate": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "commandStartedEvent": {
|
||||
+ "command": {
|
||||
+ "distinct": "collection0",
|
||||
+ "readConcern": {
|
||||
+ "level": "snapshot",
|
||||
+ "atClusterTime": {
|
||||
+ "$$matchesEntity": "savedSnapshotTime"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,460 +0,0 @@
|
||||
diff --git a/test/client-side-encryption/spec/unified/accessToken-azure.json b/test/client-side-encryption/spec/unified/accessToken-azure.json
|
||||
new file mode 100644
|
||||
index 00000000..510d8795
|
||||
--- /dev/null
|
||||
+++ b/test/client-side-encryption/spec/unified/accessToken-azure.json
|
||||
@@ -0,0 +1,186 @@
|
||||
+{
|
||||
+ "description": "accessToken-azure",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "runOnRequirements": [
|
||||
+ {
|
||||
+ "minServerVersion": "4.1.10",
|
||||
+ "csfle": {
|
||||
+ "minLibmongocryptVersion": "1.6.0"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client",
|
||||
+ "autoEncryptOpts": {
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "azure": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "database": {
|
||||
+ "id": "db",
|
||||
+ "client": "client",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "collection": {
|
||||
+ "id": "coll",
|
||||
+ "database": "db",
|
||||
+ "collectionName": "coll"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "azure": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "initialData": [
|
||||
+ {
|
||||
+ "databaseName": "db",
|
||||
+ "collectionName": "coll",
|
||||
+ "documents": [],
|
||||
+ "createOptions": {
|
||||
+ "validator": {
|
||||
+ "$jsonSchema": {
|
||||
+ "properties": {
|
||||
+ "secret": {
|
||||
+ "encrypt": {
|
||||
+ "keyId": [
|
||||
+ {
|
||||
+ "$binary": {
|
||||
+ "base64": "AZURE+AAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "bsonType": "string",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "bsonType": "object"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "databaseName": "keyvault",
|
||||
+ "collectionName": "datakeys",
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": {
|
||||
+ "$binary": {
|
||||
+ "base64": "AZURE+AAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ },
|
||||
+ "keyAltNames": [
|
||||
+ "my-key"
|
||||
+ ],
|
||||
+ "keyMaterial": {
|
||||
+ "$binary": {
|
||||
+ "base64": "n+HWZ0ZSVOYA3cvQgP7inN4JSXfOH85IngmeQxRpQHjCCcqT3IFqEWNlrsVHiz3AELimHhX4HKqOLWMUeSIT6emUDDoQX9BAv8DR1+E1w4nGs/NyEneac78EYFkK3JysrFDOgl2ypCCTKAypkn9CkAx1if4cfgQE93LW4kczcyHdGiH36CIxrCDGv1UzAvERN5Qa47DVwsM6a+hWsF2AAAJVnF0wYLLJU07TuRHdMrrphPWXZsFgyV+lRqJ7DDpReKNO8nMPLV/mHqHBHGPGQiRdb9NoJo8CvokGz4+KE8oLwzKf6V24dtwZmRkrsDV4iOhvROAzz+Euo1ypSkL3mw==",
|
||||
+ "subType": "00"
|
||||
+ }
|
||||
+ },
|
||||
+ "creationDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "updateDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "status": {
|
||||
+ "$numberInt": "0"
|
||||
+ },
|
||||
+ "masterKey": {
|
||||
+ "provider": "azure",
|
||||
+ "keyVaultEndpoint": "key-vault-csfle.vault.azure.net",
|
||||
+ "keyName": "key-name-csfle"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "Auto encrypt using access token Azure credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "insertOne",
|
||||
+ "arguments": {
|
||||
+ "document": {
|
||||
+ "_id": 1,
|
||||
+ "secret": "string0"
|
||||
+ }
|
||||
+ },
|
||||
+ "object": "coll"
|
||||
+ }
|
||||
+ ],
|
||||
+ "outcome": [
|
||||
+ {
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "secret": {
|
||||
+ "$binary": {
|
||||
+ "base64": "AQGVERPgAAAAAAAAAAAAAAAC5DbBSwPwfSlBrDtRuglvNvCXD1KzDuCKY2P+4bRFtHDjpTOE2XuytPAUaAbXf1orsPq59PVZmsbTZbt2CB8qaQ==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "collectionName": "coll",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Explicit encrypt using access token Azure credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "encrypt",
|
||||
+ "object": "clientEncryption",
|
||||
+ "arguments": {
|
||||
+ "value": "string0",
|
||||
+ "opts": {
|
||||
+ "keyAltName": "my-key",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ },
|
||||
+ "expectResult": {
|
||||
+ "$binary": {
|
||||
+ "base64": "AQGVERPgAAAAAAAAAAAAAAAC5DbBSwPwfSlBrDtRuglvNvCXD1KzDuCKY2P+4bRFtHDjpTOE2XuytPAUaAbXf1orsPq59PVZmsbTZbt2CB8qaQ==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
diff --git a/test/client-side-encryption/spec/unified/accessToken-gcp.json b/test/client-side-encryption/spec/unified/accessToken-gcp.json
|
||||
new file mode 100644
|
||||
index 00000000..f5cf8914
|
||||
--- /dev/null
|
||||
+++ b/test/client-side-encryption/spec/unified/accessToken-gcp.json
|
||||
@@ -0,0 +1,188 @@
|
||||
+{
|
||||
+ "description": "accessToken-gcp",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "runOnRequirements": [
|
||||
+ {
|
||||
+ "minServerVersion": "4.1.10",
|
||||
+ "csfle": {
|
||||
+ "minLibmongocryptVersion": "1.6.0"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client",
|
||||
+ "autoEncryptOpts": {
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "gcp": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "database": {
|
||||
+ "id": "db",
|
||||
+ "client": "client",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "collection": {
|
||||
+ "id": "coll",
|
||||
+ "database": "db",
|
||||
+ "collectionName": "coll"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "gcp": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "initialData": [
|
||||
+ {
|
||||
+ "databaseName": "db",
|
||||
+ "collectionName": "coll",
|
||||
+ "documents": [],
|
||||
+ "createOptions": {
|
||||
+ "validator": {
|
||||
+ "$jsonSchema": {
|
||||
+ "properties": {
|
||||
+ "secret": {
|
||||
+ "encrypt": {
|
||||
+ "keyId": [
|
||||
+ {
|
||||
+ "$binary": {
|
||||
+ "base64": "GCP+AAAAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "bsonType": "string",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "bsonType": "object"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "databaseName": "keyvault",
|
||||
+ "collectionName": "datakeys",
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": {
|
||||
+ "$binary": {
|
||||
+ "base64": "GCP+AAAAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ },
|
||||
+ "keyAltNames": [
|
||||
+ "my-key"
|
||||
+ ],
|
||||
+ "keyMaterial": {
|
||||
+ "$binary": {
|
||||
+ "base64": "CiQAIgLj0WyktnB4dfYHo5SLZ41K4ASQrjJUaSzl5vvVH0G12G0SiQEAjlV8XPlbnHDEDFbdTO4QIe8ER2/172U1ouLazG0ysDtFFIlSvWX5ZnZUrRMmp/R2aJkzLXEt/zf8Mn4Lfm+itnjgo5R9K4pmPNvvPKNZX5C16lrPT+aA+rd+zXFSmlMg3i5jnxvTdLHhg3G7Q/Uv1ZIJskKt95bzLoe0tUVzRWMYXLIEcohnQg==",
|
||||
+ "subType": "00"
|
||||
+ }
|
||||
+ },
|
||||
+ "creationDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "updateDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "status": {
|
||||
+ "$numberInt": "0"
|
||||
+ },
|
||||
+ "masterKey": {
|
||||
+ "provider": "gcp",
|
||||
+ "projectId": "devprod-drivers",
|
||||
+ "location": "global",
|
||||
+ "keyRing": "key-ring-csfle",
|
||||
+ "keyName": "key-name-csfle"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "Auto encrypt using access token GCP credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "insertOne",
|
||||
+ "arguments": {
|
||||
+ "document": {
|
||||
+ "_id": 1,
|
||||
+ "secret": "string0"
|
||||
+ }
|
||||
+ },
|
||||
+ "object": "coll"
|
||||
+ }
|
||||
+ ],
|
||||
+ "outcome": [
|
||||
+ {
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "secret": {
|
||||
+ "$binary": {
|
||||
+ "base64": "ARgj/gAAAAAAAAAAAAAAAAACwFd+Y5Ojw45GUXNvbcIpN9YkRdoHDHkR4kssdn0tIMKlDQOLFkWFY9X07IRlXsxPD8DcTiKnl6XINK28vhcGlg==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "collectionName": "coll",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Explicit encrypt using access token GCP credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "encrypt",
|
||||
+ "object": "clientEncryption",
|
||||
+ "arguments": {
|
||||
+ "value": "string0",
|
||||
+ "opts": {
|
||||
+ "keyAltName": "my-key",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ },
|
||||
+ "expectResult": {
|
||||
+ "$binary": {
|
||||
+ "base64": "ARgj/gAAAAAAAAAAAAAAAAACwFd+Y5Ojw45GUXNvbcIpN9YkRdoHDHkR4kssdn0tIMKlDQOLFkWFY9X07IRlXsxPD8DcTiKnl6XINK28vhcGlg==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
diff --git a/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-azure-accessToken-type.json b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-azure-accessToken-type.json
|
||||
new file mode 100644
|
||||
index 00000000..8fe5c150
|
||||
--- /dev/null
|
||||
+++ b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-azure-accessToken-type.json
|
||||
@@ -0,0 +1,31 @@
|
||||
+{
|
||||
+ "description": "clientEncryptionOpts-kmsProviders-azure-accessToken-type",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption0",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client0",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "azure": {
|
||||
+ "accessToken": 0
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "",
|
||||
+ "operations": []
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
diff --git a/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-gcp-accessToken-type.json b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-gcp-accessToken-type.json
|
||||
new file mode 100644
|
||||
index 00000000..2284e26c
|
||||
--- /dev/null
|
||||
+++ b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-gcp-accessToken-type.json
|
||||
@@ -0,0 +1,31 @@
|
||||
+{
|
||||
+ "description": "clientEncryptionOpts-kmsProviders-gcp-accessToken-type",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption0",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client0",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "gcp": {
|
||||
+ "accessToken": 0
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "",
|
||||
+ "operations": []
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
44
.github/copilot-instructions.md
vendored
44
.github/copilot-instructions.md
vendored
@ -1,44 +0,0 @@
|
||||
When reviewing code, focus on:
|
||||
|
||||
## Security Critical Issues
|
||||
- Check for hardcoded secrets, API keys, or credentials.
|
||||
- Check for instances of potential method call injection, dynamic code execution, symbol injection or other code injection vulnerabilities.
|
||||
|
||||
## Performance Red Flags
|
||||
- Spot inefficient loops and algorithmic issues.
|
||||
- Check for memory leaks and resource cleanup.
|
||||
|
||||
## Code Quality Essentials
|
||||
- Methods should be focused and appropriately sized. If a method is doing too much, suggest refactorings to split it up.
|
||||
- Use clear, descriptive naming conventions.
|
||||
- Avoid encapsulation violations and ensure proper separation of concerns.
|
||||
- All public classes, modules, and methods should have clear documentation in Sphinx format.
|
||||
|
||||
## PyMongo-specific Concerns
|
||||
- Do not review files within `pymongo/synchronous` or files in `test/` that also have a file of the same name in `test/asynchronous` unless the reviewed changes include a `_IS_SYNC` statement. PyMongo generates these files from `pymongo/asynchronous` and `test/asynchronous` using `tools/synchro.py`.
|
||||
- All asynchronous functions must not call any blocking I/O.
|
||||
|
||||
## Review Style
|
||||
- Be specific and actionable in feedback.
|
||||
- Explain the "why" behind recommendations.
|
||||
- Acknowledge good patterns when you see them.
|
||||
- Ask clarifying questions when code intent is unclear.
|
||||
|
||||
Always prioritize security vulnerabilities and performance issues that could impact users.
|
||||
|
||||
Always suggest changes to improve readability and testability. For example, this suggestion seeks to make the code more readable, reusable, and testable:
|
||||
|
||||
```python
|
||||
# Instead of:
|
||||
if user.email and "@" in user.email and len(user.email) > 5:
|
||||
submit_button.enabled = True
|
||||
else:
|
||||
submit_button.enabled = False
|
||||
|
||||
# Consider:
|
||||
def valid_email(email):
|
||||
return email and "@" in email and len(email) > 5
|
||||
|
||||
|
||||
submit_button.enabled = valid_email(user.email)
|
||||
```
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -5,8 +5,6 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
|
||||
27
.github/pull_request_template.md
vendored
27
.github/pull_request_template.md
vendored
@ -1,33 +1,38 @@
|
||||
<!-- Thanks for contributing! -->
|
||||
<!-- Please ensure that the title of the PR is in the following form:
|
||||
[JIRA TICKET]: Issue Title
|
||||
[Issue Type]-[Issue Key]: Issue Title
|
||||
|
||||
If you are an external contributor and there is no JIRA ticket associated with your change, then use your best judgement
|
||||
for the PR title. A MongoDB employee will create a JIRA ticket and edit the name and links as appropriate.
|
||||
|
||||
Note on AI Contributions:
|
||||
We only accept pull requests that are authored and submitted by human contributors who fully understand the changes they are proposing.
|
||||
All contributions must be written and understood by human contributors. Please read about our policy in our contributing guide.
|
||||
-->
|
||||
[JIRA TICKET]
|
||||
[Issue Key](https://jira.mongodb.org/browse/%7BISSUE_KEY%7D)
|
||||
## Summary
|
||||
<!-- What conceptually is this PR introducing? If context is already provided from the JIRA ticket, still place it in the
|
||||
Pull Request as you should not make the reviewer do digging for a basic summary. -->
|
||||
|
||||
## Changes in this PR
|
||||
<!-- What changes did you make to the code? What new APIs (public or private) were added, removed, or edited to generate
|
||||
the desired outcome explained in the above summary? -->
|
||||
|
||||
## Test Plan
|
||||
## Testing Plan
|
||||
<!-- How did you test the code? If you added unit tests, you can say that. If you didn’t introduce unit tests, explain why.
|
||||
All code should be tested in some way – so please list what your validation strategy was. -->
|
||||
|
||||
### Screenshots (optional)
|
||||
<!-- Usually a great supplement to a test plan, especially if this requires local testing. -->
|
||||
|
||||
## Checklist
|
||||
<!-- Do not delete the items provided on this checklist. -->
|
||||
|
||||
### Checklist for Author
|
||||
- [ ] Did you update the changelog (if necessary)?
|
||||
- [ ] Is there test coverage?
|
||||
- [ ] Is any followup work tracked in a JIRA ticket? If so, add link(s).
|
||||
- [ ] Is the intention of the code captured in relevant tests?
|
||||
- [ ] If there are new TODOs, has a related JIRA ticket been created?
|
||||
|
||||
### Checklist for Reviewer
|
||||
### Checklist for Reviewer {@primary_reviewer}
|
||||
- [ ] Does the title of the PR reference a JIRA Ticket?
|
||||
- [ ] Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
|
||||
- [ ] Have you checked for spelling & grammar errors?
|
||||
- [ ] Is all relevant documentation (README or docstring) updated?
|
||||
|
||||
## Focus Areas for Reviewer (optional)
|
||||
<!-- List any complex portion of code you believe needs additional scrutiny and explain why. -->
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
|
||||
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@ -63,6 +63,6 @@ jobs:
|
||||
pip install -e .
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
|
||||
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
17
.github/workflows/dist.yml
vendored
17
.github/workflows/dist.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout pymongo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: runner.os == 'Linux'
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
with:
|
||||
# setup-qemu-action by default uses `tonistiigi/binfmt:latest` image,
|
||||
# which is out of date. This causes seg faults during build.
|
||||
@ -83,7 +83,6 @@ jobs:
|
||||
- name: Assert all versions in wheelhouse
|
||||
if: ${{ ! startsWith(matrix.buildplat[1], 'macos') }}
|
||||
run: |
|
||||
ls wheelhouse/*cp39*.whl
|
||||
ls wheelhouse/*cp310*.whl
|
||||
ls wheelhouse/*cp311*.whl
|
||||
ls wheelhouse/*cp312*.whl
|
||||
@ -92,7 +91,7 @@ jobs:
|
||||
# Free-threading builds:
|
||||
ls wheelhouse/*cp314t*.whl
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheel-${{ matrix.buildplat[1] }}
|
||||
path: ./wheelhouse/*.whl
|
||||
@ -102,7 +101,7 @@ jobs:
|
||||
name: Make SDist
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@ -111,7 +110,7 @@ jobs:
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
# Build sdist on lowest supported Python
|
||||
python-version: "3.9"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Build SDist
|
||||
run: |
|
||||
@ -125,7 +124,7 @@ jobs:
|
||||
cd ..
|
||||
python -c "from pymongo import has_c; assert has_c()"
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "sdist"
|
||||
path: ./dist/*.tar.gz
|
||||
@ -136,13 +135,13 @@ jobs:
|
||||
name: Download Wheels
|
||||
steps:
|
||||
- name: Download all workflow run artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v5
|
||||
- name: Flatten directory
|
||||
working-directory: .
|
||||
run: |
|
||||
find . -mindepth 2 -type f -exec mv {} . \;
|
||||
find . -type d -empty -delete
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: all-dist-${{ github.run_id }}
|
||||
path: "./*"
|
||||
|
||||
2
.github/workflows/release-python.yml
vendored
2
.github/workflows/release-python.yml
vendored
@ -75,7 +75,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: all-dist-${{ github.run_id }}
|
||||
path: dist/
|
||||
|
||||
104
.github/workflows/sbom.yml
vendored
104
.github/workflows/sbom.yml
vendored
@ -1,104 +0,0 @@
|
||||
name: Generate SBOM
|
||||
|
||||
# This workflow uses cyclonedx-py and publishes an sbom.json artifact.
|
||||
# It runs on manual trigger or when package files change on main branch,
|
||||
# and creates a PR with the updated SBOM.
|
||||
# Internal documentation: go/sbom-scope
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches: ['master']
|
||||
paths:
|
||||
- 'requirements.txt'
|
||||
- 'requirements/**.txt'
|
||||
- '!requirements/docs.txt'
|
||||
- '!requirements/test.txt'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sbom:
|
||||
name: Generate SBOM and Create PR
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: sbom-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python tools/generate_sbom_requirements.py
|
||||
pip install -r sbom-requirements.txt
|
||||
pip install .
|
||||
pip uninstall -y pip setuptools
|
||||
deactivate
|
||||
python -m venv .venv-sbom
|
||||
source .venv-sbom/bin/activate
|
||||
pip install cyclonedx-bom==7.2.1
|
||||
cyclonedx-py environment --spec-version 1.5 --output-format JSON --output-file sbom.json .venv
|
||||
# Add PURL for pymongo (local package doesn't get PURL automatically)
|
||||
jq '(.components[] | select(.name == "pymongo" and .purl == null)) |= (. + {purl: ("pkg:pypi/pymongo@" + .version)})' sbom.json > sbom.tmp.json && mv sbom.tmp.json sbom.json
|
||||
|
||||
- name: Download CycloneDX CLI
|
||||
run: |
|
||||
curl -L -s -o /tmp/cyclonedx "https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.29.1/cyclonedx-linux-x64"
|
||||
chmod +x /tmp/cyclonedx
|
||||
|
||||
- name: Validate SBOM
|
||||
run: /tmp/cyclonedx validate --input-file sbom.json --fail-on-errors
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -rf .venv .venv-sbom sbom-requirements.txt
|
||||
|
||||
- name: Upload SBOM artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sbom
|
||||
path: sbom.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: Update SBOM after dependency changes'
|
||||
branch: auto-update-sbom-${{ github.run_id }}
|
||||
delete-branch: true
|
||||
title: 'Automation: Update SBOM'
|
||||
body: |
|
||||
## Automated SBOM Update
|
||||
|
||||
This PR was automatically generated because dependency manifest files changed.
|
||||
|
||||
### Changes
|
||||
- Updated `sbom.json` to reflect current dependencies
|
||||
|
||||
### Verification
|
||||
The SBOM was generated using cyclonedx-py v7.2.1 with the current Python environment.
|
||||
|
||||
### Triggered by
|
||||
- Commit: ${{ github.sha }}
|
||||
- Workflow run: ${{ github.run_id }}
|
||||
|
||||
---
|
||||
_This PR was created automatically by the [SBOM workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})_
|
||||
labels: |
|
||||
sbom
|
||||
automated
|
||||
dependencies
|
||||
75
.github/workflows/test-python.yml
vendored
75
.github/workflows/test-python.yml
vendored
@ -22,11 +22,11 @@ jobs:
|
||||
static:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -64,11 +64,11 @@ jobs:
|
||||
|
||||
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@ -79,46 +79,15 @@ jobs:
|
||||
- name: Run tests
|
||||
run: uv run --extra test pytest -v
|
||||
|
||||
coverage:
|
||||
# This enables a coverage report for a given PR, which will be augmented by
|
||||
# the combined codecov report uploaded in Evergreen.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Coverage
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
- id: setup-mongodb
|
||||
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||
with:
|
||||
version: "8.0"
|
||||
- name: Install just
|
||||
run: uv tool install rust-just
|
||||
- name: Setup tests
|
||||
run: COVERAGE=1 just setup-tests
|
||||
- name: Run tests
|
||||
run: just run-tests
|
||||
- name: Generate xml report
|
||||
run: uv tool run --with "coverage[toml]" coverage xml
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
doctest:
|
||||
runs-on: ubuntu-latest
|
||||
name: DocTest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -139,11 +108,11 @@ jobs:
|
||||
name: Docs Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -158,11 +127,11 @@ jobs:
|
||||
name: Link Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -180,11 +149,11 @@ jobs:
|
||||
matrix:
|
||||
python: ["3.10", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "${{matrix.python}}"
|
||||
@ -201,11 +170,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Integration Tests
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -231,7 +200,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Make an sdist"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
@ -239,13 +208,13 @@ jobs:
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'pyproject.toml'
|
||||
# Build sdist on lowest supported Python
|
||||
python-version: "3.9"
|
||||
python-version: "3.10"
|
||||
- name: Build SDist
|
||||
shell: bash
|
||||
run: |
|
||||
pip install build
|
||||
python -m build --sdist
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "sdist"
|
||||
path: dist/*.tar.gz
|
||||
@ -257,7 +226,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Download sdist
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: sdist/
|
||||
- name: Unpack SDist
|
||||
@ -273,7 +242,7 @@ jobs:
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'sdist/test/pyproject.toml'
|
||||
# Test sdist on lowest supported Python
|
||||
python-version: "3.9"
|
||||
python-version: "3.10"
|
||||
- id: setup-mongodb
|
||||
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||
- name: Run connect test from sdist
|
||||
@ -291,13 +260,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test minimum dependencies and Python
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
python-version: "3.9"
|
||||
python-version: "3.10"
|
||||
- id: setup-mongodb
|
||||
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||
with:
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@ -14,8 +14,8 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
uses: zizmorcore/zizmor-action@da5ac40c5419dcf7f21630fb2f95e725ae8fb9d5
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -41,6 +41,4 @@ test/lambda/*.json
|
||||
|
||||
# test results and logs
|
||||
xunit-results/
|
||||
coverage.xml
|
||||
server.log
|
||||
.coverage
|
||||
|
||||
116
CONTRIBUTING.md
116
CONTRIBUTING.md
@ -16,7 +16,7 @@ be of interest or that has already been addressed.
|
||||
|
||||
## Supported Interpreters
|
||||
|
||||
PyMongo supports CPython 3.9+ and PyPy3.9+. Language features not
|
||||
PyMongo supports CPython 3.10+ and PyPy3.10+. Language features not
|
||||
supported by all interpreters can not be used.
|
||||
|
||||
## Style Guide
|
||||
@ -85,53 +85,49 @@ likelihood for getting review sooner shoots up.
|
||||
- `versionadded:: 3.11`
|
||||
- `versionchanged:: 3.5`
|
||||
|
||||
### AI-Generated Contributions Policy
|
||||
**Pull Request Template Breakdown**
|
||||
|
||||
#### Our Stance
|
||||
- **Github PR Title**
|
||||
|
||||
We only accept pull requests that are authored and submitted by human contributors who fully understand the changes they are proposing. Pull requests that are not clearly owned and understood by a human contributor may be closed. **All contributions must be submitted, reviewed, and understood by human contributors.**
|
||||
- The PR Title format should always be
|
||||
`[JIRA-ID] : Jira Title or Blurb Summary`.
|
||||
|
||||
##### Why This Policy Exists
|
||||
- **JIRA LINK**
|
||||
|
||||
At MongoDB, we understand the power and prevalence of AI tools in software development. With that being said, many MongoDB libraries are foundational tools used in production systems worldwide. The nature of these libraries requires:
|
||||
- Convenient link to the associated JIRA ticket.
|
||||
|
||||
- **Deep domain expertise**: MongoDB's wire protocol, BSON specification, connection pooling, authentication mechanisms, and concurrency patterns require an understanding that AI alone cannot substantiate.
|
||||
- **Summary**
|
||||
|
||||
- **Long-term maintainability**: Contributors need to be able to explain *why* code is written a certain way, explain design decisions, and be available to iterate on their contributions.
|
||||
- Small blurb on why this is needed. The JIRA task should have
|
||||
the more in-depth description, but this should still, at a
|
||||
high level, give anyone looking an understanding of why the
|
||||
PR has been checked in.
|
||||
|
||||
- **Security responsibility**: Authentication, credential handling, and TLS implementation cannot be left to probabilistic code generation.
|
||||
- **Changes in this PR**
|
||||
|
||||
##### What This Means for Contributors
|
||||
- The explicit code changes that this PR is introducing. This
|
||||
should be more specific than just the task name. (Unless the
|
||||
task name is very clear).
|
||||
|
||||
**Required:**
|
||||
- **Test Plan**
|
||||
|
||||
- Full understanding of every line of code you submit
|
||||
- Ability to explain and defend your implementation choices
|
||||
- Willingness to iterate and maintain your contributions
|
||||
- Everything needs a test description. Describe what you did
|
||||
to validate your changes actually worked; if you did
|
||||
nothing, then document you did not test it. Aim to make
|
||||
these steps reproducible by other engineers, specifically
|
||||
with your primary reviewer in mind.
|
||||
|
||||
**Encouraged:**
|
||||
- **Screenshots**
|
||||
|
||||
- Using AI assistants as learning tools to understand concepts
|
||||
- IDE autocomplete features that suggest standard patterns
|
||||
- AI help for brainstorming approaches (but write the code yourself)
|
||||
- Writing code using AI tools, reviewing each line and revising code as necessary.
|
||||
- Any images that provide more context to the PR. Usually,
|
||||
these just coincide with the test plan.
|
||||
|
||||
**Not allowed:**
|
||||
- **Callouts or follow-up items**
|
||||
|
||||
- Submitting PRs generated solely by AI tools
|
||||
- Copy-pasting AI-generated code without full understanding
|
||||
|
||||
##### Disclosure
|
||||
|
||||
If you used AI assistance in any way during your contribution, please disclose what the AI assistant was used for in your PR description. We would love to know what tools developers have found useful in iterating in their day to day.
|
||||
|
||||
##### Questions?
|
||||
|
||||
If you're unsure whether your contribution complies with this policy, please ask for guidance within the scope of the PR and clarify any uncertainty. We're happy to guide contributors toward successful contributions.
|
||||
|
||||
---
|
||||
|
||||
*This policy helps us maintain the reliability, security, and trustworthiness that production applications depend on. Thank you for understanding and for contributing thoughtfully to PyMongo.*
|
||||
- This is a good place for identifying "to-dos" that you've
|
||||
placed in the code (Must have an accompanying JIRA Ticket).
|
||||
- Potential bugs that you are unsure how to test in the code.
|
||||
- Opinions you want to receive about your code.
|
||||
|
||||
## Running Linters
|
||||
|
||||
@ -201,7 +197,7 @@ the pages will re-render and the browser will automatically refresh.
|
||||
version of Python, set `UV_PYTHON` before running `just install`.
|
||||
- Ensure you have started the appropriate Mongo Server(s). You can run `just run-server` with optional args
|
||||
to set up the server. All given options will be passed to
|
||||
[`run-mongodb.sh`](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/run-mongodb.sh). Run `$DRIVERS_TOOLS/.evergreen/run-mongodb.sh start -h`
|
||||
[`run-orchestration.sh`](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/run-orchestration.sh). Run `$DRIVERS_TOOLS/evergreen/run-orchestration.sh -h`
|
||||
for a full list of options.
|
||||
- Run `just test` or `pytest` to run all of the tests.
|
||||
- Append `test/<mod_name>.py::<class_name>::<test_name>` to run
|
||||
@ -209,7 +205,6 @@ the pages will re-render and the browser will automatically refresh.
|
||||
and the `<class_name>` to test a full module. For example:
|
||||
`just test test/test_change_stream.py::TestUnifiedChangeStreamsErrors::test_change_stream_errors_on_ElectionInProgress`.
|
||||
- Use the `-k` argument to select tests by pattern.
|
||||
- Run `just test-coverage` to run tests with coverage and display a report. After running tests with coverage, use `just coverage-html` to generate an HTML report in `htmlcov/index.html`.
|
||||
|
||||
|
||||
## Running tests that require secrets, services, or other configuration
|
||||
@ -392,16 +387,11 @@ If you are running one of the `no-responder` tests, omit the `run-server` step.
|
||||
To run any of the test suites with minimum supported dependencies, pass `--test-min-deps` to
|
||||
`just setup-tests`.
|
||||
|
||||
## Testing time-dependent operations
|
||||
|
||||
- `test.utils_shared.delay` - One can trigger an arbitrarily long-running operation on the server using this delay utility
|
||||
in combination with a `$where` operation. Use this to test behaviors around timeouts or signals.
|
||||
|
||||
## Adding a new test suite
|
||||
|
||||
- If adding new tests files that should only be run for that test suite, add a pytest marker to the file and add
|
||||
to the list of pytest markers in `pyproject.toml`. Then add the test suite to the `TEST_SUITE_MAP` in `.evergreen/scripts/utils.py`. If for some reason it is not a pytest-runnable test, add it to the list of `EXTRA_TESTS` instead.
|
||||
- If the test uses Atlas or otherwise doesn't use `run-mongodb.sh`, add it to the `NO_RUN_ORCHESTRATION` list in
|
||||
- If the test uses Atlas or otherwise doesn't use `run-orchestration.sh`, add it to the `NO_RUN_ORCHESTRATION` list in
|
||||
`.evergreen/scripts/utils.py`.
|
||||
- If there is something special required to run the local server or there is an extra flag that should always be set
|
||||
like `AUTH`, add that logic to `.evergreen/scripts/run_server.py`.
|
||||
@ -492,7 +482,6 @@ results into the patch file.
|
||||
For example: the imaginary, unimplemented PYTHON-1234 ticket has associated spec test changes. To add those changes to `PYTHON-1234.patch`), do the following:
|
||||
```bash
|
||||
git diff HEAD~1 path/to/file >> .evergreen/spec-patch/PYTHON-1234.patch
|
||||
```
|
||||
|
||||
#### Running Locally
|
||||
Both `resync-all-specs.sh` and `resync-all-specs.py` can be run locally (and won't generate a PR).
|
||||
@ -505,20 +494,13 @@ python3 ./.evergreen/scripts/resync-all-specs.py
|
||||
|
||||
Follow the [Python Driver Release Process Wiki](https://wiki.corp.mongodb.com/display/DRIVERS/Python+Driver+Release+Process).
|
||||
|
||||
## Project Structure and Asyncio Considerations
|
||||
## Asyncio considerations
|
||||
|
||||
This section describes the layout of the `pymongo/` package.
|
||||
PyMongo adds asyncio capability by modifying the source files in `*/asynchronous` to `*/synchronous` using
|
||||
[unasync](https://github.com/python-trio/unasync/) and some custom transforms.
|
||||
|
||||
Within `pymongo/`, the code is further divided into the `pymongo/asynchronous` and `pymongo/synchronous` subdirectories.
|
||||
Files in `pymongo/synchronous` are generated from `pymongo/asynchronous` using the `synchro` pre-commit hook, which uses [unasync](https://github.com/python-trio/unasync/) and some custom transforms.
|
||||
|
||||
As a result, **all modifications** within `pymongo` must be made in either the top-level `pymongo` directory when they have to exhibit differing behavior between sync and async contexts or the `pymongo/asynchronous` directory, not `pymongo/synchronous`.
|
||||
Any changes made directly to files in the `pymongo/synchronous` directory will be overwritten by the `synchro` hook when it is run, which happens automatically on commit.
|
||||
|
||||
Some top-level files (e.g. `pymongo/collection.py`) are re-export files for existing import compatibility and should not be modified directly.
|
||||
The other top-level files (e.g. `pymongo/network_layer.py`, `pymongo/pool_shared.py`) contain either shared code used in both the asynchronous and synchronous APIs, or code that is very different between the two APIs and therefore cannot be generated from the async version using `synchro`.
|
||||
|
||||
Run `pre-commit run --all-files synchro` before running tests to generate the latest version of the synchronous code.
|
||||
Where possible, edit the code in `*/asynchronous/*.py` and not the synchronous files.
|
||||
You can run `pre-commit run --all-files synchro` before running tests if you are testing synchronous code.
|
||||
|
||||
To prevent the `synchro` hook from accidentally overwriting code, it first checks to see whether a sync version
|
||||
of a file is changing and not its async counterpart, and will fail.
|
||||
@ -537,10 +519,8 @@ Use this generated file as a starting point for the completed conversion.
|
||||
|
||||
The script is used like so: `python tools/convert_test_to_async.py [test_file.py]`
|
||||
|
||||
## CPU profiling
|
||||
|
||||
## Generating a flame graph using py-spy
|
||||
To profile a test script and generate a flame graph, follow these steps:
|
||||
|
||||
1. Install `py-spy` if you haven't already:
|
||||
```bash
|
||||
pip install py-spy
|
||||
@ -550,26 +530,6 @@ To profile a test script and generate a flame graph, follow these steps:
|
||||
(Note: on macOS you will need to run this command using `sudo` to allow `py-spy` to attach to the Python process.)
|
||||
4. If you need to include native code (for example the C extensions), profiling should be done on a Linux system, as macOS and Windows do not support the `--native` option of `py-spy`.
|
||||
Creating an ubuntu Evergreen spawn host and using `scp` to copy the flamegraph `.svg` file back to your local machine is the best way to do this.
|
||||
5. You can then view the flamegraph using an SVG viewer like a browser.
|
||||
|
||||
## Memory profiling
|
||||
|
||||
To test for a memory leak or any memory-related issues, the current best tool is [memray](https://bloomberg.github.io/memray/overview.html).
|
||||
In order to include code from our C extensions, it must be run in native mode, on Linux.
|
||||
To do so, either spin up an Ubuntu docker container or an Ubuntu Evergreen spawn host.
|
||||
|
||||
From the spawn host or Ubuntu image, do the following:
|
||||
|
||||
1. Install `memray` if you haven't already:
|
||||
```bash
|
||||
pip install memray
|
||||
```
|
||||
2. Inside your test script, perform any required setup and then loop over the code you want to profile for improved sampling.
|
||||
3. Run memray with the script under test with the `--native` flag, e.g. `python -m memray run --native -o test.bin <path/to/script>`.
|
||||
4. Generate the flamegraph with `python -m memray flamegraph -o test.html test.bin`.
|
||||
See the [docs](https://bloomberg.github.io/memray/flamegraph.html) for more options.
|
||||
5. Then, from the host computer, use either scp or docker cp to copy the flamegraph, e.g. `scp ubuntu@ec2-3-82-52-49.compute-1.amazonaws.com:/home/ubuntu/test.html .`.
|
||||
6. You can then view the flamegraph html in a browser.
|
||||
|
||||
## Dependabot updates
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
[](https://pypi.org/project/pymongo)
|
||||
[](https://pepy.tech/project/pymongo)
|
||||
[](http://pymongo.readthedocs.io/en/stable/api?badge=stable)
|
||||
[](https://codecov.io/gh/mongodb/mongo-python-driver)
|
||||
|
||||
## About
|
||||
|
||||
@ -98,7 +97,7 @@ package that is incompatible with PyMongo.
|
||||
|
||||
## Dependencies
|
||||
|
||||
PyMongo supports CPython 3.9+ and PyPy3.9+.
|
||||
PyMongo supports CPython 3.10+ and PyPy3.10+.
|
||||
|
||||
Required dependencies:
|
||||
|
||||
@ -140,8 +139,7 @@ python -m pip install "pymongo[snappy]"
|
||||
```
|
||||
|
||||
Wire protocol compression with zstandard requires
|
||||
[backports.zstd](https://pypi.org/project/backports.zstd)
|
||||
when used with Python versions before 3.14:
|
||||
[zstandard](https://pypi.org/project/zstandard):
|
||||
|
||||
```bash
|
||||
python -m pip install "pymongo[zstd]"
|
||||
@ -216,4 +214,4 @@ pip install -e ".[test]"
|
||||
pytest
|
||||
```
|
||||
|
||||
For more advanced testing scenarios, see the [contributing guide](https://github.com/mongodb/mongo-python-driver/blob/master/CONTRIBUTING.md#running-tests-locally).
|
||||
For more advanced testing scenarios, see the [contributing guide](./CONTRIBUTING.md#running-tests-locally).
|
||||
|
||||
@ -1109,9 +1109,7 @@ def _decode_all(data: _ReadableBuffer, opts: CodecOptions[_DocumentType]) -> lis
|
||||
while position < end:
|
||||
obj_size = _UNPACK_INT_FROM(data, position)[0]
|
||||
if data_len - position < obj_size:
|
||||
raise InvalidBSON(
|
||||
f"invalid object size: expected {obj_size}, got {data_len - position}"
|
||||
)
|
||||
raise InvalidBSON("invalid object size")
|
||||
obj_end = position + obj_size - 1
|
||||
if data[obj_end] != 0:
|
||||
raise InvalidBSON("bad eoo")
|
||||
|
||||
@ -109,7 +109,6 @@ struct module_state {
|
||||
#define DATETIME_CLAMP 2
|
||||
#define DATETIME_MS 3
|
||||
#define DATETIME_AUTO 4
|
||||
#define PYTHON_3_12 0x030C0000
|
||||
|
||||
/* Converts integer to its string representation in decimal notation. */
|
||||
extern int cbson_long_long_to_str(long long num, char* str, size_t size) {
|
||||
@ -250,67 +249,6 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
*/
|
||||
static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw);
|
||||
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
/* Transfer traceback from old_exc to new_exc.
|
||||
* Steals reference to old_exc. */
|
||||
static PyObject* _transfer_traceback(PyObject *old_exc, PyObject *new_exc) {
|
||||
PyObject *tb = PyException_GetTraceback(old_exc);
|
||||
if (tb) {
|
||||
PyException_SetTraceback(new_exc, tb);
|
||||
Py_DECREF(tb);
|
||||
}
|
||||
Py_DECREF(old_exc);
|
||||
return new_exc;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Rewrap the current exception as InvalidBSON(str(e)) if it is not already an InvalidBSON error. */
|
||||
static void _rewrap_as_invalid_bson(void) {
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
PyObject *exc = PyErr_GetRaisedException();
|
||||
if (exc && PyErr_GivenExceptionMatches(exc, PyExc_Exception)) {
|
||||
PyObject *InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
if (!PyErr_GivenExceptionMatches(exc, InvalidBSON)) {
|
||||
PyObject *err_msg = PyObject_Str(exc);
|
||||
if (err_msg) {
|
||||
PyObject *new_exc = PyObject_CallOneArg(InvalidBSON, err_msg);
|
||||
if (new_exc) {
|
||||
exc = _transfer_traceback(exc, new_exc);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(err_msg);
|
||||
}
|
||||
Py_DECREF(InvalidBSON);
|
||||
}
|
||||
}
|
||||
/* Steals reference to exc. */
|
||||
PyErr_SetRaisedException(exc);
|
||||
#else
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *InvalidBSON = NULL;
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
|
||||
InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
if (!PyErr_GivenExceptionMatches(etype, InvalidBSON)) {
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidBSON;
|
||||
if (evalue) {
|
||||
PyObject *msg = PyObject_Str(evalue);
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
} else {
|
||||
Py_DECREF(InvalidBSON);
|
||||
}
|
||||
}
|
||||
}
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Date stuff */
|
||||
static PyObject* datetime_from_millis(long long millis) {
|
||||
/* To encode a datetime instance like datetime(9999, 12, 31, 23, 59, 59, 999999)
|
||||
@ -356,57 +294,34 @@ static PyObject* datetime_from_millis(long long millis) {
|
||||
timeinfo.tm_sec,
|
||||
microseconds);
|
||||
if(!datetime) {
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
PyObject *exc = PyErr_GetRaisedException();
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
|
||||
/* Only add additional error message on ValueError exceptions. */
|
||||
if (exc && PyErr_GivenExceptionMatches(exc, PyExc_ValueError)) {
|
||||
PyObject* err_msg = PyObject_Str(exc);
|
||||
/*
|
||||
* Calling _error clears the error state, so fetch it first.
|
||||
*/
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
|
||||
/* Only add addition error message on ValueError exceptions. */
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_ValueError)) {
|
||||
if (evalue) {
|
||||
PyObject* err_msg = PyObject_Str(evalue);
|
||||
if (err_msg) {
|
||||
PyObject* appendage = PyUnicode_FromString(" (Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO) or MongoClient(datetime_conversion='DATETIME_AUTO')). See: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes");
|
||||
if (appendage) {
|
||||
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
|
||||
if (msg) {
|
||||
PyObject* new_exc = PyObject_CallOneArg(PyExc_ValueError, msg);
|
||||
if (new_exc) {
|
||||
exc = _transfer_traceback(exc, new_exc);
|
||||
}
|
||||
Py_DECREF(msg);
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
}
|
||||
Py_XDECREF(appendage);
|
||||
}
|
||||
Py_XDECREF(err_msg);
|
||||
}
|
||||
/* Steals reference to exc. */
|
||||
PyErr_SetRaisedException(exc);
|
||||
#else
|
||||
/* Calling _error clears the error state, so fetch it first.*/
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
|
||||
/* Only add additional error message on ValueError exceptions. */
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_ValueError)) {
|
||||
if (evalue) {
|
||||
PyObject* err_msg = PyObject_Str(evalue);
|
||||
if (err_msg) {
|
||||
PyObject* appendage = PyUnicode_FromString(" (Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO) or MongoClient(datetime_conversion='DATETIME_AUTO')). See: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes");
|
||||
if (appendage) {
|
||||
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
|
||||
if (msg) {
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
}
|
||||
Py_XDECREF(appendage);
|
||||
}
|
||||
Py_XDECREF(err_msg);
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
}
|
||||
/* Steals references to args. */
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
#endif
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
}
|
||||
/* Steals references to args. */
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
}
|
||||
return datetime;
|
||||
}
|
||||
@ -441,8 +356,7 @@ static PyObject* datetime_ms_from_millis(PyObject* self, long long millis){
|
||||
if (!(ll_millis = PyLong_FromLongLong(millis))){
|
||||
return NULL;
|
||||
}
|
||||
PyObject* args[1] = {ll_millis};
|
||||
dt = PyObject_Vectorcall(state->DatetimeMS, args, 1, NULL);
|
||||
dt = PyObject_CallFunctionObjArgs(state->DatetimeMS, ll_millis, NULL);
|
||||
Py_DECREF(ll_millis);
|
||||
return dt;
|
||||
}
|
||||
@ -487,9 +401,7 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
||||
int64_t min_millis_offset = 0;
|
||||
int64_t max_millis_offset = 0;
|
||||
if (options->tz_aware && options->tzinfo && options->tzinfo != Py_None) {
|
||||
PyObject* utcoffset_args[2] = {options->tzinfo, state->min_datetime};
|
||||
PyObject* utcoffset = PyObject_VectorcallMethod(
|
||||
state->_utcoffset_str, utcoffset_args, 2, NULL);
|
||||
PyObject* utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->min_datetime, NULL);
|
||||
if (utcoffset == NULL) {
|
||||
return 0;
|
||||
}
|
||||
@ -508,9 +420,7 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
||||
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
|
||||
}
|
||||
Py_DECREF(utcoffset);
|
||||
utcoffset_args[1] = state->max_datetime;
|
||||
utcoffset = PyObject_VectorcallMethod(
|
||||
state->_utcoffset_str, utcoffset_args, 2, NULL);
|
||||
utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->max_datetime, NULL);
|
||||
if (utcoffset == NULL) {
|
||||
return 0;
|
||||
}
|
||||
@ -571,9 +481,7 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
||||
|
||||
/* convert to local time */
|
||||
if (options->tzinfo != Py_None) {
|
||||
PyObject* astimezone_args[2] = {value, options->tzinfo};
|
||||
PyObject* temp = PyObject_VectorcallMethod(
|
||||
state->_astimezone_str, astimezone_args, 2, NULL);
|
||||
PyObject* temp = PyObject_CallMethodObjArgs(value, state->_astimezone_str, options->tzinfo, NULL);
|
||||
Py_DECREF(value);
|
||||
value = temp;
|
||||
}
|
||||
@ -780,8 +688,7 @@ static int _load_python_objects(PyObject* module) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
PyObject* compile_args[1] = {empty_string};
|
||||
compiled = PyObject_Vectorcall(re_compile, compile_args, 1, NULL);
|
||||
compiled = PyObject_CallFunction(re_compile, "O", empty_string);
|
||||
Py_DECREF(re_compile);
|
||||
if (compiled == NULL) {
|
||||
state->REType = NULL;
|
||||
@ -804,19 +711,13 @@ static long _type_marker(PyObject* object, PyObject* _type_marker_str) {
|
||||
PyObject* type_marker = NULL;
|
||||
long type = 0;
|
||||
|
||||
#if PY_VERSION_HEX >= 0x030D0000
|
||||
// 3.13
|
||||
if (PyObject_GetOptionalAttr(object, _type_marker_str, &type_marker) == -1) {
|
||||
if (PyObject_HasAttr(object, _type_marker_str)) {
|
||||
type_marker = PyObject_GetAttr(object, _type_marker_str);
|
||||
if (type_marker == NULL) {
|
||||
return -1;
|
||||
}
|
||||
# else
|
||||
if (PyObject_HasAttr(object, _type_marker_str)) {
|
||||
type_marker = PyObject_GetAttr(object, _type_marker_str);
|
||||
if (type_marker == NULL) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* Python objects with broken __getattr__ implementations could return
|
||||
* arbitrary types for a call to PyObject_GetAttrString. For example
|
||||
@ -913,7 +814,6 @@ int convert_codec_options(PyObject* self, PyObject* options_obj, codec_options_t
|
||||
}
|
||||
|
||||
options->is_raw_bson = (101 == type_marker);
|
||||
options->is_dict_class = (options->document_class == (PyObject*)&PyDict_Type);
|
||||
options->options_obj = options_obj;
|
||||
|
||||
Py_INCREF(options->options_obj);
|
||||
@ -1113,20 +1013,10 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
}
|
||||
/*
|
||||
* Use _type_marker attribute instead of PyObject_IsInstance for better perf.
|
||||
*
|
||||
* Skip _type_marker lookup for common built-in types
|
||||
* that we know don't have a _type_marker attribute. This avoids the overhead
|
||||
* of PyObject_HasAttr/PyObject_GetAttr calls for the most common cases.
|
||||
*/
|
||||
if (PyUnicode_CheckExact(value) || PyLong_CheckExact(value) || PyFloat_CheckExact(value) ||
|
||||
PyBool_Check(value) || PyDict_CheckExact(value) || PyList_CheckExact(value) ||
|
||||
PyTuple_CheckExact(value) || PyBytes_CheckExact(value) || value == Py_None) {
|
||||
type = 0;
|
||||
} else {
|
||||
type = _type_marker(value, state->_type_marker_str);
|
||||
if (type < 0) {
|
||||
return 0;
|
||||
}
|
||||
type = _type_marker(value, state->_type_marker_str);
|
||||
if (type < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@ -1337,9 +1227,7 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
case 100:
|
||||
{
|
||||
/* DBRef */
|
||||
PyObject* as_doc_args[1] = {value};
|
||||
PyObject* as_doc = PyObject_VectorcallMethod(
|
||||
state->_as_doc_str, as_doc_args, 1, NULL);
|
||||
PyObject* as_doc = PyObject_CallMethodObjArgs(value, state->_as_doc_str, NULL);
|
||||
if (!as_doc) {
|
||||
return 0;
|
||||
}
|
||||
@ -1495,9 +1383,7 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
return write_unicode(buffer, value);
|
||||
} else if (PyDateTime_Check(value)) {
|
||||
long long millis;
|
||||
PyObject* utcoffset_args[1] = {value};
|
||||
PyObject* utcoffset = PyObject_VectorcallMethod(
|
||||
state->_utcoffset_str, utcoffset_args, 1, NULL);
|
||||
PyObject* utcoffset = PyObject_CallMethodObjArgs(value, state->_utcoffset_str , NULL);
|
||||
if (utcoffset == NULL)
|
||||
return 0;
|
||||
if (utcoffset != Py_None) {
|
||||
@ -1536,9 +1422,7 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
if (!(uuid_rep_obj = PyLong_FromLong(options->uuid_rep))) {
|
||||
return 0;
|
||||
}
|
||||
PyObject* from_uuid_args[3] = {state->Binary, value, uuid_rep_obj};
|
||||
binary_value = PyObject_VectorcallMethod(
|
||||
state->_from_uuid_str, from_uuid_args, 3, NULL);
|
||||
binary_value = PyObject_CallMethodObjArgs(state->Binary, state->_from_uuid_str, value, uuid_rep_obj, NULL);
|
||||
Py_DECREF(uuid_rep_obj);
|
||||
|
||||
if (binary_value == NULL) {
|
||||
@ -1568,8 +1452,7 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
if (converter != NULL) {
|
||||
/* Transform types that have a registered converter.
|
||||
* A new reference is created upon transformation. */
|
||||
PyObject* converter_args[1] = {value};
|
||||
new_value = PyObject_Vectorcall(converter, converter_args, 1, NULL);
|
||||
new_value = PyObject_CallFunctionObjArgs(converter, value, NULL);
|
||||
if (new_value == NULL) {
|
||||
return 0;
|
||||
}
|
||||
@ -1583,9 +1466,8 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
/* Try the fallback encoder if one is provided and we have not already
|
||||
* attempted to use the fallback encoder. */
|
||||
if (!in_fallback_call && options->type_registry.has_fallback_encoder) {
|
||||
PyObject* fallback_args[1] = {value};
|
||||
new_value = PyObject_Vectorcall(
|
||||
options->type_registry.fallback_encoder, fallback_args, 1, NULL);
|
||||
new_value = PyObject_CallFunctionObjArgs(
|
||||
options->type_registry.fallback_encoder, value, NULL);
|
||||
if (new_value == NULL) {
|
||||
// propagate any exception raised by the callback
|
||||
return 0;
|
||||
@ -1766,46 +1648,6 @@ fail:
|
||||
/* Update Invalid Document error to include doc as a property.
|
||||
*/
|
||||
void handle_invalid_doc_error(PyObject* dict) {
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
PyObject *exc = PyErr_GetRaisedException();
|
||||
PyObject *msg = NULL, *new_msg = NULL;
|
||||
PyObject *InvalidDocument = NULL;
|
||||
|
||||
if (exc == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidDocument = _error("InvalidDocument");
|
||||
if (InvalidDocument == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (PyErr_GivenExceptionMatches(exc, InvalidDocument)) {
|
||||
msg = PyObject_Str(exc);
|
||||
if (msg) {
|
||||
const char *msg_utf8 = PyUnicode_AsUTF8(msg);
|
||||
if (msg_utf8 == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
new_msg = PyUnicode_FromFormat("Invalid document: %s", msg_utf8);
|
||||
if (new_msg == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
/* Add doc to the error instance as a property. */
|
||||
PyObject* exc_args[2] = {new_msg, dict};
|
||||
PyObject* new_exc = PyObject_Vectorcall(InvalidDocument, exc_args, 2, NULL);
|
||||
if (new_exc) {
|
||||
exc = _transfer_traceback(exc, new_exc);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup:
|
||||
/* Steals reference to exc. */
|
||||
PyErr_SetRaisedException(exc);
|
||||
Py_XDECREF(msg);
|
||||
Py_XDECREF(InvalidDocument);
|
||||
Py_XDECREF(new_msg);
|
||||
#else
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *msg = NULL, *new_msg = NULL, *new_evalue = NULL;
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
@ -1826,8 +1668,7 @@ cleanup:
|
||||
goto cleanup;
|
||||
}
|
||||
// Add doc to the error instance as a property.
|
||||
PyObject* exc_args[2] = {new_msg, dict};
|
||||
new_evalue = PyObject_Vectorcall(InvalidDocument, exc_args, 2, NULL);
|
||||
new_evalue = PyObject_CallFunctionObjArgs(InvalidDocument, new_msg, dict, NULL);
|
||||
Py_DECREF(evalue);
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidDocument;
|
||||
@ -1848,7 +1689,6 @@ cleanup:
|
||||
Py_XDECREF(InvalidDocument);
|
||||
Py_XDECREF(new_evalue);
|
||||
Py_XDECREF(new_msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@ -2104,8 +1944,7 @@ static PyObject *_dbref_hook(PyObject* self, PyObject* value) {
|
||||
PyMapping_DelItem(value, state->_dollar_db_str);
|
||||
}
|
||||
|
||||
PyObject* dbref_args[4] = {ref, id, database, value};
|
||||
ret = PyObject_Vectorcall(state->DBRef, dbref_args, 4, NULL);
|
||||
ret = PyObject_CallFunctionObjArgs(state->DBRef, ref, id, database, value, NULL);
|
||||
Py_DECREF(value);
|
||||
} else {
|
||||
ret = value;
|
||||
@ -2281,7 +2120,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
memcpy(&length, buffer + *position, 4);
|
||||
length = BSON_UINT32_FROM_LE(length);
|
||||
if (max - 5 < length) { // Account for 5-byte header. max >= 5 guaranteed above
|
||||
if (max < length) {
|
||||
goto invalid;
|
||||
}
|
||||
|
||||
@ -2321,13 +2160,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
goto uuiderror;
|
||||
}
|
||||
|
||||
PyObject* subtype_obj = PyLong_FromLong(subtype);
|
||||
if (!subtype_obj) {
|
||||
goto uuiderror;
|
||||
}
|
||||
PyObject* binary_args[2] = {data, subtype_obj};
|
||||
binary_value = PyObject_Vectorcall(state->Binary, binary_args, 2, NULL);
|
||||
Py_DECREF(subtype_obj);
|
||||
binary_value = PyObject_CallFunction(state->Binary, "(Oi)", data, subtype);
|
||||
if (binary_value == NULL) {
|
||||
goto uuiderror;
|
||||
}
|
||||
@ -2342,9 +2175,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
if (!uuid_rep_obj) {
|
||||
goto uuiderror;
|
||||
}
|
||||
PyObject* as_uuid_args[2] = {binary_value, uuid_rep_obj};
|
||||
value = PyObject_VectorcallMethod(
|
||||
state->_as_uuid_str, as_uuid_args, 2, NULL);
|
||||
value = PyObject_CallMethodObjArgs(binary_value, state->_as_uuid_str, uuid_rep_obj, NULL);
|
||||
Py_DECREF(uuid_rep_obj);
|
||||
}
|
||||
|
||||
@ -2363,8 +2194,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
Py_DECREF(data);
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* binary_args[2] = {data, st};
|
||||
value = PyObject_Vectorcall(state->Binary, binary_args, 2, NULL);
|
||||
value = PyObject_CallFunctionObjArgs(state->Binary, data, st, NULL);
|
||||
Py_DECREF(st);
|
||||
Py_DECREF(data);
|
||||
if (!value) {
|
||||
@ -2385,13 +2215,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
if (max < 12) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* oid_bytes = PyBytes_FromStringAndSize(buffer + *position, 12);
|
||||
if (!oid_bytes) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* oid_args[1] = {oid_bytes};
|
||||
value = PyObject_Vectorcall(state->ObjectId, oid_args, 1, NULL);
|
||||
Py_DECREF(oid_bytes);
|
||||
value = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12);
|
||||
*position += 12;
|
||||
break;
|
||||
}
|
||||
@ -2470,14 +2294,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
*position += (unsigned)flags_length + 1;
|
||||
|
||||
PyObject* flags_obj = PyLong_FromLong(flags);
|
||||
if (!flags_obj) {
|
||||
Py_DECREF(pattern);
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* regex_args[2] = {pattern, flags_obj};
|
||||
value = PyObject_Vectorcall(state->Regex, regex_args, 2, NULL);
|
||||
Py_DECREF(flags_obj);
|
||||
value = PyObject_CallFunction(state->Regex, "Oi", pattern, flags);
|
||||
Py_DECREF(pattern);
|
||||
break;
|
||||
}
|
||||
@ -2510,21 +2327,13 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
*position += coll_length;
|
||||
|
||||
PyObject* oid_bytes = PyBytes_FromStringAndSize(buffer + *position, 12);
|
||||
if (!oid_bytes) {
|
||||
Py_DECREF(collection);
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* oid_args[1] = {oid_bytes};
|
||||
id = PyObject_Vectorcall(state->ObjectId, oid_args, 1, NULL);
|
||||
Py_DECREF(oid_bytes);
|
||||
id = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12);
|
||||
if (!id) {
|
||||
Py_DECREF(collection);
|
||||
goto invalid;
|
||||
}
|
||||
*position += 12;
|
||||
PyObject* dbref_args[2] = {collection, id};
|
||||
value = PyObject_Vectorcall(state->DBRef, dbref_args, 2, NULL);
|
||||
value = PyObject_CallFunctionObjArgs(state->DBRef, collection, id, NULL);
|
||||
Py_DECREF(collection);
|
||||
Py_DECREF(id);
|
||||
break;
|
||||
@ -2554,8 +2363,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
goto invalid;
|
||||
}
|
||||
*position += value_length;
|
||||
PyObject* code_args[1] = {code};
|
||||
value = PyObject_Vectorcall(state->Code, code_args, 1, NULL);
|
||||
value = PyObject_CallFunctionObjArgs(state->Code, code, NULL, NULL);
|
||||
Py_DECREF(code);
|
||||
break;
|
||||
}
|
||||
@ -2621,8 +2429,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
*position += scope_size;
|
||||
|
||||
PyObject* code_scope_args[2] = {code, scope};
|
||||
value = PyObject_Vectorcall(state->Code, code_scope_args, 2, NULL);
|
||||
value = PyObject_CallFunctionObjArgs(state->Code, code, scope, NULL);
|
||||
Py_DECREF(code);
|
||||
Py_DECREF(scope);
|
||||
break;
|
||||
@ -2652,19 +2459,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
memcpy(&time, buffer + *position + 4, 4);
|
||||
inc = BSON_UINT32_FROM_LE(inc);
|
||||
time = BSON_UINT32_FROM_LE(time);
|
||||
PyObject* time_obj = PyLong_FromUnsignedLong(time);
|
||||
if (!time_obj) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* inc_obj = PyLong_FromUnsignedLong(inc);
|
||||
if (!inc_obj) {
|
||||
Py_DECREF(time_obj);
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* ts_args[2] = {time_obj, inc_obj};
|
||||
value = PyObject_Vectorcall(state->Timestamp, ts_args, 2, NULL);
|
||||
Py_DECREF(time_obj);
|
||||
Py_DECREF(inc_obj);
|
||||
value = PyObject_CallFunction(state->Timestamp, "II", time, inc);
|
||||
*position += 8;
|
||||
break;
|
||||
}
|
||||
@ -2676,13 +2471,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
memcpy(&ll, buffer + *position, 8);
|
||||
ll = (int64_t)BSON_UINT64_FROM_LE(ll);
|
||||
PyObject* ll_obj = PyLong_FromLongLong(ll);
|
||||
if (!ll_obj) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* int64_args[1] = {ll_obj};
|
||||
value = PyObject_Vectorcall(state->BSONInt64, int64_args, 1, NULL);
|
||||
Py_DECREF(ll_obj);
|
||||
value = PyObject_CallFunction(state->BSONInt64, "L", ll);
|
||||
*position += 8;
|
||||
break;
|
||||
}
|
||||
@ -2695,21 +2484,19 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
if (!_bytes_obj) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* dec128_args[2] = {state->Decimal128, _bytes_obj};
|
||||
value = PyObject_VectorcallMethod(
|
||||
state->_from_bid_str, dec128_args, 2, NULL);
|
||||
value = PyObject_CallMethodObjArgs(state->Decimal128, state->_from_bid_str, _bytes_obj, NULL);
|
||||
Py_DECREF(_bytes_obj);
|
||||
*position += 16;
|
||||
break;
|
||||
}
|
||||
case 255:
|
||||
{
|
||||
value = PyObject_Vectorcall(state->MinKey, NULL, 0, NULL);
|
||||
value = PyObject_CallFunctionObjArgs(state->MinKey, NULL);
|
||||
break;
|
||||
}
|
||||
case 127:
|
||||
{
|
||||
value = PyObject_Vectorcall(state->MaxKey, NULL, 0, NULL);
|
||||
value = PyObject_CallFunctionObjArgs(state->MaxKey, NULL);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -2761,8 +2548,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
converter = PyDict_GetItem(options->type_registry.decoder_map, value_type);
|
||||
if (converter != NULL) {
|
||||
PyObject* converter_args[1] = {value};
|
||||
PyObject* new_value = PyObject_Vectorcall(converter, converter_args, 1, NULL);
|
||||
PyObject* new_value = PyObject_CallFunctionObjArgs(converter, value, NULL);
|
||||
Py_DECREF(value_type);
|
||||
Py_DECREF(value);
|
||||
return new_value;
|
||||
@ -2780,7 +2566,42 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
* Wrap any non-InvalidBSON errors in InvalidBSON.
|
||||
*/
|
||||
if (PyErr_Occurred()) {
|
||||
_rewrap_as_invalid_bson();
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *InvalidBSON = NULL;
|
||||
|
||||
/*
|
||||
* Calling _error clears the error state, so fetch it first.
|
||||
*/
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
|
||||
/* Dont reraise anything but PyExc_Exceptions as InvalidBSON. */
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
|
||||
InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
if (!PyErr_GivenExceptionMatches(etype, InvalidBSON)) {
|
||||
/*
|
||||
* Raise InvalidBSON(str(e)).
|
||||
*/
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidBSON;
|
||||
|
||||
if (evalue) {
|
||||
PyObject *msg = PyObject_Str(evalue);
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
} else {
|
||||
/*
|
||||
* The current exception matches InvalidBSON, so we don't
|
||||
* need this reference after all.
|
||||
*/
|
||||
Py_DECREF(InvalidBSON);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Steals references to args. */
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
} else {
|
||||
PyObject *InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
@ -2818,7 +2639,25 @@ static int _element_to_dict(PyObject* self, const char* string,
|
||||
if (!*name) {
|
||||
/* If NULL is returned then wrap the UnicodeDecodeError
|
||||
in an InvalidBSON error */
|
||||
_rewrap_as_invalid_bson();
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *InvalidBSON = NULL;
|
||||
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
|
||||
InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidBSON;
|
||||
|
||||
if (evalue) {
|
||||
PyObject *msg = PyObject_Str(evalue);
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
}
|
||||
}
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
return -1;
|
||||
}
|
||||
position += (unsigned)name_length + 1;
|
||||
@ -2877,20 +2716,11 @@ static PyObject* _elements_to_dict(PyObject* self, const char* string,
|
||||
unsigned max,
|
||||
const codec_options_t* options) {
|
||||
unsigned position = 0;
|
||||
PyObject* dict;
|
||||
int raw_array = 0;
|
||||
|
||||
/* Use PyDict_New() directly when document_class is dict.
|
||||
* This avoids the overhead of PyObject_CallObject() for the common case. */
|
||||
if (options->is_dict_class) {
|
||||
dict = PyDict_New();
|
||||
} else {
|
||||
dict = PyObject_CallObject(options->document_class, NULL);
|
||||
}
|
||||
PyObject* dict = PyObject_CallObject(options->document_class, NULL);
|
||||
if (!dict) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int raw_array = 0;
|
||||
while (position < max) {
|
||||
PyObject* name = NULL;
|
||||
PyObject* value = NULL;
|
||||
@ -2905,24 +2735,7 @@ static PyObject* _elements_to_dict(PyObject* self, const char* string,
|
||||
position = (unsigned)new_position;
|
||||
}
|
||||
|
||||
/* Use PyDict_SetItem() when document_class is dict.
|
||||
* PyDict_SetItem() is faster than PyObject_SetItem() because it
|
||||
* avoids method lookup overhead. */
|
||||
if (options->is_dict_class) {
|
||||
if (PyDict_SetItem(dict, name, value) < 0) {
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(value);
|
||||
Py_DECREF(dict);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
if (PyObject_SetItem(dict, name, value) < 0) {
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(value);
|
||||
Py_DECREF(dict);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
PyObject_SetItem(dict, name, value);
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(value);
|
||||
}
|
||||
@ -2934,14 +2747,9 @@ static PyObject* elements_to_dict(PyObject* self, const char* string,
|
||||
const codec_options_t* options) {
|
||||
PyObject* result;
|
||||
if (options->is_raw_bson) {
|
||||
PyObject* bson_bytes = PyBytes_FromStringAndSize(string, max);
|
||||
if (!bson_bytes) {
|
||||
return NULL;
|
||||
}
|
||||
PyObject* raw_args[2] = {bson_bytes, options->options_obj};
|
||||
result = PyObject_Vectorcall(options->document_class, raw_args, 2, NULL);
|
||||
Py_DECREF(bson_bytes);
|
||||
return result;
|
||||
return PyObject_CallFunction(
|
||||
options->document_class, "y#O",
|
||||
string, max, options->options_obj);
|
||||
}
|
||||
if (Py_EnterRecursiveCall(" while decoding a BSON document"))
|
||||
return NULL;
|
||||
|
||||
@ -72,7 +72,6 @@ typedef struct codec_options_t {
|
||||
unsigned char datetime_conversion;
|
||||
PyObject* options_obj;
|
||||
unsigned char is_raw_bson;
|
||||
unsigned char is_dict_class;
|
||||
} codec_options_t;
|
||||
|
||||
/* C API functions */
|
||||
|
||||
208
bson/binary.py
208
bson/binary.py
@ -65,9 +65,6 @@ if TYPE_CHECKING:
|
||||
from array import array as _array
|
||||
from mmap import mmap as _mmap
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
class UuidRepresentation:
|
||||
UNSPECIFIED = 0
|
||||
@ -237,20 +234,13 @@ class BinaryVector:
|
||||
|
||||
__slots__ = ("data", "dtype", "padding")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Union[Sequence[float | int], npt.NDArray[np.number]],
|
||||
dtype: BinaryVectorDtype,
|
||||
padding: int = 0,
|
||||
):
|
||||
def __init__(self, data: Sequence[float | int], dtype: BinaryVectorDtype, padding: int = 0):
|
||||
"""
|
||||
:param data: Sequence of numbers representing the mathematical vector.
|
||||
:param dtype: The data type stored in binary
|
||||
:param padding: The number of bits in the final byte that are to be ignored
|
||||
when a vector element's size is less than a byte
|
||||
and the length of the vector is not a multiple of 8.
|
||||
(Padding is equivalent to a negative value of `count` in
|
||||
`numpy.unpackbits <https://numpy.org/doc/stable/reference/generated/numpy.unpackbits.html>`_)
|
||||
"""
|
||||
self.data = data
|
||||
self.dtype = dtype
|
||||
@ -435,19 +425,9 @@ class Binary(bytes):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def from_vector(
|
||||
cls: Type[Binary],
|
||||
vector: npt.NDArray[np.number],
|
||||
dtype: BinaryVectorDtype,
|
||||
padding: int = 0,
|
||||
) -> Binary:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def from_vector(
|
||||
cls: Type[Binary],
|
||||
vector: Union[BinaryVector, list[int], list[float], npt.NDArray[np.number]],
|
||||
vector: Union[BinaryVector, list[int], list[float]],
|
||||
dtype: Optional[BinaryVectorDtype] = None,
|
||||
padding: Optional[int] = None,
|
||||
) -> Binary:
|
||||
@ -479,72 +459,34 @@ class Binary(bytes):
|
||||
vector = vector.data # type: ignore
|
||||
|
||||
padding = 0 if padding is None else padding
|
||||
if not isinstance(dtype, BinaryVectorDtype):
|
||||
raise TypeError(
|
||||
"dtype must be a bson.BinaryVectorDtype of BinaryVectorDType.INT8, PACKED_BIT, FLOAT32"
|
||||
)
|
||||
if dtype == BinaryVectorDtype.INT8: # pack ints in [-128, 127] as signed int8
|
||||
format_str = "b"
|
||||
if padding:
|
||||
raise ValueError(f"padding does not apply to {dtype=}")
|
||||
elif dtype == BinaryVectorDtype.PACKED_BIT: # pack ints in [0, 255] as unsigned uint8
|
||||
format_str = "B"
|
||||
if 0 <= padding > 7:
|
||||
raise ValueError(f"{padding=}. It must be in [0,1, ..7].")
|
||||
if padding and not vector:
|
||||
raise ValueError("Empty vector with non-zero padding.")
|
||||
elif dtype == BinaryVectorDtype.FLOAT32: # pack floats as float32
|
||||
format_str = "f"
|
||||
if padding:
|
||||
raise ValueError(f"padding does not apply to {dtype=}")
|
||||
else:
|
||||
raise NotImplementedError("%s not yet supported" % dtype)
|
||||
|
||||
metadata = struct.pack("<sB", dtype.value, padding)
|
||||
|
||||
if isinstance(vector, list):
|
||||
if dtype == BinaryVectorDtype.INT8: # pack ints in [-128, 127] as signed int8
|
||||
format_str = "b"
|
||||
if padding:
|
||||
raise ValueError(f"padding does not apply to {dtype=}")
|
||||
elif dtype == BinaryVectorDtype.PACKED_BIT: # pack ints in [0, 255] as unsigned uint8
|
||||
format_str = "B"
|
||||
if 0 <= padding > 7:
|
||||
raise ValueError(f"{padding=}. It must be in [0,1, ..7].")
|
||||
if padding and not vector:
|
||||
raise ValueError("Empty vector with non-zero padding.")
|
||||
elif dtype == BinaryVectorDtype.FLOAT32: # pack floats as float32
|
||||
format_str = "f"
|
||||
if padding:
|
||||
raise ValueError(f"padding does not apply to {dtype=}")
|
||||
else:
|
||||
raise NotImplementedError("%s not yet supported" % dtype)
|
||||
data = struct.pack(f"<{len(vector)}{format_str}", *vector)
|
||||
else: # vector is numpy array or incorrect type.
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Failed to create binary from vector. Check type. If numpy array, numpy must be installed."
|
||||
) from exc
|
||||
if not isinstance(vector, np.ndarray):
|
||||
raise TypeError(
|
||||
"Could not create Binary. Vector must be a BinaryVector, list[int], list[float] or numpy ndarray."
|
||||
)
|
||||
if vector.ndim != 1:
|
||||
raise ValueError(
|
||||
"from_numpy_vector only supports 1D arrays as it creates a single vector."
|
||||
)
|
||||
|
||||
if dtype == BinaryVectorDtype.FLOAT32:
|
||||
vector = vector.astype(np.dtype("float32"), copy=False)
|
||||
elif dtype == BinaryVectorDtype.INT8:
|
||||
if vector.min() >= -128 and vector.max() <= 127:
|
||||
vector = vector.astype(np.dtype("int8"), copy=False)
|
||||
else:
|
||||
raise ValueError("Values found outside INT8 range.")
|
||||
elif dtype == BinaryVectorDtype.PACKED_BIT:
|
||||
if vector.min() >= 0 and vector.max() <= 127:
|
||||
vector = vector.astype(np.dtype("uint8"), copy=False)
|
||||
else:
|
||||
raise ValueError("Values found outside UINT8 range.")
|
||||
else:
|
||||
raise NotImplementedError("%s not yet supported" % dtype)
|
||||
data = vector.tobytes()
|
||||
|
||||
data = struct.pack(f"<{len(vector)}{format_str}", *vector) # type: ignore
|
||||
if padding and len(vector) and not (data[-1] & ((1 << padding) - 1)) == 0:
|
||||
raise ValueError(
|
||||
"Vector has a padding P, but bits in the final byte lower than P are non-zero. They must be zero."
|
||||
)
|
||||
return cls(metadata + data, subtype=VECTOR_SUBTYPE)
|
||||
|
||||
def as_vector(self, return_numpy: bool = False) -> BinaryVector:
|
||||
"""From the Binary, create a list or 1-d numpy array of numbers, along with dtype and padding.
|
||||
def as_vector(self) -> BinaryVector:
|
||||
"""From the Binary, create a list of numbers, along with dtype and padding.
|
||||
|
||||
:param return_numpy: If True, BinaryVector.data will be a one-dimensional numpy array. By default, it is a list.
|
||||
:return: BinaryVector
|
||||
|
||||
.. versionadded:: 4.10
|
||||
@ -553,84 +495,54 @@ class Binary(bytes):
|
||||
if self.subtype != VECTOR_SUBTYPE:
|
||||
raise ValueError(f"Cannot decode subtype {self.subtype} as a vector")
|
||||
|
||||
dtype, padding = struct.unpack_from("<sB", self)
|
||||
position = 0
|
||||
dtype, padding = struct.unpack_from("<sB", self, position)
|
||||
position += 2
|
||||
dtype = BinaryVectorDtype(dtype)
|
||||
offset = 2
|
||||
n_bytes = len(self) - offset
|
||||
n_values = len(self) - position
|
||||
|
||||
if padding and dtype != BinaryVectorDtype.PACKED_BIT:
|
||||
raise ValueError(
|
||||
f"Corrupt data. Padding ({padding}) must be 0 for all but PACKED_BIT dtypes. ({dtype=})"
|
||||
)
|
||||
|
||||
if not return_numpy:
|
||||
if dtype == BinaryVectorDtype.INT8:
|
||||
dtype_format = "b"
|
||||
format_string = f"<{n_bytes}{dtype_format}"
|
||||
vector = list(struct.unpack_from(format_string, self, offset))
|
||||
return BinaryVector(vector, dtype, padding)
|
||||
if dtype == BinaryVectorDtype.INT8:
|
||||
dtype_format = "b"
|
||||
format_string = f"<{n_values}{dtype_format}"
|
||||
vector = list(struct.unpack_from(format_string, self, position))
|
||||
return BinaryVector(vector, dtype, padding)
|
||||
|
||||
elif dtype == BinaryVectorDtype.FLOAT32:
|
||||
n_values = n_bytes // 4
|
||||
if n_bytes % 4:
|
||||
raise ValueError(
|
||||
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
|
||||
)
|
||||
dtype_format = "f"
|
||||
format_string = f"<{n_values}{dtype_format}"
|
||||
vector = list(struct.unpack_from(format_string, self, offset))
|
||||
return BinaryVector(vector, dtype, padding)
|
||||
elif dtype == BinaryVectorDtype.FLOAT32:
|
||||
n_bytes = len(self) - position
|
||||
n_values = n_bytes // 4
|
||||
if n_bytes % 4:
|
||||
raise ValueError(
|
||||
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
|
||||
)
|
||||
dtype_format = "f"
|
||||
format_string = f"<{n_values}{dtype_format}"
|
||||
vector = list(struct.unpack_from(format_string, self, position))
|
||||
return BinaryVector(vector, dtype, padding)
|
||||
|
||||
elif dtype == BinaryVectorDtype.PACKED_BIT:
|
||||
# data packed as uint8
|
||||
if padding and not n_bytes:
|
||||
raise ValueError("Corrupt data. Vector has a padding P, but no data.")
|
||||
if padding > 7 or padding < 0:
|
||||
raise ValueError(f"Corrupt data. Padding ({padding}) must be between 0 and 7.")
|
||||
dtype_format = "B"
|
||||
format_string = f"<{n_bytes}{dtype_format}"
|
||||
unpacked_uint8s = list(struct.unpack_from(format_string, self, offset))
|
||||
if padding and n_bytes and unpacked_uint8s[-1] & (1 << padding) - 1 != 0:
|
||||
warnings.warn(
|
||||
"Vector has a padding P, but bits in the final byte lower than P are non-zero. For pymongo>=5.0, they must be zero.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return BinaryVector(unpacked_uint8s, dtype, padding)
|
||||
elif dtype == BinaryVectorDtype.PACKED_BIT:
|
||||
# data packed as uint8
|
||||
if padding and not n_values:
|
||||
raise ValueError("Corrupt data. Vector has a padding P, but no data.")
|
||||
if padding > 7 or padding < 0:
|
||||
raise ValueError(f"Corrupt data. Padding ({padding}) must be between 0 and 7.")
|
||||
dtype_format = "B"
|
||||
format_string = f"<{n_values}{dtype_format}"
|
||||
unpacked_uint8s = list(struct.unpack_from(format_string, self, position))
|
||||
if padding and n_values and unpacked_uint8s[-1] & (1 << padding) - 1 != 0:
|
||||
warnings.warn(
|
||||
"Vector has a padding P, but bits in the final byte lower than P are non-zero. For pymongo>=5.0, they must be zero.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return BinaryVector(unpacked_uint8s, dtype, padding)
|
||||
|
||||
else:
|
||||
raise NotImplementedError("Binary Vector dtype %s not yet supported" % dtype.name)
|
||||
else: # create a numpy array
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Converting binary to numpy.ndarray requires numpy to be installed."
|
||||
) from exc
|
||||
if dtype == BinaryVectorDtype.INT8:
|
||||
data = np.frombuffer(self[offset:], dtype="int8")
|
||||
elif dtype == BinaryVectorDtype.FLOAT32:
|
||||
if n_bytes % 4:
|
||||
raise ValueError(
|
||||
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
|
||||
)
|
||||
data = np.frombuffer(self[offset:], dtype="float32")
|
||||
elif dtype == BinaryVectorDtype.PACKED_BIT:
|
||||
# data packed as uint8
|
||||
if padding and not n_bytes:
|
||||
raise ValueError("Corrupt data. Vector has a padding P, but no data.")
|
||||
if padding > 7 or padding < 0:
|
||||
raise ValueError(f"Corrupt data. Padding ({padding}) must be between 0 and 7.")
|
||||
data = np.frombuffer(self[offset:], dtype="uint8")
|
||||
if padding and np.unpackbits(data[-1])[-padding:].sum() > 0:
|
||||
warnings.warn(
|
||||
"Vector has a padding P, but bits in the final byte lower than P are non-zero. For pymongo>=5.0, they must be zero.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("Binary Vector dtype %s not yet supported" % dtype.name)
|
||||
return BinaryVector(data, dtype, padding)
|
||||
else:
|
||||
raise NotImplementedError("Binary Vector dtype %s not yet supported" % dtype.name)
|
||||
|
||||
@property
|
||||
def subtype(self) -> int:
|
||||
|
||||
@ -273,6 +273,9 @@ if TYPE_CHECKING:
|
||||
def _arguments_repr(self) -> str:
|
||||
...
|
||||
|
||||
def _options_dict(self) -> dict[Any, Any]:
|
||||
...
|
||||
|
||||
# NamedTuple API
|
||||
@classmethod
|
||||
def _make(cls, obj: Iterable[Any]) -> CodecOptions[_DocumentType]:
|
||||
@ -463,6 +466,19 @@ else:
|
||||
)
|
||||
)
|
||||
|
||||
def _options_dict(self) -> dict[str, Any]:
|
||||
"""Dictionary of the arguments used to create this object."""
|
||||
# TODO: PYTHON-2442 use _asdict() instead
|
||||
return {
|
||||
"document_class": self.document_class,
|
||||
"tz_aware": self.tz_aware,
|
||||
"uuid_representation": self.uuid_representation,
|
||||
"unicode_decode_error_handler": self.unicode_decode_error_handler,
|
||||
"tzinfo": self.tzinfo,
|
||||
"type_registry": self.type_registry,
|
||||
"datetime_conversion": self.datetime_conversion,
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self._arguments_repr()})"
|
||||
|
||||
@ -478,7 +494,7 @@ else:
|
||||
|
||||
.. versionadded:: 3.5
|
||||
"""
|
||||
opts = self._asdict()
|
||||
opts = self._options_dict()
|
||||
opts.update(kwargs)
|
||||
return CodecOptions(**opts)
|
||||
|
||||
|
||||
@ -382,6 +382,19 @@ class JSONOptions(_BASE_CLASS):
|
||||
)
|
||||
)
|
||||
|
||||
def _options_dict(self) -> dict[Any, Any]:
|
||||
# TODO: PYTHON-2442 use _asdict() instead
|
||||
options_dict = super()._options_dict()
|
||||
options_dict.update(
|
||||
{
|
||||
"strict_number_long": self.strict_number_long,
|
||||
"datetime_representation": self.datetime_representation,
|
||||
"strict_uuid": self.strict_uuid,
|
||||
"json_mode": self.json_mode,
|
||||
}
|
||||
)
|
||||
return options_dict
|
||||
|
||||
def with_options(self, **kwargs: Any) -> JSONOptions:
|
||||
"""
|
||||
Make a copy of this JSONOptions, overriding some options::
|
||||
@ -395,7 +408,7 @@ class JSONOptions(_BASE_CLASS):
|
||||
|
||||
.. versionadded:: 3.12
|
||||
"""
|
||||
opts = self._asdict()
|
||||
opts = self._options_dict()
|
||||
for opt in ("strict_number_long", "datetime_representation", "strict_uuid", "json_mode"):
|
||||
opts[opt] = kwargs.get(opt, getattr(self, opt))
|
||||
opts.update(kwargs)
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"""Tools for working with MongoDB ObjectIds."""
|
||||
from __future__ import annotations
|
||||
|
||||
import binascii
|
||||
import datetime
|
||||
import os
|
||||
import struct
|
||||
@ -97,27 +98,11 @@ class ObjectId:
|
||||
objectid.rst>`_.
|
||||
"""
|
||||
if oid is None:
|
||||
# Generate a new value for this ObjectId.
|
||||
with ObjectId._inc_lock:
|
||||
inc = ObjectId._inc
|
||||
ObjectId._inc = (inc + 1) % (_MAX_COUNTER_VALUE + 1)
|
||||
|
||||
# 4 bytes current time, 5 bytes random, 3 bytes inc.
|
||||
self.__id = _PACK_INT_RANDOM(int(time.time()), ObjectId._random()) + _PACK_INT(inc)[1:4]
|
||||
self.__generate()
|
||||
elif isinstance(oid, bytes) and len(oid) == 12:
|
||||
self.__id = oid
|
||||
elif isinstance(oid, str):
|
||||
if len(oid) == 24:
|
||||
try:
|
||||
self.__id = bytes.fromhex(oid)
|
||||
except (TypeError, ValueError):
|
||||
_raise_invalid_id(oid)
|
||||
else:
|
||||
_raise_invalid_id(oid)
|
||||
elif isinstance(oid, ObjectId):
|
||||
self.__id = oid.binary
|
||||
else:
|
||||
raise TypeError(f"id must be an instance of (bytes, str, ObjectId), not {type(oid)}")
|
||||
self.__validate(oid)
|
||||
|
||||
@classmethod
|
||||
def from_datetime(cls: Type[ObjectId], generation_time: datetime.datetime) -> ObjectId:
|
||||
@ -178,6 +163,37 @@ class ObjectId:
|
||||
cls.__random = _random_bytes()
|
||||
return cls.__random
|
||||
|
||||
def __generate(self) -> None:
|
||||
"""Generate a new value for this ObjectId."""
|
||||
with ObjectId._inc_lock:
|
||||
inc = ObjectId._inc
|
||||
ObjectId._inc = (inc + 1) % (_MAX_COUNTER_VALUE + 1)
|
||||
|
||||
# 4 bytes current time, 5 bytes random, 3 bytes inc.
|
||||
self.__id = _PACK_INT_RANDOM(int(time.time()), ObjectId._random()) + _PACK_INT(inc)[1:4]
|
||||
|
||||
def __validate(self, oid: Any) -> None:
|
||||
"""Validate and use the given id for this ObjectId.
|
||||
|
||||
Raises TypeError if id is not an instance of :class:`str`,
|
||||
:class:`bytes`, or ObjectId. Raises InvalidId if it is not a
|
||||
valid ObjectId.
|
||||
|
||||
:param oid: a valid ObjectId
|
||||
"""
|
||||
if isinstance(oid, ObjectId):
|
||||
self.__id = oid.binary
|
||||
elif isinstance(oid, str):
|
||||
if len(oid) == 24:
|
||||
try:
|
||||
self.__id = bytes.fromhex(oid)
|
||||
except (TypeError, ValueError):
|
||||
_raise_invalid_id(oid)
|
||||
else:
|
||||
_raise_invalid_id(oid)
|
||||
else:
|
||||
raise TypeError(f"id must be an instance of (bytes, str, ObjectId), not {type(oid)}")
|
||||
|
||||
@property
|
||||
def binary(self) -> bytes:
|
||||
"""12-byte binary representation of this ObjectId."""
|
||||
@ -218,7 +234,7 @@ class ObjectId:
|
||||
self.__id = oid
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__id.hex()
|
||||
return binascii.hexlify(self.__id).decode()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ObjectId('{self!s}')"
|
||||
|
||||
16
bson/son.py
16
bson/son.py
@ -22,7 +22,6 @@ from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import Mapping as _Mapping
|
||||
from typing import (
|
||||
Any,
|
||||
@ -100,28 +99,13 @@ class SON(Dict[_Key, _Value]):
|
||||
yield from self.__keys
|
||||
|
||||
def has_key(self, key: _Key) -> bool:
|
||||
warnings.warn(
|
||||
"SON.has_key() is deprecated, use the in operator instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return key in self.__keys
|
||||
|
||||
def iterkeys(self) -> Iterator[_Key]:
|
||||
warnings.warn(
|
||||
"SON.iterkeys() is deprecated, use the keys() method instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.__iter__()
|
||||
|
||||
# fourth level uses definitions from lower levels
|
||||
def itervalues(self) -> Iterator[_Value]:
|
||||
warnings.warn(
|
||||
"SON.itervalues() is deprecated, use the values() method instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
for _, v in self.items():
|
||||
yield v
|
||||
|
||||
|
||||
@ -5,4 +5,3 @@
|
||||
.. automodule:: pymongo.asynchronous.command_cursor
|
||||
:synopsis: Tools for iterating over MongoDB command results
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
@ -7,8 +7,6 @@
|
||||
|
||||
.. autoclass:: pymongo.asynchronous.cursor.AsyncCursor(collection, filter=None, projection=None, skip=0, limit=0, no_cursor_timeout=False, cursor_type=CursorType.NON_TAILABLE, sort=None, allow_partial_results=False, oplog_replay=False, batch_size=0, collation=None, hint=None, max_scan=None, max_time_ms=None, max=None, min=None, return_key=False, show_record_id=False, snapshot=False, comment=None, session=None, allow_disk_use=None)
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
.. describe:: c[index]
|
||||
|
||||
|
||||
@ -4,4 +4,3 @@
|
||||
.. automodule:: pymongo.command_cursor
|
||||
:synopsis: Tools for iterating over MongoDB command results
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
.. autoclass:: pymongo.cursor.Cursor(collection, filter=None, projection=None, skip=0, limit=0, no_cursor_timeout=False, cursor_type=CursorType.NON_TAILABLE, sort=None, allow_partial_results=False, oplog_replay=False, batch_size=0, collation=None, hint=None, max_scan=None, max_time_ms=None, max=None, min=None, return_key=False, show_record_id=False, snapshot=False, comment=None, session=None, allow_disk_use=None)
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. describe:: c[index]
|
||||
|
||||
|
||||
@ -1,64 +1,21 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Changes in Version 4.17.0 (2026/04/20)
|
||||
--------------------------------------
|
||||
|
||||
PyMongo 4.17 brings a number of changes including:
|
||||
|
||||
- ``has_key``, ``iterkeys`` and ``itervalues`` in :class:`bson.son.SON` have
|
||||
been deprecated and will be removed in PyMongo 5.0. These methods were
|
||||
deprecated in favor of the standard dictionary containment operator ``in``
|
||||
and the ``keys()`` and ``values()`` methods, respectively.
|
||||
- Added the :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.bind` and :meth:`~pymongo.client_session.ClientSession.bind` methods
|
||||
that allow users to bind a session to all database operations within the scope of a context manager instead of having to explicitly pass the session to each individual operation.
|
||||
See the `Transactions docs <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/transactions/#methods>`_ for examples and more information.
|
||||
- Added support for MongoDB's Intelligent Workload Management (IWM) and ingress connection rate limiting features.
|
||||
The driver now gracefully handles write-blocking scenarios and optimizes connection establishment during high-load conditions to maintain application availability.
|
||||
See the `IWM <https://www.mongodb.com/docs/atlas/intelligent-workload-management>`_ or `Overload Errors <https://www.mongodb.com/docs/atlas/overload-errors/?interface=driver&language=python>`_ docs for more information.
|
||||
|
||||
Changes in Version 4.16.0 (2026/01/07)
|
||||
Changes in Version 4.16.0 (XXXX/XX/XX)
|
||||
--------------------------------------
|
||||
|
||||
PyMongo 4.16 brings a number of changes including:
|
||||
|
||||
.. warning:: PyMongo 4.16 drops support for Python 3.9 and PyPy 3.10: Python 3.10+ or PyPy 3.11+ is now required.
|
||||
|
||||
- Dropped support for Python 3.9 and PyPy 3.10.
|
||||
- Removed invalid documents from :class:`bson.errors.InvalidDocument` error messages as
|
||||
doing so may leak sensitive user data.
|
||||
Instead, invalid documents are stored in :attr:`bson.errors.InvalidDocument.document`.
|
||||
- PyMongo now requires ``dnspython>=2.6.1``, since ``dnspython`` 1.0 is no longer maintained.
|
||||
The minimum version is ``2.6.1`` to account for `CVE-2023-29483 <https://www.cve.org/CVERecord?id=CVE-2023-29483>`_.
|
||||
- PyMongo now requires ``dnspython>=2.6.1``, since ``dnspython`` 1.0 is no longer maintained and is incompatible with
|
||||
Python 3.10+. The minimum version is ``2.6.1`` to account for `CVE-2023-29483 <https://www.cve.org/CVERecord?id=CVE-2023-29483>`_.
|
||||
- Removed support for Eventlet.
|
||||
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
|
||||
- Use Zstandard support from the standard library for Python 3.14+, and use ``backports.zstd`` for older versions.
|
||||
- Fixed return type annotation for ``find_one_and_*`` methods on :class:`~pymongo.asynchronous.collection.AsyncCollection`
|
||||
and :class:`~pymongo.synchronous.collection.Collection` to include ``None``.
|
||||
- Added support for NumPy 1D-arrays in :class:`bson.binary.BinaryVector`.
|
||||
- Prevented :class:`~pymongo.encryption.ClientEncryption` from loading the crypt
|
||||
shared library to fix "MongoCryptError: An existing crypt_shared library is
|
||||
loaded by the application" unless the linked library search path is set.
|
||||
|
||||
Changes in Version 4.15.5 (2025/12/02)
|
||||
--------------------------------------
|
||||
|
||||
Version 4.15.5 is a bug fix release.
|
||||
|
||||
- Fixed a bug that could cause ``AutoReconnect("connection pool paused")`` errors when cursors fetched more documents from the database after SDAM heartbeat failures.
|
||||
|
||||
Changes in Version 4.15.4 (2025/10/21)
|
||||
--------------------------------------
|
||||
|
||||
Version 4.15.4 is a bug fix release.
|
||||
|
||||
- Relaxed the callback type of :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.with_transaction` to allow the broader Awaitable type rather than only Coroutine objects.
|
||||
- Added the missing Python 3.14 trove classifier to the package metadata.
|
||||
|
||||
Issues Resolved
|
||||
...............
|
||||
|
||||
See the `PyMongo 4.15.4 release notes in JIRA`_ for the list of resolved issues
|
||||
in this release.
|
||||
|
||||
.. _PyMongo 4.15.4 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=47237
|
||||
|
||||
Changes in Version 4.15.3 (2025/10/07)
|
||||
--------------------------------------
|
||||
|
||||
@ -88,8 +88,6 @@ pygments_style = "sphinx"
|
||||
linkcheck_ignore = [
|
||||
"https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-monitoring.md#requesting-an-immediate-check",
|
||||
"https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback",
|
||||
"https://github.com/mongodb/specifications/blob/master/source/uri-options/uri-options.md",
|
||||
"https://github.com/mongodb/specifications/blob/master/source/uri-options/uri-options.md",
|
||||
"https://github.com/mongodb/libmongocrypt/blob/master/bindings/python/README.rst#installing-from-source",
|
||||
r"https://wiki.centos.org/[\w/]*",
|
||||
r"https://sourceforge.net/",
|
||||
@ -188,8 +186,8 @@ latex_documents = [
|
||||
("index", "PyMongo.tex", "PyMongo Documentation", "Michael Dirolf", "manual"),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the title page.
|
||||
# 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,
|
||||
|
||||
@ -107,4 +107,3 @@ The following is a list of people who have contributed to
|
||||
- Jeffrey A. Clark (aclark4life)
|
||||
- Steven Silvester (blink1073)
|
||||
- Noah Stapp (NoahStapp)
|
||||
- Cal Jacobson (cj81499)
|
||||
|
||||
@ -46,7 +46,6 @@ from pymongo.asynchronous.client_session import AsyncClientSession
|
||||
from pymongo.asynchronous.collection import AsyncCollection
|
||||
from pymongo.asynchronous.cursor import AsyncCursor
|
||||
from pymongo.asynchronous.database import AsyncDatabase
|
||||
from pymongo.asynchronous.helpers import anext
|
||||
from pymongo.common import validate_string
|
||||
from pymongo.errors import (
|
||||
BulkWriteError,
|
||||
|
||||
@ -57,7 +57,6 @@ from pymongo.synchronous.client_session import ClientSession
|
||||
from pymongo.synchronous.collection import Collection
|
||||
from pymongo.synchronous.cursor import Cursor
|
||||
from pymongo.synchronous.database import Database
|
||||
from pymongo.synchronous.helpers import next
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
|
||||
59
justfile
59
justfile
@ -2,7 +2,7 @@
|
||||
set shell := ["bash", "-c"]
|
||||
|
||||
# Commonly used command segments.
|
||||
typing_run := "uv run --group typing --extra aws --extra encryption --with numpy --extra ocsp --extra snappy --extra test --extra zstd"
|
||||
typing_run := "uv run --group typing --extra aws --extra encryption --extra ocsp --extra snappy --extra test --extra zstd"
|
||||
docs_run := "uv run --extra docs"
|
||||
doc_build := "./doc/_build"
|
||||
mypy_args := "--install-types --non-interactive"
|
||||
@ -16,78 +16,57 @@ default:
|
||||
resync:
|
||||
@uv sync --quiet
|
||||
|
||||
# Set up the development environment
|
||||
install:
|
||||
bash .evergreen/scripts/setup-dev-env.sh
|
||||
|
||||
# Build the HTML documentation
|
||||
[group('docs')]
|
||||
docs: && resync
|
||||
{{docs_run}} sphinx-build -W -b html doc {{doc_build}}/html
|
||||
|
||||
# Serve the docs locally with live-reload
|
||||
[group('docs')]
|
||||
docs-serve: && resync
|
||||
{{docs_run}} sphinx-autobuild -W -b html doc --watch ./pymongo --watch ./bson --watch ./gridfs {{doc_build}}/serve
|
||||
|
||||
# Check documentation hyperlinks for broken URLs
|
||||
[group('docs')]
|
||||
docs-linkcheck: && resync
|
||||
{{docs_run}} sphinx-build -E -b linkcheck doc {{doc_build}}/linkcheck
|
||||
|
||||
# Run mypy and pyright
|
||||
[group('typing')]
|
||||
typing: && resync
|
||||
just typing-mypy
|
||||
just typing-pyright
|
||||
|
||||
# Run mypy against the library source and test suite
|
||||
[group('typing')]
|
||||
typing-mypy: && resync
|
||||
{{typing_run}} python -m mypy {{mypy_args}} bson gridfs tools pymongo
|
||||
{{typing_run}} python -m mypy {{mypy_args}} --config-file mypy_test.ini test
|
||||
{{typing_run}} python -m mypy {{mypy_args}} test/test_typing.py test/test_typing_strict.py
|
||||
{{typing_run}} mypy {{mypy_args}} bson gridfs tools pymongo
|
||||
{{typing_run}} mypy {{mypy_args}} --config-file mypy_test.ini test
|
||||
{{typing_run}} mypy {{mypy_args}} test/test_typing.py test/test_typing_strict.py
|
||||
|
||||
# Run pyright against the typing test files
|
||||
[group('typing')]
|
||||
typing-pyright: && resync
|
||||
{{typing_run}} python -m pyright test/test_typing.py test/test_typing_strict.py
|
||||
{{typing_run}} python -m pyright -p strict_pyrightconfig.json test/test_typing_strict.py
|
||||
{{typing_run}} pyright test/test_typing.py test/test_typing_strict.py
|
||||
{{typing_run}} pyright -p strict_pyrightconfig.json test/test_typing_strict.py
|
||||
|
||||
# Run all pre-commit hooks across the repository
|
||||
[group('lint')]
|
||||
lint *args="": && resync
|
||||
uvx pre-commit run --all-files {{args}}
|
||||
|
||||
# Run shellcheck, doc8, and slotscheck
|
||||
[group('lint')]
|
||||
lint-manual *args="": && resync
|
||||
uvx pre-commit run --all-files --hook-stage manual {{args}}
|
||||
|
||||
# Run pytest (e.g. just test test/test_uri_parser.py)
|
||||
[group('test')]
|
||||
test *args="-v --durations=5 --maxfail=10": && resync
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
uv run ${USE_ACTIVE_VENV:+--active} --extra test python -m pytest {{args}}
|
||||
uv run --extra test pytest {{args}}
|
||||
|
||||
# Run the BSON test suite with numpy
|
||||
[group('test')]
|
||||
test-numpy *args="": && resync
|
||||
just setup-tests numpy {{args}}
|
||||
just run-tests test/test_bson.py
|
||||
|
||||
# Run tests via the Evergreen test runner script
|
||||
[group('test')]
|
||||
run-tests *args: && resync
|
||||
bash ./.evergreen/run-tests.sh {{args}}
|
||||
|
||||
# Set up the test environment (auth, TLS, etc.)
|
||||
[group('test')]
|
||||
setup-tests *args="":
|
||||
bash .evergreen/scripts/setup-tests.sh {{args}}
|
||||
|
||||
# Tear down resources created by setup-tests
|
||||
[group('test')]
|
||||
teardown-tests:
|
||||
bash .evergreen/scripts/teardown-tests.sh
|
||||
@ -96,30 +75,6 @@ teardown-tests:
|
||||
integration-tests:
|
||||
bash integration_tests/run.sh
|
||||
|
||||
# Run the full test suite with coverage
|
||||
[group('test')]
|
||||
test-coverage *args="":
|
||||
just setup-tests --cov
|
||||
just run-tests {{args}}
|
||||
|
||||
# Print the coverage summary to the terminal
|
||||
[group('coverage')]
|
||||
coverage-report:
|
||||
uv tool run --with "coverage[toml]" coverage report
|
||||
|
||||
# Generate an HTML coverage report in htmlcov/
|
||||
[group('coverage')]
|
||||
coverage-html:
|
||||
uv tool run --with "coverage[toml]" coverage html
|
||||
@echo "Coverage report generated in htmlcov/index.html"
|
||||
|
||||
# Generate an XML coverage report at coverage.xml
|
||||
[group('coverage')]
|
||||
coverage-xml:
|
||||
uv tool run --with "coverage[toml]" coverage xml
|
||||
@echo "Coverage report generated in coverage.xml"
|
||||
|
||||
# Start a MongoDB server via drivers-evergreen-tools
|
||||
[group('server')]
|
||||
run-server *args="":
|
||||
bash .evergreen/scripts/run-server.sh {{args}}
|
||||
|
||||
@ -17,7 +17,6 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def _get_azure_response(
|
||||
@ -30,7 +29,7 @@ def _get_azure_response(
|
||||
url += "?api-version=2018-02-01"
|
||||
url += f"&resource={resource}"
|
||||
if client_id:
|
||||
url += f"&client_id={quote(client_id)}"
|
||||
url += f"&client_id={client_id}"
|
||||
headers = {"Metadata": "true", "Accept": "application/json"}
|
||||
request = Request(url, headers=headers) # noqa: S310
|
||||
try:
|
||||
|
||||
@ -18,7 +18,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
__version__ = "4.18.0.dev0"
|
||||
__version__ = "4.16.0.dev0"
|
||||
|
||||
|
||||
def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]:
|
||||
|
||||
@ -59,7 +59,6 @@ from pymongo.errors import (
|
||||
InvalidOperation,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
)
|
||||
from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES
|
||||
@ -564,17 +563,9 @@ class _AsyncClientBulk:
|
||||
error, ConnectionFailure
|
||||
) and not isinstance(error, (NotPrimaryError, WaitQueueTimeoutError))
|
||||
|
||||
retryable_label_error = isinstance(
|
||||
error, PyMongoError
|
||||
) and error.has_error_label("RetryableError")
|
||||
|
||||
# Synthesize the full bulk result without modifying the
|
||||
# current one because this write operation may be retried.
|
||||
if retryable and (
|
||||
retryable_top_level_error
|
||||
or retryable_network_error
|
||||
or retryable_label_error
|
||||
):
|
||||
if retryable and (retryable_top_level_error or retryable_network_error):
|
||||
full = copy.deepcopy(full_result)
|
||||
_merge_command(self.ops, self.idx_offset, full, result)
|
||||
_throw_client_bulk_write_exception(full, self.verbose_results)
|
||||
|
||||
@ -135,19 +135,16 @@ Classes
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Mapping as _Mapping
|
||||
from contextvars import ContextVar, Token
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncContextManager,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
NoReturn,
|
||||
@ -160,13 +157,11 @@ from bson.binary import Binary
|
||||
from bson.int64 import Int64
|
||||
from bson.timestamp import Timestamp
|
||||
from pymongo import _csot
|
||||
from pymongo.asynchronous.cursor_base import _ConnectionManager
|
||||
from pymongo.asynchronous.cursor import _ConnectionManager
|
||||
from pymongo.errors import (
|
||||
ConfigurationError,
|
||||
ConnectionFailure,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WTimeoutError,
|
||||
@ -186,28 +181,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
_SESSION: ContextVar[Optional[AsyncClientSession]] = ContextVar("SESSION", default=None)
|
||||
|
||||
|
||||
class _AsyncBoundSessionContext:
|
||||
"""Context manager returned by AsyncClientSession.bind() that manages bound state."""
|
||||
|
||||
def __init__(self, session: AsyncClientSession, end_session: bool) -> None:
|
||||
self._session = session
|
||||
self._session_token: Optional[Token[AsyncClientSession]] = None
|
||||
self._end_session = end_session
|
||||
|
||||
async def __aenter__(self) -> AsyncClientSession:
|
||||
self._session_token = _SESSION.set(self._session) # type: ignore[assignment]
|
||||
return self._session
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
if self._session_token:
|
||||
_SESSION.reset(self._session_token) # type: ignore[arg-type]
|
||||
self._session_token = None
|
||||
if self._end_session:
|
||||
await self._session.end_session()
|
||||
|
||||
|
||||
class SessionOptions:
|
||||
"""Options for a new :class:`AsyncClientSession`.
|
||||
@ -431,7 +404,6 @@ class _Transaction:
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.client = client
|
||||
self.has_completed_command = False
|
||||
|
||||
def active(self) -> bool:
|
||||
return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS)
|
||||
@ -439,9 +411,6 @@ class _Transaction:
|
||||
def starting(self) -> bool:
|
||||
return self.state == _TxnState.STARTING
|
||||
|
||||
def set_starting(self) -> None:
|
||||
self.state = _TxnState.STARTING
|
||||
|
||||
@property
|
||||
def pinned_conn(self) -> Optional[AsyncConnection]:
|
||||
if self.active() and self.conn_mgr:
|
||||
@ -467,7 +436,6 @@ class _Transaction:
|
||||
self.sharded = False
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.has_completed_command = False
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.conn_mgr:
|
||||
@ -502,29 +470,11 @@ _UNKNOWN_COMMIT_ERROR_CODES: frozenset = _RETRYABLE_ERROR_CODES | frozenset( #
|
||||
# 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
|
||||
_BACKOFF_MAX = 0.500 # 500ms max backoff
|
||||
_BACKOFF_INITIAL = 0.005 # 5ms initial backoff
|
||||
|
||||
|
||||
def _within_time_limit(start_time: float, backoff: float = 0) -> bool:
|
||||
def _within_time_limit(start_time: float) -> bool:
|
||||
"""Are we within the with_transaction retry limit?"""
|
||||
remaining = _csot.remaining()
|
||||
if remaining is not None and remaining <= 0:
|
||||
return False
|
||||
return time.monotonic() + backoff - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
|
||||
|
||||
def _make_timeout_error(error: BaseException) -> PyMongoError:
|
||||
"""Convert error to a NetworkTimeout or ExecutionTimeout as appropriate."""
|
||||
if _csot.remaining() is not None:
|
||||
timeout_error: PyMongoError = ExecutionTimeout(
|
||||
str(error), 50, {"ok": 0, "errmsg": str(error), "code": 50}
|
||||
)
|
||||
else:
|
||||
timeout_error = NetworkTimeout(str(error))
|
||||
if isinstance(error, PyMongoError):
|
||||
timeout_error._error_labels = error._error_labels.copy()
|
||||
return timeout_error
|
||||
return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
@ -597,24 +547,6 @@ class AsyncClientSession:
|
||||
if self._server_session is None:
|
||||
raise InvalidOperation("Cannot use ended session")
|
||||
|
||||
def bind(self, end_session: bool = True) -> _AsyncBoundSessionContext:
|
||||
"""Bind this session so it is implicitly passed to all database operations within the returned context.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with client.start_session() as s:
|
||||
async with s.bind():
|
||||
# session=s is passed implicitly
|
||||
await client.db.collection.insert_one({"x": 1})
|
||||
|
||||
:param end_session: Whether to end the session on exiting the returned context. Defaults to True.
|
||||
If set to False, :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.end_session()` must be called
|
||||
once the session is no longer used.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return _AsyncBoundSessionContext(self, end_session)
|
||||
|
||||
async def __aenter__(self) -> AsyncClientSession:
|
||||
return self
|
||||
|
||||
@ -672,7 +604,7 @@ class AsyncClientSession:
|
||||
|
||||
async def with_transaction(
|
||||
self,
|
||||
callback: Callable[[AsyncClientSession], Awaitable[_T]],
|
||||
callback: Callable[[AsyncClientSession], Coroutine[Any, Any, _T]],
|
||||
read_concern: Optional[ReadConcern] = None,
|
||||
write_concern: Optional[WriteConcern] = None,
|
||||
read_preference: Optional[_ServerMode] = None,
|
||||
@ -771,17 +703,7 @@ class AsyncClientSession:
|
||||
https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback
|
||||
"""
|
||||
start_time = time.monotonic()
|
||||
retry = 0
|
||||
last_error: Optional[BaseException] = None
|
||||
while True:
|
||||
if retry: # Implement exponential backoff on retry.
|
||||
jitter = random.random() # noqa: S311
|
||||
backoff = jitter * min(_BACKOFF_INITIAL * (1.5**retry), _BACKOFF_MAX)
|
||||
if not _within_time_limit(start_time, backoff):
|
||||
assert last_error is not None
|
||||
raise _make_timeout_error(last_error) from last_error
|
||||
await asyncio.sleep(backoff)
|
||||
retry += 1
|
||||
await self.start_transaction(
|
||||
read_concern, write_concern, read_preference, max_commit_time_ms
|
||||
)
|
||||
@ -789,16 +711,15 @@ class AsyncClientSession:
|
||||
ret = await callback(self)
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException as exc:
|
||||
last_error = exc
|
||||
if self.in_transaction:
|
||||
await self.abort_transaction()
|
||||
if isinstance(exc, PyMongoError) and exc.has_error_label(
|
||||
"TransientTransactionError"
|
||||
if (
|
||||
isinstance(exc, PyMongoError)
|
||||
and exc.has_error_label("TransientTransactionError")
|
||||
and _within_time_limit(start_time)
|
||||
):
|
||||
if _within_time_limit(start_time):
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
raise
|
||||
|
||||
if not self.in_transaction:
|
||||
@ -809,18 +730,17 @@ class AsyncClientSession:
|
||||
try:
|
||||
await self.commit_transaction()
|
||||
except PyMongoError as exc:
|
||||
last_error = exc
|
||||
if exc.has_error_label(
|
||||
"UnknownTransactionCommitResult"
|
||||
) and not _max_time_expired_error(exc):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from 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"):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
if exc.has_error_label("TransientTransactionError") and _within_time_limit(
|
||||
start_time
|
||||
):
|
||||
# Retry the entire transaction.
|
||||
break
|
||||
raise
|
||||
@ -1101,11 +1021,7 @@ class AsyncClientSession:
|
||||
read_preference: _ServerMode,
|
||||
conn: AsyncConnection,
|
||||
) -> None:
|
||||
# getMores must be sent with a session if the cursor was opened with one
|
||||
operation = next(iter(command))
|
||||
if not conn.supports_sessions and (
|
||||
isinstance(self._server_session, _EmptyServerSession) or operation != "getMore"
|
||||
):
|
||||
if not conn.supports_sessions:
|
||||
if not self._implicit:
|
||||
raise ConfigurationError("Sessions are not supported by this MongoDB deployment")
|
||||
return
|
||||
|
||||
@ -20,6 +20,7 @@ from collections import abc
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncContextManager,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Generic,
|
||||
@ -570,6 +571,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
await change_stream._initialize_cursor()
|
||||
return change_stream
|
||||
|
||||
async def _conn_for_writes(
|
||||
self, session: Optional[AsyncClientSession], operation: str
|
||||
) -> AsyncContextManager[AsyncConnection]:
|
||||
return await self._database.client._conn_for_writes(session, operation)
|
||||
|
||||
async def _command(
|
||||
self,
|
||||
conn: AsyncConnection,
|
||||
@ -646,10 +652,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
if "size" in options:
|
||||
options["size"] = float(options["size"])
|
||||
cmd.update(options)
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
async with await self._conn_for_writes(session, operation=_Op.CREATE) as conn:
|
||||
if qev2_required and conn.max_wire_version < 21:
|
||||
raise ConfigurationError(
|
||||
"Driver support of Queryable Encryption is incompatible with server. "
|
||||
@ -666,8 +669,6 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.CREATE)
|
||||
|
||||
async def _create(
|
||||
self,
|
||||
options: MutableMapping[str, Any],
|
||||
@ -2239,10 +2240,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
command (like maxTimeMS) can be passed as keyword arguments.
|
||||
"""
|
||||
names = []
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
async with await self._conn_for_writes(session, operation=_Op.CREATE_INDEXES) as conn:
|
||||
supports_quorum = conn.max_wire_version >= 9
|
||||
|
||||
def gen_indexes() -> Iterator[Mapping[str, Any]]:
|
||||
@ -2271,11 +2269,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
write_concern=self._write_concern_for(session),
|
||||
session=session,
|
||||
)
|
||||
return names
|
||||
|
||||
return await self.database.client._retryable_write(
|
||||
False, inner, session, _Op.CREATE_INDEXES
|
||||
)
|
||||
return names
|
||||
|
||||
async def create_index(
|
||||
self,
|
||||
@ -2428,6 +2422,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
kwargs["comment"] = comment
|
||||
await self._drop_index("*", session=session, **kwargs)
|
||||
|
||||
@_csot.apply
|
||||
async def drop_index(
|
||||
self,
|
||||
index_or_name: _IndexKeyHint,
|
||||
@ -2495,10 +2490,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
async with await self._conn_for_writes(session, operation=_Op.DROP_INDEXES) as conn:
|
||||
await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
@ -2508,8 +2500,6 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.DROP_INDEXES)
|
||||
|
||||
async def list_indexes(
|
||||
self,
|
||||
session: Optional[AsyncClientSession] = None,
|
||||
@ -2773,22 +2763,17 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd = {"createSearchIndexes": self.name, "indexes": list(gen_indexes())}
|
||||
cmd.update(kwargs)
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
async with await self._conn_for_writes(
|
||||
session, operation=_Op.CREATE_SEARCH_INDEXES
|
||||
) as conn:
|
||||
resp = await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
return [index["name"] for index in resp["indexesCreated"]]
|
||||
|
||||
return await self.database.client._retryable_write(
|
||||
False, inner, session, _Op.CREATE_SEARCH_INDEXES
|
||||
)
|
||||
|
||||
async def drop_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2814,21 +2799,15 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
async with await self._conn_for_writes(session, operation=_Op.DROP_SEARCH_INDEXES) as conn:
|
||||
await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.DROP_SEARCH_INDEXES)
|
||||
|
||||
async def update_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2856,21 +2835,15 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
async with await self._conn_for_writes(session, operation=_Op.UPDATE_SEARCH_INDEX) as conn:
|
||||
await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.UPDATE_SEARCH_INDEX)
|
||||
|
||||
async def options(
|
||||
self,
|
||||
session: Optional[AsyncClientSession] = None,
|
||||
@ -2945,7 +2918,6 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
session,
|
||||
retryable=not cmd._performs_write,
|
||||
operation=_Op.AGGREGATE,
|
||||
is_aggregate_write=cmd._performs_write,
|
||||
)
|
||||
|
||||
async def aggregate(
|
||||
@ -3151,21 +3123,17 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
write_concern = self._write_concern_for_cmd(cmd, session)
|
||||
client = self._database.client
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> MutableMapping[str, Any]:
|
||||
return await conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
client=client,
|
||||
)
|
||||
|
||||
return await client._retryable_write(False, inner, session, _Op.RENAME)
|
||||
async with await self._conn_for_writes(session, operation=_Op.RENAME) as conn:
|
||||
async with self._database.client._tmp_session(session) as s:
|
||||
return await conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=s,
|
||||
client=self._database.client,
|
||||
)
|
||||
|
||||
async def distinct(
|
||||
self,
|
||||
@ -3342,7 +3310,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
let: Optional[Mapping[str, Any]] = None,
|
||||
comment: Optional[Any] = None,
|
||||
**kwargs: Any,
|
||||
) -> Optional[_DocumentType]:
|
||||
) -> _DocumentType:
|
||||
"""Finds a single document and deletes it, returning the document.
|
||||
|
||||
>>> await db.test.count_documents({'x': 1})
|
||||
@ -3352,10 +3320,6 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
>>> await db.test.count_documents({'x': 1})
|
||||
1
|
||||
|
||||
Returns ``None`` if no document matches the filter.
|
||||
|
||||
>>> await db.test.find_one_and_delete({'_exists': False})
|
||||
|
||||
If multiple documents match *filter*, a *sort* can be applied.
|
||||
|
||||
>>> async for doc in db.test.find({'x': 1}):
|
||||
@ -3438,22 +3402,10 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
let: Optional[Mapping[str, Any]] = None,
|
||||
comment: Optional[Any] = None,
|
||||
**kwargs: Any,
|
||||
) -> Optional[_DocumentType]:
|
||||
) -> _DocumentType:
|
||||
"""Finds a single document and replaces it, returning either the
|
||||
original or the replaced document.
|
||||
|
||||
>>> await db.test.find_one({'x': 1})
|
||||
{'_id': 0, 'x': 1}
|
||||
>>> await db.test.find_one_and_replace({'x': 1}, {'y': 2})
|
||||
{'_id': 0, 'x': 1}
|
||||
>>> await db.test.find_one({'x': 1})
|
||||
>>> await db.test.find_one({'y': 2})
|
||||
{'_id': 0, 'y': 2}
|
||||
|
||||
Returns ``None`` if no document matches the filter.
|
||||
|
||||
>>> await db.test.find_one_and_replace({'_exists': False}, {'x': 1})
|
||||
|
||||
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.
|
||||
@ -3558,17 +3510,13 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
let: Optional[Mapping[str, Any]] = None,
|
||||
comment: Optional[Any] = None,
|
||||
**kwargs: Any,
|
||||
) -> Optional[_DocumentType]:
|
||||
) -> _DocumentType:
|
||||
"""Finds a single document and updates it, returning either the
|
||||
original or the updated document.
|
||||
|
||||
>>> await db.test.find_one({'_id': 665})
|
||||
{'_id': 665, 'done': False, 'count': 25}
|
||||
>>> await db.test.find_one_and_update(
|
||||
... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})
|
||||
{'_id': 665, 'done': False, 'count': 25}
|
||||
>>> await db.test.find_one({'_id': 665})
|
||||
{'_id': 665, 'done': True, 'count': 26}
|
||||
{'_id': 665, 'done': False, 'count': 25}}
|
||||
|
||||
Returns ``None`` if no document matches the filter.
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Generic,
|
||||
Mapping,
|
||||
NoReturn,
|
||||
Optional,
|
||||
@ -28,10 +29,17 @@ from typing import (
|
||||
)
|
||||
|
||||
from bson import CodecOptions, _convert_raw_document_lists_to_streams
|
||||
from pymongo.asynchronous.cursor_base import _AsyncCursorBase, _ConnectionManager
|
||||
from pymongo import _csot
|
||||
from pymongo.asynchronous.cursor import _ConnectionManager
|
||||
from pymongo.cursor_shared import _CURSOR_CLOSED_ERRORS
|
||||
from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure
|
||||
from pymongo.message import _GetMore, _OpMsg, _OpReply, _RawBatchGetMore
|
||||
from pymongo.message import (
|
||||
_CursorAddress,
|
||||
_GetMore,
|
||||
_OpMsg,
|
||||
_OpReply,
|
||||
_RawBatchGetMore,
|
||||
)
|
||||
from pymongo.response import PinnedResponse
|
||||
from pymongo.typings import _Address, _DocumentOut, _DocumentType
|
||||
|
||||
@ -43,7 +51,7 @@ if TYPE_CHECKING:
|
||||
_IS_SYNC = False
|
||||
|
||||
|
||||
class AsyncCommandCursor(_AsyncCursorBase[_DocumentType]):
|
||||
class AsyncCommandCursor(Generic[_DocumentType]):
|
||||
"""An asynchronous cursor / iterator over command cursors."""
|
||||
|
||||
_getmore_class = _GetMore
|
||||
@ -90,8 +98,8 @@ class AsyncCommandCursor(_AsyncCursorBase[_DocumentType]):
|
||||
f"max_await_time_ms must be an integer or None, not {type(max_await_time_ms)}"
|
||||
)
|
||||
|
||||
def _get_namespace(self) -> str:
|
||||
return self._ns
|
||||
def __del__(self) -> None:
|
||||
self._die_no_lock()
|
||||
|
||||
def batch_size(self, batch_size: int) -> AsyncCommandCursor[_DocumentType]:
|
||||
"""Limits the number of documents returned in one batch. Each batch
|
||||
@ -153,12 +161,94 @@ class AsyncCommandCursor(_AsyncCursorBase[_DocumentType]):
|
||||
) -> Sequence[_DocumentOut]:
|
||||
return response.unpack_response(cursor_id, codec_options, user_fields, legacy_response)
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
"""Does this cursor have the potential to return more data?
|
||||
|
||||
Even if :attr:`alive` is ``True``, :meth:`next` can raise
|
||||
:exc:`StopIteration`. Best to use a for loop::
|
||||
|
||||
async for doc in collection.aggregate(pipeline):
|
||||
print(doc)
|
||||
|
||||
.. note:: :attr:`alive` can be True while iterating a cursor from
|
||||
a failed server. In this case :attr:`alive` will return False after
|
||||
:meth:`next` fails to retrieve the next batch of results from the
|
||||
server.
|
||||
"""
|
||||
return bool(len(self._data) or (not self._killed))
|
||||
|
||||
@property
|
||||
def cursor_id(self) -> int:
|
||||
"""Returns the id of the cursor."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def address(self) -> Optional[_Address]:
|
||||
"""The (host, port) of the server used, or None.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[AsyncClientSession]:
|
||||
"""The cursor's :class:`~pymongo.asynchronous.client_session.AsyncClientSession`, or None.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
"""
|
||||
if self._session and not self._session._implicit:
|
||||
return self._session
|
||||
return None
|
||||
|
||||
def _prepare_to_die(self) -> tuple[int, Optional[_CursorAddress]]:
|
||||
already_killed = self._killed
|
||||
self._killed = True
|
||||
if self._id and not already_killed:
|
||||
cursor_id = self._id
|
||||
assert self._address is not None
|
||||
address = _CursorAddress(self._address, self._ns)
|
||||
else:
|
||||
# Skip killCursors.
|
||||
cursor_id = 0
|
||||
address = None
|
||||
return cursor_id, address
|
||||
|
||||
def _die_no_lock(self) -> None:
|
||||
"""Closes this cursor without acquiring a lock."""
|
||||
cursor_id, address = self._prepare_to_die()
|
||||
self._collection.database.client._cleanup_cursor_no_lock(
|
||||
cursor_id, address, self._sock_mgr, self._session
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
async def _die_lock(self) -> None:
|
||||
"""Closes this cursor."""
|
||||
cursor_id, address = self._prepare_to_die()
|
||||
await self._collection.database.client._cleanup_cursor_lock(
|
||||
cursor_id,
|
||||
address,
|
||||
self._sock_mgr,
|
||||
self._session,
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
def _end_session(self) -> None:
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session._end_implicit_session()
|
||||
self._session = None
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Explicitly close / kill this cursor."""
|
||||
await self._die_lock()
|
||||
|
||||
async def _send_message(self, operation: _GetMore) -> None:
|
||||
"""Send a getmore message and handle the response."""
|
||||
client = self._collection.database.client
|
||||
@ -240,9 +330,6 @@ class AsyncCommandCursor(_AsyncCursorBase[_DocumentType]):
|
||||
def __aiter__(self) -> AsyncIterator[_DocumentType]:
|
||||
return self
|
||||
|
||||
async def __aenter__(self) -> AsyncCommandCursor[_DocumentType]:
|
||||
return self
|
||||
|
||||
async def next(self) -> _DocumentType:
|
||||
"""Advance the cursor."""
|
||||
# Block until a document is returnable.
|
||||
@ -298,6 +385,41 @@ class AsyncCommandCursor(_AsyncCursorBase[_DocumentType]):
|
||||
"""
|
||||
return await self._try_next(get_more_allowed=True)
|
||||
|
||||
async def __aenter__(self) -> AsyncCommandCursor[_DocumentType]:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
await self.close()
|
||||
|
||||
@_csot.apply
|
||||
async def to_list(self, length: Optional[int] = None) -> list[_DocumentType]:
|
||||
"""Converts the contents of this cursor to a list more efficiently than ``[doc async for doc in cursor]``.
|
||||
|
||||
To use::
|
||||
|
||||
>>> await cursor.to_list()
|
||||
|
||||
Or, so read at most n items from the cursor::
|
||||
|
||||
>>> await cursor.to_list(n)
|
||||
|
||||
If the cursor is empty or has no more results, an empty list will be returned.
|
||||
|
||||
.. versionadded:: 4.9
|
||||
"""
|
||||
res: list[_DocumentType] = []
|
||||
remaining = length
|
||||
if isinstance(length, int) and length < 1:
|
||||
raise ValueError("to_list() length must be greater than 0")
|
||||
while self.alive:
|
||||
if not await self._next_batch(res, remaining):
|
||||
break
|
||||
if length is not None:
|
||||
remaining = length - len(res)
|
||||
if remaining == 0:
|
||||
break
|
||||
return res
|
||||
|
||||
|
||||
class AsyncRawBatchCommandCursor(AsyncCommandCursor[_DocumentType]):
|
||||
_getmore_class = _RawBatchGetMore
|
||||
|
||||
@ -21,6 +21,7 @@ from collections import deque
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
@ -35,9 +36,7 @@ from typing import (
|
||||
from bson import RE_TYPE, _convert_raw_document_lists_to_streams
|
||||
from bson.code import Code
|
||||
from bson.son import SON
|
||||
from pymongo import helpers_shared
|
||||
from pymongo.asynchronous.cursor_base import _AsyncCursorBase, _ConnectionManager
|
||||
from pymongo.asynchronous.helpers import anext
|
||||
from pymongo import _csot, helpers_shared
|
||||
from pymongo.collation import validate_collation_or_none
|
||||
from pymongo.common import (
|
||||
validate_is_document_type,
|
||||
@ -45,7 +44,9 @@ from pymongo.common import (
|
||||
)
|
||||
from pymongo.cursor_shared import _CURSOR_CLOSED_ERRORS, _QUERY_OPTIONS, CursorType, _Hint, _Sort
|
||||
from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure
|
||||
from pymongo.lock import _async_create_lock
|
||||
from pymongo.message import (
|
||||
_CursorAddress,
|
||||
_GetMore,
|
||||
_OpMsg,
|
||||
_OpReply,
|
||||
@ -63,12 +64,31 @@ if TYPE_CHECKING:
|
||||
from bson.codec_options import CodecOptions
|
||||
from pymongo.asynchronous.client_session import AsyncClientSession
|
||||
from pymongo.asynchronous.collection import AsyncCollection
|
||||
from pymongo.asynchronous.pool import AsyncConnection
|
||||
from pymongo.read_preferences import _ServerMode
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
|
||||
class AsyncCursor(_AsyncCursorBase[_DocumentType]):
|
||||
class _ConnectionManager:
|
||||
"""Used with exhaust cursors to ensure the connection is returned."""
|
||||
|
||||
def __init__(self, conn: AsyncConnection, more_to_come: bool):
|
||||
self.conn: Optional[AsyncConnection] = conn
|
||||
self.more_to_come = more_to_come
|
||||
self._lock = _async_create_lock()
|
||||
|
||||
def update_exhaust(self, more_to_come: bool) -> None:
|
||||
self.more_to_come = more_to_come
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Return this instance's connection to the connection pool."""
|
||||
if self.conn:
|
||||
await self.conn.unpin()
|
||||
self.conn = None
|
||||
|
||||
|
||||
class AsyncCursor(Generic[_DocumentType]):
|
||||
_query_class = _Query
|
||||
_getmore_class = _GetMore
|
||||
|
||||
@ -245,8 +265,8 @@ class AsyncCursor(_AsyncCursorBase[_DocumentType]):
|
||||
"""The number of documents retrieved so far."""
|
||||
return self._retrieved
|
||||
|
||||
def _get_namespace(self) -> str:
|
||||
return f"{self._dbname}.{self._collname}"
|
||||
def __del__(self) -> None:
|
||||
self._die_no_lock()
|
||||
|
||||
def clone(self) -> AsyncCursor[_DocumentType]:
|
||||
"""Get a clone of this cursor.
|
||||
@ -878,6 +898,55 @@ class AsyncCursor(_AsyncCursorBase[_DocumentType]):
|
||||
self._read_preference = self._collection._read_preference_for(self.session)
|
||||
return self._read_preference
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
"""Does this cursor have the potential to return more data?
|
||||
|
||||
This is mostly useful with `tailable cursors
|
||||
<https://www.mongodb.com/docs/manual/core/tailable-cursors/>`_
|
||||
since they will stop iterating even though they *may* return more
|
||||
results in the future.
|
||||
|
||||
With regular cursors, simply use an asynchronous for loop instead of :attr:`alive`::
|
||||
|
||||
async for doc in collection.find():
|
||||
print(doc)
|
||||
|
||||
.. note:: Even if :attr:`alive` is True, :meth:`next` can raise
|
||||
:exc:`StopIteration`. :attr:`alive` can also be True while iterating
|
||||
a cursor from a failed server. In this case :attr:`alive` will
|
||||
return False after :meth:`next` fails to retrieve the next batch
|
||||
of results from the server.
|
||||
"""
|
||||
return bool(len(self._data) or (not self._killed))
|
||||
|
||||
@property
|
||||
def cursor_id(self) -> Optional[int]:
|
||||
"""Returns the id of the cursor
|
||||
|
||||
.. versionadded:: 2.2
|
||||
"""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def address(self) -> Optional[tuple[str, Any]]:
|
||||
"""The (host, port) of the server used, or None.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Renamed from "conn_id".
|
||||
"""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[AsyncClientSession]:
|
||||
"""The cursor's :class:`~pymongo.asynchronous.client_session.AsyncClientSession`, or None.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
"""
|
||||
if self._session and not self._session._implicit:
|
||||
return self._session
|
||||
return None
|
||||
|
||||
def __copy__(self) -> AsyncCursor[_DocumentType]:
|
||||
"""Support function for `copy.copy()`.
|
||||
|
||||
@ -941,6 +1010,59 @@ class AsyncCursor(_AsyncCursorBase[_DocumentType]):
|
||||
y[key] = value # type:ignore[index]
|
||||
return y
|
||||
|
||||
def _prepare_to_die(self, already_killed: bool) -> tuple[int, Optional[_CursorAddress]]:
|
||||
self._killed = True
|
||||
if self._id and not already_killed:
|
||||
cursor_id = self._id
|
||||
assert self._address is not None
|
||||
address = _CursorAddress(self._address, f"{self._dbname}.{self._collname}")
|
||||
else:
|
||||
# Skip killCursors.
|
||||
cursor_id = 0
|
||||
address = None
|
||||
return cursor_id, address
|
||||
|
||||
def _die_no_lock(self) -> None:
|
||||
"""Closes this cursor without acquiring a lock."""
|
||||
try:
|
||||
already_killed = self._killed
|
||||
except AttributeError:
|
||||
# ___init__ did not run to completion (or at all).
|
||||
return
|
||||
|
||||
cursor_id, address = self._prepare_to_die(already_killed)
|
||||
self._collection.database.client._cleanup_cursor_no_lock(
|
||||
cursor_id, address, self._sock_mgr, self._session
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
async def _die_lock(self) -> None:
|
||||
"""Closes this cursor."""
|
||||
try:
|
||||
already_killed = self._killed
|
||||
except AttributeError:
|
||||
# ___init__ did not run to completion (or at all).
|
||||
return
|
||||
|
||||
cursor_id, address = self._prepare_to_die(already_killed)
|
||||
await self._collection.database.client._cleanup_cursor_lock(
|
||||
cursor_id,
|
||||
address,
|
||||
self._sock_mgr,
|
||||
self._session,
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Explicitly close / kill this cursor."""
|
||||
await self._die_lock()
|
||||
|
||||
async def distinct(self, key: str) -> list[Any]:
|
||||
"""Get a list of distinct values for `key` among all documents
|
||||
in the result set of this query.
|
||||
@ -1173,8 +1295,40 @@ class AsyncCursor(_AsyncCursorBase[_DocumentType]):
|
||||
async def __aenter__(self) -> AsyncCursor[_DocumentType]:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
await self.close()
|
||||
|
||||
class AsyncRawBatchCursor(AsyncCursor[_DocumentType]):
|
||||
@_csot.apply
|
||||
async def to_list(self, length: Optional[int] = None) -> list[_DocumentType]:
|
||||
"""Converts the contents of this cursor to a list more efficiently than ``[doc async for doc in cursor]``.
|
||||
|
||||
To use::
|
||||
|
||||
>>> await cursor.to_list()
|
||||
|
||||
Or, to read at most n items from the cursor::
|
||||
|
||||
>>> await cursor.to_list(n)
|
||||
|
||||
If the cursor is empty or has no more results, an empty list will be returned.
|
||||
|
||||
.. versionadded:: 4.9
|
||||
"""
|
||||
res: list[_DocumentType] = []
|
||||
remaining = length
|
||||
if isinstance(length, int) and length < 1:
|
||||
raise ValueError("to_list() length must be greater than 0")
|
||||
while self.alive:
|
||||
if not await self._next_batch(res, remaining):
|
||||
break
|
||||
if length is not None:
|
||||
remaining = length - len(res)
|
||||
if remaining == 0:
|
||||
break
|
||||
return res
|
||||
|
||||
|
||||
class AsyncRawBatchCursor(AsyncCursor, Generic[_DocumentType]): # type: ignore[type-arg]
|
||||
"""An asynchronous cursor / iterator over raw batches of BSON data from a query result."""
|
||||
|
||||
_query_class = _RawBatchQuery
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
# Copyright 2026-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.
|
||||
|
||||
"""Asynchronous cursor base extending the shared agnostic cursor base."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from pymongo import _csot
|
||||
from pymongo.cursor_shared import _AgnosticCursorBase
|
||||
from pymongo.lock import _async_create_lock
|
||||
from pymongo.typings import _DocumentType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pymongo.asynchronous.client_session import AsyncClientSession
|
||||
from pymongo.asynchronous.pool import AsyncConnection
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
|
||||
class _ConnectionManager:
|
||||
"""Used with exhaust cursors to ensure the connection is returned."""
|
||||
|
||||
def __init__(self, conn: AsyncConnection, more_to_come: bool):
|
||||
self.conn: Optional[AsyncConnection] = conn
|
||||
self.more_to_come = more_to_come
|
||||
self._lock = _async_create_lock()
|
||||
|
||||
def update_exhaust(self, more_to_come: bool) -> None:
|
||||
self.more_to_come = more_to_come
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Return this instance's connection to the connection pool."""
|
||||
if self.conn:
|
||||
await self.conn.unpin()
|
||||
self.conn = None
|
||||
|
||||
|
||||
class _AsyncCursorBase(_AgnosticCursorBase[_DocumentType]):
|
||||
"""Asynchronous cursor base class."""
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[AsyncClientSession]:
|
||||
"""The cursor's :class:`~pymongo.asynchronous.client_session.AsyncClientSession`, or None.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
"""
|
||||
if self._session and not self._session._implicit:
|
||||
return self._session
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
async def _next_batch(self, result: list, total: Optional[int] = None) -> bool: # type: ignore[type-arg]
|
||||
...
|
||||
|
||||
async def _die_lock(self) -> None:
|
||||
"""Closes this cursor."""
|
||||
try:
|
||||
already_killed = self._killed
|
||||
except AttributeError:
|
||||
# ___init__ did not run to completion (or at all).
|
||||
return
|
||||
|
||||
cursor_id, address = self._prepare_to_die(already_killed)
|
||||
await self._collection.database.client._cleanup_cursor_lock(
|
||||
cursor_id,
|
||||
address,
|
||||
self._sock_mgr,
|
||||
self._session,
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Explicitly close / kill this cursor."""
|
||||
await self._die_lock()
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
await self.close()
|
||||
|
||||
@_csot.apply
|
||||
async def to_list(self, length: Optional[int] = None) -> list[_DocumentType]:
|
||||
"""Converts the contents of this cursor to a list more efficiently than ``[doc async for doc in cursor]``.
|
||||
|
||||
To use::
|
||||
|
||||
>>> await cursor.to_list()
|
||||
|
||||
Or, to read at most n items from the cursor::
|
||||
|
||||
>>> await cursor.to_list(n)
|
||||
|
||||
If the cursor is empty or has no more results, an empty list will be returned.
|
||||
|
||||
.. versionadded:: 4.9
|
||||
"""
|
||||
res: list[_DocumentType] = []
|
||||
remaining = length
|
||||
if isinstance(length, int) and length < 1:
|
||||
raise ValueError("to_list() length must be greater than 0")
|
||||
while self.alive:
|
||||
if not await self._next_batch(res, remaining):
|
||||
break
|
||||
if length is not None:
|
||||
remaining = length - len(res)
|
||||
if remaining == 0:
|
||||
break
|
||||
return res
|
||||
@ -698,7 +698,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
.. versionadded:: 3.9
|
||||
|
||||
.. _aggregation pipeline:
|
||||
https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
|
||||
https://mongodb.com/docs/manual/reference/operator/aggregation-pipeline
|
||||
|
||||
.. _aggregate command:
|
||||
https://mongodb.com/docs/manual/reference/command/aggregate
|
||||
@ -931,15 +931,14 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (session and session._txn_read_preference()) or ReadPreference.PRIMARY
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession],
|
||||
_server: Server,
|
||||
conn: AsyncConnection,
|
||||
read_preference: _ServerMode,
|
||||
) -> Union[dict[str, Any], _CodecDocumentType]:
|
||||
async with await self._client._conn_for_reads(
|
||||
read_preference, session, operation=command_name
|
||||
) as (
|
||||
connection,
|
||||
read_preference,
|
||||
):
|
||||
return await self._command(
|
||||
conn,
|
||||
connection,
|
||||
command,
|
||||
value,
|
||||
check,
|
||||
@ -950,10 +949,6 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return await self._client._retryable_read(
|
||||
inner, read_preference, session, command_name, None, False, is_run_command=True
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
async def cursor_command(
|
||||
self,
|
||||
@ -1021,17 +1016,17 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
async with self._client._tmp_session(session) as tmp_session:
|
||||
opts = codec_options or DEFAULT_CODEC_OPTIONS
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (
|
||||
tmp_session and tmp_session._txn_read_preference()
|
||||
) or ReadPreference.PRIMARY
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession],
|
||||
_server: Server,
|
||||
conn: AsyncConnection,
|
||||
read_preference: _ServerMode,
|
||||
) -> AsyncCommandCursor[_DocumentType]:
|
||||
async with await self._client._conn_for_reads(
|
||||
read_preference, tmp_session, command_name
|
||||
) as (
|
||||
conn,
|
||||
read_preference,
|
||||
):
|
||||
response = await self._command(
|
||||
conn,
|
||||
command,
|
||||
@ -1040,7 +1035,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
None,
|
||||
read_preference,
|
||||
opts,
|
||||
session=session,
|
||||
session=tmp_session,
|
||||
**kwargs,
|
||||
)
|
||||
coll = self.get_collection("$cmd", read_preference=read_preference)
|
||||
@ -1050,7 +1045,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
response["cursor"],
|
||||
conn.address,
|
||||
max_await_time_ms=max_await_time_ms,
|
||||
session=session,
|
||||
session=tmp_session,
|
||||
comment=comment,
|
||||
)
|
||||
await cmd_cursor._maybe_pin_connection(conn)
|
||||
@ -1058,10 +1053,6 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
else:
|
||||
raise InvalidOperation("Command does not return a cursor.")
|
||||
|
||||
return await self.client._retryable_read(
|
||||
inner, read_preference, tmp_session, command_name, None, False
|
||||
)
|
||||
|
||||
async def _retryable_read_command(
|
||||
self,
|
||||
command: Union[str, MutableMapping[str, Any]],
|
||||
@ -1263,11 +1254,9 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
command["comment"] = comment
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> dict[str, Any]:
|
||||
async with await self._client._conn_for_writes(session, operation=_Op.DROP) as connection:
|
||||
return await self._command(
|
||||
conn,
|
||||
connection,
|
||||
command,
|
||||
allowable_errors=["ns not found", 26],
|
||||
write_concern=self._write_concern_for(session),
|
||||
@ -1275,8 +1264,6 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
return await self.client._retryable_write(False, inner, session, _Op.DROP)
|
||||
|
||||
@_csot.apply
|
||||
async def drop_collection(
|
||||
self,
|
||||
|
||||
@ -717,10 +717,7 @@ class AsyncClientEncryption(Generic[_DocumentType]):
|
||||
self._encryption = AsyncExplicitEncrypter(
|
||||
self._io_callbacks,
|
||||
_create_mongocrypt_options(
|
||||
kms_providers=kms_providers,
|
||||
schema_map=None,
|
||||
key_expiration_ms=key_expiration_ms,
|
||||
bypass_encryption=True, # Don't load crypt_shared
|
||||
kms_providers=kms_providers, schema_map=None, key_expiration_ms=key_expiration_ms
|
||||
),
|
||||
)
|
||||
# Use the same key vault collection as the callback.
|
||||
|
||||
@ -16,12 +16,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import functools
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@ -29,8 +24,6 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from pymongo import _csot
|
||||
from pymongo.common import MAX_ADAPTIVE_RETRIES
|
||||
from pymongo.errors import (
|
||||
OperationFailure,
|
||||
)
|
||||
@ -43,7 +36,6 @@ F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def _handle_reauth(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
async def inner(*args: Any, **kwargs: Any) -> Any:
|
||||
no_reauth = kwargs.pop("no_reauth", False)
|
||||
from pymongo.asynchronous.pool import AsyncConnection
|
||||
@ -76,46 +68,6 @@ def _handle_reauth(func: F) -> F:
|
||||
return cast(F, inner)
|
||||
|
||||
|
||||
_BACKOFF_INITIAL = 0.1
|
||||
_BACKOFF_MAX = 10
|
||||
|
||||
|
||||
def _backoff(
|
||||
attempt: int, initial_delay: float = _BACKOFF_INITIAL, max_delay: float = _BACKOFF_MAX
|
||||
) -> float:
|
||||
jitter = random.random() # noqa: S311
|
||||
return jitter * min(initial_delay * (2**attempt), max_delay)
|
||||
|
||||
|
||||
class _RetryPolicy:
|
||||
"""A retry limiter that performs exponential backoff with jitter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attempts: int = MAX_ADAPTIVE_RETRIES,
|
||||
backoff_initial: float = _BACKOFF_INITIAL,
|
||||
backoff_max: float = _BACKOFF_MAX,
|
||||
):
|
||||
self.attempts = attempts
|
||||
self.backoff_initial = backoff_initial
|
||||
self.backoff_max = backoff_max
|
||||
|
||||
def backoff(self, attempt: int) -> float:
|
||||
"""Return the backoff duration for the given attempt."""
|
||||
return _backoff(max(0, attempt - 1), self.backoff_initial, self.backoff_max)
|
||||
|
||||
async def should_retry(self, attempt: int, delay: float) -> bool:
|
||||
"""Return if we have retry attempts remaining and the next backoff would not exceed a timeout."""
|
||||
if attempt > self.attempts:
|
||||
return False
|
||||
|
||||
if _csot.get_timeout():
|
||||
if time.monotonic() + delay > _csot.get_deadline():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _getaddrinfo(
|
||||
host: Any, port: Any, **kwargs: Any
|
||||
) -> list[
|
||||
@ -132,17 +84,3 @@ async def _getaddrinfo(
|
||||
return await loop.getaddrinfo(host, port, **kwargs) # type: ignore[return-value]
|
||||
else:
|
||||
return socket.getaddrinfo(host, port, **kwargs)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
anext = builtins.anext
|
||||
aiter = builtins.aiter
|
||||
else:
|
||||
|
||||
async def anext(cls: Any) -> Any:
|
||||
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
|
||||
return await cls.__anext__()
|
||||
|
||||
def aiter(cls: Any) -> Any:
|
||||
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
|
||||
return cls.__aiter__()
|
||||
|
||||
@ -35,7 +35,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
import warnings
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
@ -66,11 +65,8 @@ from pymongo import _csot, common, helpers_shared, periodic_executor
|
||||
from pymongo.asynchronous import client_session, database, uri_parser
|
||||
from pymongo.asynchronous.change_stream import AsyncChangeStream, AsyncClusterChangeStream
|
||||
from pymongo.asynchronous.client_bulk import _AsyncClientBulk
|
||||
from pymongo.asynchronous.client_session import _SESSION, _EmptyServerSession
|
||||
from pymongo.asynchronous.client_session import _EmptyServerSession
|
||||
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
|
||||
from pymongo.asynchronous.helpers import (
|
||||
_RetryPolicy,
|
||||
)
|
||||
from pymongo.asynchronous.settings import TopologySettings
|
||||
from pymongo.asynchronous.topology import Topology, _ErrorContext
|
||||
from pymongo.client_options import ClientOptions
|
||||
@ -143,7 +139,7 @@ if TYPE_CHECKING:
|
||||
from bson.objectid import ObjectId
|
||||
from pymongo.asynchronous.bulk import _AsyncBulk
|
||||
from pymongo.asynchronous.client_session import AsyncClientSession, _ServerSession
|
||||
from pymongo.asynchronous.cursor_base import _ConnectionManager
|
||||
from pymongo.asynchronous.cursor import _ConnectionManager
|
||||
from pymongo.asynchronous.encryption import _Encrypter
|
||||
from pymongo.asynchronous.pool import AsyncConnection
|
||||
from pymongo.asynchronous.server import Server
|
||||
@ -426,8 +422,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
with the server. Currently supported options are "snappy", "zlib"
|
||||
and "zstd". Support for snappy requires the
|
||||
`python-snappy <https://pypi.org/project/python-snappy/>`_ package.
|
||||
zlib support requires the Python standard library zlib module. For
|
||||
Python before 3.14 zstd requires the `backports.zstd <https://pypi.org/project/backports.zstd/>`_
|
||||
zlib support requires the Python standard library zlib module. zstd
|
||||
requires the `zstandard <https://pypi.org/project/zstandard/>`_
|
||||
package. By default no compression is used. Compression support
|
||||
must also be enabled on the server. MongoDB 3.6+ supports snappy
|
||||
and zlib compression. MongoDB 4.2+ adds support for zstd.
|
||||
@ -614,18 +610,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
client to use Stable API. See `versioned API <https://www.mongodb.com/docs/manual/reference/stable-api/#what-is-the-stable-api--and-should-you-use-it->`_ for
|
||||
details.
|
||||
|
||||
| **Overload retry options:**
|
||||
|
||||
- `max_adaptive_retries`: (int) How many retries to allow for overload errors. Defaults to ``2``.
|
||||
- `enable_overload_retargeting`: (boolean) Whether overload retargeting is enabled for this client.
|
||||
If enabled, server overload errors will cause retry attempts to select a server that has not yet returned an overload error, if possible.
|
||||
Defaults to ``False``.
|
||||
|
||||
.. seealso:: The MongoDB documentation on `connections <https://dochub.mongodb.org/core/connections>`_.
|
||||
|
||||
.. versionchanged:: 4.17
|
||||
Added the ``max_adaptive_retries`` and ``enable_overload_retargeting`` URI and keyword arguments.
|
||||
|
||||
.. versionchanged:: 4.5
|
||||
Added the ``serverMonitoringMode`` keyword argument.
|
||||
|
||||
@ -893,14 +879,11 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self._options.read_concern,
|
||||
)
|
||||
|
||||
self._retry_policy = _RetryPolicy(attempts=self._options.max_adaptive_retries)
|
||||
|
||||
self._init_based_on_options(self._seeds, srv_max_hosts, srv_service_name)
|
||||
|
||||
self._opened = False
|
||||
self._closed = False
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
if not is_srv:
|
||||
self._init_background()
|
||||
|
||||
@ -1425,8 +1408,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
def _ensure_session(
|
||||
self, session: Optional[AsyncClientSession] = None
|
||||
) -> Optional[AsyncClientSession]:
|
||||
"""If provided session and bound session are None, lend a temporary session."""
|
||||
session = session or self._get_bound_session()
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
if session:
|
||||
return session
|
||||
|
||||
@ -2008,8 +1990,6 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref: Optional[_ServerMode] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Internal retryable helper for all client transactions.
|
||||
|
||||
@ -2021,8 +2001,6 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Server Address, defaults to None
|
||||
:param read_pref: Topology of read operation, defaults to None
|
||||
:param retryable: If the operation should be retried once, defaults to None
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
|
||||
:return: Output of the calling func()
|
||||
"""
|
||||
@ -2037,8 +2015,6 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address=address,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
).run()
|
||||
|
||||
async def _retryable_read(
|
||||
@ -2050,8 +2026,6 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = True,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Execute an operation with consecutive retries if possible
|
||||
|
||||
@ -2067,8 +2041,6 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Optional address when sending a message, defaults to None
|
||||
:param retryable: if we should attempt retries
|
||||
(may not always be supported even if supplied), defaults to False
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False.
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
"""
|
||||
|
||||
# Ensure that the client supports retrying on reads and there is no session in
|
||||
@ -2087,8 +2059,6 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref=read_pref,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
)
|
||||
|
||||
async def _retryable_write(
|
||||
@ -2297,14 +2267,11 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self, session: Optional[client_session.AsyncClientSession]
|
||||
) -> AsyncGenerator[Optional[client_session.AsyncClientSession], None]:
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
if session is not None and not isinstance(session, client_session.AsyncClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be an AsyncClientSession or None, not {type(session)}"
|
||||
)
|
||||
|
||||
# Check for a bound session. If one exists, treat it as an explicitly passed session.
|
||||
session = session or self._get_bound_session()
|
||||
if session:
|
||||
if session is not None:
|
||||
if not isinstance(session, client_session.AsyncClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be an AsyncClientSession or None, not {type(session)}"
|
||||
)
|
||||
# Don't call end_session.
|
||||
yield session
|
||||
return
|
||||
@ -2334,18 +2301,6 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
if session is not None:
|
||||
session._process_response(reply)
|
||||
|
||||
def _get_bound_session(self) -> Optional[AsyncClientSession]:
|
||||
bound_session = _SESSION.get()
|
||||
if bound_session:
|
||||
if bound_session.client is self:
|
||||
return bound_session
|
||||
else:
|
||||
raise InvalidOperation(
|
||||
"Only the client that created the bound session can perform operations within its context block. See <PLACEHOLDER> for more information."
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
async def server_info(
|
||||
self, session: Optional[client_session.AsyncClientSession] = None
|
||||
) -> dict[str, Any]:
|
||||
@ -2483,13 +2438,15 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
f"name_or_database must be an instance of str or a AsyncDatabase, not {type(name)}"
|
||||
)
|
||||
|
||||
await self[name].command(
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
async with await self._conn_for_writes(session, operation=_Op.DROP_DATABASE) as conn:
|
||||
await self[name]._command(
|
||||
conn,
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
async def bulk_write(
|
||||
@ -2773,15 +2730,12 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
):
|
||||
self._last_error: Optional[Exception] = None
|
||||
self._retrying = False
|
||||
self._always_retryable = False
|
||||
self._max_retries = float("inf") if _csot.get_timeout() is not None else 1
|
||||
self._multiple_retries = _csot.get_timeout() is not None
|
||||
self._client = mongo_client
|
||||
self._retry_policy = mongo_client._retry_policy
|
||||
|
||||
self._func = func
|
||||
self._bulk = bulk
|
||||
self._session = session
|
||||
@ -2797,8 +2751,6 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._operation = operation
|
||||
self._operation_id = operation_id
|
||||
self._attempt_number = 0
|
||||
self._is_run_command = is_run_command
|
||||
self._is_aggregate_write = is_aggregate_write
|
||||
|
||||
async def run(self) -> T:
|
||||
"""Runs the supplied func() and attempts a retry
|
||||
@ -2818,13 +2770,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
while True:
|
||||
self._check_last_error(check_csot=True)
|
||||
try:
|
||||
res = await self._read() if self._is_read else await self._write()
|
||||
# Track whether the transaction has completed a command.
|
||||
# If we need to apply backpressure to the first command,
|
||||
# we will need to revert back to starting state.
|
||||
if self._session is not None and self._session.in_transaction:
|
||||
self._session._transaction.has_completed_command = True
|
||||
return res
|
||||
return await self._read() if self._is_read else await self._write()
|
||||
except ServerSelectionTimeoutError:
|
||||
# The application may think the write was never attempted
|
||||
# if we raise ServerSelectionTimeoutError on the retry
|
||||
@ -2835,80 +2781,37 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
# most likely be a waste of time.
|
||||
raise
|
||||
except PyMongoError as exc:
|
||||
always_retryable = False
|
||||
overloaded = False
|
||||
exc_to_check = exc
|
||||
|
||||
if self._is_run_command and not (
|
||||
self._client.options.retry_reads and self._client.options.retry_writes
|
||||
):
|
||||
raise
|
||||
if self._is_aggregate_write and not self._client.options.retry_writes:
|
||||
raise
|
||||
|
||||
# Execute specialized catch on read
|
||||
if self._is_read:
|
||||
if isinstance(exc, (ConnectionFailure, OperationFailure)):
|
||||
# ConnectionFailures do not supply a code property
|
||||
exc_code = getattr(exc, "code", None)
|
||||
overloaded = exc.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc.has_error_label("RetryableError") and overloaded
|
||||
if not self._client.options.retry_reads or (
|
||||
not always_retryable
|
||||
and (
|
||||
self._is_not_eligible_for_retry()
|
||||
or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
)
|
||||
)
|
||||
if self._is_not_eligible_for_retry() or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
):
|
||||
raise
|
||||
self._retrying = True
|
||||
self._last_error = exc
|
||||
self._attempt_number += 1
|
||||
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if (
|
||||
overloaded
|
||||
and self._session is not None
|
||||
and self._session.in_transaction
|
||||
):
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
else:
|
||||
raise
|
||||
|
||||
# Specialized catch on write operation
|
||||
if not self._is_read:
|
||||
if isinstance(exc, ClientBulkWriteException) and isinstance(
|
||||
exc.error, PyMongoError
|
||||
):
|
||||
exc_to_check = exc.error
|
||||
retryable_write_label = exc_to_check.has_error_label("RetryableWriteError")
|
||||
overloaded = exc_to_check.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc_to_check.has_error_label("RetryableError") and overloaded
|
||||
|
||||
# Always retry abortTransaction and commitTransaction up to once
|
||||
if self._operation not in ["abortTransaction", "commitTransaction"] and (
|
||||
not self._client.options.retry_writes
|
||||
or not (self._retryable or always_retryable)
|
||||
):
|
||||
if not self._retryable:
|
||||
raise
|
||||
if retryable_write_label or always_retryable:
|
||||
if isinstance(exc, ClientBulkWriteException) and exc.error:
|
||||
retryable_write_error_exc = isinstance(
|
||||
exc.error, PyMongoError
|
||||
) and exc.error.has_error_label("RetryableWriteError")
|
||||
else:
|
||||
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
|
||||
if retryable_write_error_exc:
|
||||
assert self._session
|
||||
await self._session._unpin()
|
||||
if not always_retryable and (
|
||||
not retryable_write_label or self._is_not_eligible_for_retry()
|
||||
):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
if not retryable_write_error_exc or self._is_not_eligible_for_retry():
|
||||
if exc.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
@ -2917,39 +2820,17 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._bulk.retrying = True
|
||||
else:
|
||||
self._retrying = True
|
||||
if not exc_to_check.has_error_label("NoWritesPerformed"):
|
||||
if not exc.has_error_label("NoWritesPerformed"):
|
||||
self._last_error = exc
|
||||
if self._last_error is None:
|
||||
self._last_error = exc
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if overloaded and self._session is not None and self._session.in_transaction:
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
|
||||
if self._server is not None and (
|
||||
self._client.topology_description.topology_type_name == "Sharded"
|
||||
or (overloaded and self._client.options.enable_overload_retargeting)
|
||||
):
|
||||
if self._client.topology_description.topology_type == TOPOLOGY_TYPE.Sharded:
|
||||
self._deprioritized_servers.append(self._server)
|
||||
|
||||
self._always_retryable = always_retryable
|
||||
if overloaded:
|
||||
delay = self._retry_policy.backoff(self._attempt_number)
|
||||
if not await self._retry_policy.should_retry(self._attempt_number, delay):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
def _is_not_eligible_for_retry(self) -> bool:
|
||||
"""Checks if the exchange is not eligible for retry"""
|
||||
return not self._retryable or (
|
||||
self._is_retrying() and self._attempt_number >= self._max_retries
|
||||
)
|
||||
return not self._retryable or (self._is_retrying() and not self._multiple_retries)
|
||||
|
||||
def _is_retrying(self) -> bool:
|
||||
"""Checks if the exchange is currently undergoing a retry"""
|
||||
@ -3008,7 +2889,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
and conn.supports_sessions
|
||||
)
|
||||
is_mongos = conn.is_mongos
|
||||
if not self._always_retryable and not sessions_supported:
|
||||
if not sessions_supported:
|
||||
# A retry is not possible because this server does
|
||||
# not support sessions raise the last error.
|
||||
self._check_last_error()
|
||||
@ -3040,7 +2921,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
conn,
|
||||
read_pref,
|
||||
):
|
||||
if self._retrying and not self._retryable and not self._always_retryable:
|
||||
if self._retrying and not self._retryable:
|
||||
self._check_last_error()
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
|
||||
@ -19,8 +19,6 @@ import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import weakref
|
||||
@ -54,12 +52,10 @@ from pymongo.errors import ( # type:ignore[attr-defined]
|
||||
DocumentTooLarge,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
_CertificateError,
|
||||
)
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
|
||||
@ -108,6 +104,21 @@ if TYPE_CHECKING:
|
||||
from pymongo.typings import _Address, _CollationIn
|
||||
from pymongo.write_concern import WriteConcern
|
||||
|
||||
try:
|
||||
from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl
|
||||
|
||||
def _set_non_inheritable_non_atomic(fd: int) -> None:
|
||||
"""Set the close-on-exec flag on the given file descriptor."""
|
||||
flags = fcntl(fd, F_GETFD)
|
||||
fcntl(fd, F_SETFD, flags | FD_CLOEXEC)
|
||||
|
||||
except ImportError:
|
||||
# Windows, various platforms we don't claim to support
|
||||
# (Jython, IronPython, ..), systems that don't provide
|
||||
# everything we need from fcntl, etc.
|
||||
def _set_non_inheritable_non_atomic(fd: int) -> None: # noqa: ARG001
|
||||
"""Dummy function for platforms that don't provide fcntl."""
|
||||
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
@ -254,7 +265,6 @@ class AsyncConnection:
|
||||
cmd = self.hello_cmd()
|
||||
performing_handshake = not self.performed_handshake
|
||||
awaitable = False
|
||||
cmd["backpressure"] = True
|
||||
if performing_handshake:
|
||||
self.performed_handshake = True
|
||||
cmd["client"] = self.opts.metadata
|
||||
@ -696,6 +706,8 @@ class PoolState:
|
||||
CLOSED = 3
|
||||
|
||||
|
||||
# Do *not* explicitly inherit from object or Jython won't call __del__
|
||||
# https://bugs.jython.org/issue1057
|
||||
class Pool:
|
||||
def __init__(
|
||||
self,
|
||||
@ -757,10 +769,14 @@ class Pool:
|
||||
# Enforces: maxConnecting
|
||||
# Also used for: clearing the wait queue
|
||||
self._max_connecting_cond = _async_create_condition(self.lock)
|
||||
self._pending = 0
|
||||
self._max_connecting = self.opts.max_connecting
|
||||
self._pending = 0
|
||||
self._client_id = client_id
|
||||
# Log before publishing event to prevent potential listener preemption in tests
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -770,11 +786,6 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
**self.opts.non_default_options,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
# Similar to active_sockets but includes threads in the wait queue.
|
||||
self.operation_count: int = 0
|
||||
# Retain references to pinned connections to prevent the CPython GC
|
||||
@ -789,6 +800,9 @@ class Pool:
|
||||
async with self.lock:
|
||||
if self.state != PoolState.READY:
|
||||
self.state = PoolState.READY
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -797,9 +811,6 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
@ -860,6 +871,9 @@ class Pool:
|
||||
else:
|
||||
for conn in sockets:
|
||||
await conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -868,11 +882,15 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
else:
|
||||
if old_state != PoolState.PAUSED:
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -882,13 +900,6 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
serviceId=service_id,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if not _IS_SYNC:
|
||||
await asyncio.gather(
|
||||
*[conn.close_conn(ConnectionClosedReason.STALE) for conn in sockets], # type: ignore[func-returns-value]
|
||||
@ -992,21 +1003,6 @@ class Pool:
|
||||
self.requests -= 1
|
||||
self.size_cond.notify()
|
||||
|
||||
def _handle_connection_error(self, error: BaseException) -> None:
|
||||
# Handle system overload condition for non-sdam pools.
|
||||
# Look for errors of type AutoReconnect and add error labels if appropriate.
|
||||
if self.is_sdam or type(error) not in (AutoReconnect, NetworkTimeout):
|
||||
return
|
||||
assert isinstance(error, AutoReconnect) # Appease type checker.
|
||||
# If the original error was a DNS, certificate, or SSL error, ignore it.
|
||||
if isinstance(error.__cause__, (_CertificateError, SSLErrors, socket.gaierror)):
|
||||
# End of file errors are excluded, because the server may have disconnected
|
||||
# during the handshake.
|
||||
if not isinstance(error.__cause__, (ssl.SSLEOFError, ssl.SSLZeroReturnError)):
|
||||
return
|
||||
error._add_error_label("SystemOverloadedError")
|
||||
error._add_error_label("RetryableError")
|
||||
|
||||
async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> AsyncConnection:
|
||||
"""Connect to Mongo and return a new AsyncConnection.
|
||||
|
||||
@ -1058,10 +1054,10 @@ class Pool:
|
||||
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
|
||||
error=ConnectionClosedReason.ERROR,
|
||||
)
|
||||
self._handle_connection_error(error)
|
||||
if isinstance(error, (IOError, OSError, *SSLErrors)):
|
||||
details = _get_timeout_details(self.opts)
|
||||
_raise_connection_failure(self.address, error, timeout_details=details)
|
||||
|
||||
raise
|
||||
|
||||
conn = AsyncConnection(networking_interface, self, self.address, conn_id, self.is_sdam) # type: ignore[arg-type]
|
||||
@ -1070,22 +1066,18 @@ class Pool:
|
||||
self.active_contexts.discard(tmp_context)
|
||||
if tmp_context.cancelled:
|
||||
conn.cancel_context.cancel()
|
||||
completed_hello = False
|
||||
try:
|
||||
if not self.is_sdam:
|
||||
await conn.hello()
|
||||
completed_hello = True
|
||||
self.is_writable = conn.is_writable
|
||||
if handler:
|
||||
handler.contribute_socket(conn, completed_handshake=False)
|
||||
|
||||
await conn.authenticate()
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException as e:
|
||||
except BaseException:
|
||||
async with self.lock:
|
||||
self.active_contexts.discard(conn.cancel_context)
|
||||
if not completed_hello:
|
||||
self._handle_connection_error(e)
|
||||
await conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
raise
|
||||
|
||||
@ -1414,8 +1406,8 @@ class Pool:
|
||||
:class:`~pymongo.errors.AutoReconnect` exceptions on server
|
||||
hiccups, etc. We only check if the socket was closed by an external
|
||||
error if it has been > 1 second since the socket was checked into the
|
||||
pool to keep performance reasonable -
|
||||
we can't avoid AutoReconnects completely anyway.
|
||||
pool, to keep performance reasonable - we can't avoid AutoReconnects
|
||||
completely anyway.
|
||||
"""
|
||||
idle_time_seconds = conn.idle_time_seconds()
|
||||
# If socket is idle, open a new one.
|
||||
@ -1426,9 +1418,8 @@ class Pool:
|
||||
await conn.close_conn(ConnectionClosedReason.IDLE)
|
||||
return True
|
||||
|
||||
check_interval_seconds = self._check_interval_seconds
|
||||
if check_interval_seconds is not None and (
|
||||
check_interval_seconds == 0 or idle_time_seconds > check_interval_seconds
|
||||
if self._check_interval_seconds is not None and (
|
||||
self._check_interval_seconds == 0 or idle_time_seconds > self._check_interval_seconds
|
||||
):
|
||||
if conn.conn_closed():
|
||||
await conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
|
||||
@ -111,7 +111,7 @@ class Topology:
|
||||
self._publish_tp = self._listeners is not None and self._listeners.enabled_for_topology
|
||||
|
||||
# Create events queue if there are publishers.
|
||||
self._events: queue.Queue[Any] | None = None
|
||||
self._events = None
|
||||
self.__events_executor: Any = None
|
||||
|
||||
if self._publish_server or self._publish_tp:
|
||||
@ -126,7 +126,6 @@ class Topology:
|
||||
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put((self._listeners.publish_topology_opened, (self._topology_id,)))
|
||||
self._settings = topology_settings
|
||||
topology_description = TopologyDescription(
|
||||
@ -144,7 +143,6 @@ class Topology:
|
||||
)
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_topology_description_changed,
|
||||
@ -163,7 +161,6 @@ class Topology:
|
||||
for seed in topology_settings.seeds:
|
||||
if self._publish_server:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put((self._listeners.publish_server_opened, (seed, self._topology_id)))
|
||||
if _SDAM_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
@ -268,7 +265,6 @@ class Topology:
|
||||
server_selection_timeout: Optional[float] = None,
|
||||
address: Optional[_Address] = None,
|
||||
operation_id: Optional[int] = None,
|
||||
deprioritized_servers: Optional[list[Server]] = None,
|
||||
) -> list[Server]:
|
||||
"""Return a list of Servers matching selector, or time out.
|
||||
|
||||
@ -296,12 +292,7 @@ class Topology:
|
||||
|
||||
async with self._lock:
|
||||
server_descriptions = await self._select_servers_loop(
|
||||
selector,
|
||||
server_timeout,
|
||||
operation,
|
||||
operation_id,
|
||||
address,
|
||||
deprioritized_servers=deprioritized_servers,
|
||||
selector, server_timeout, operation, operation_id, address
|
||||
)
|
||||
|
||||
return [
|
||||
@ -315,7 +306,6 @@ class Topology:
|
||||
operation: str,
|
||||
operation_id: Optional[int],
|
||||
address: Optional[_Address],
|
||||
deprioritized_servers: Optional[list[Server]] = None,
|
||||
) -> list[ServerDescription]:
|
||||
"""select_servers() guts. Hold the lock when calling this."""
|
||||
now = time.monotonic()
|
||||
@ -334,12 +324,7 @@ class Topology:
|
||||
)
|
||||
|
||||
server_descriptions = self._description.apply_selector(
|
||||
selector,
|
||||
address,
|
||||
custom_selector=self._settings.server_selector,
|
||||
deprioritized_servers=[server.description for server in deprioritized_servers]
|
||||
if deprioritized_servers
|
||||
else None,
|
||||
selector, address, custom_selector=self._settings.server_selector
|
||||
)
|
||||
|
||||
while not server_descriptions:
|
||||
@ -400,13 +385,9 @@ class Topology:
|
||||
operation_id: Optional[int] = None,
|
||||
) -> Server:
|
||||
servers = await self.select_servers(
|
||||
selector,
|
||||
operation,
|
||||
server_selection_timeout,
|
||||
address,
|
||||
operation_id,
|
||||
deprioritized_servers,
|
||||
selector, operation, server_selection_timeout, address, operation_id
|
||||
)
|
||||
servers = _filter_servers(servers, deprioritized_servers)
|
||||
if len(servers) == 1:
|
||||
return servers[0]
|
||||
server1, server2 = random.sample(servers, 2)
|
||||
@ -510,7 +491,6 @@ class Topology:
|
||||
suppress_event = sd_old == server_description
|
||||
if self._publish_server and not suppress_event:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_server_description_changed,
|
||||
@ -523,7 +503,6 @@ class Topology:
|
||||
|
||||
if self._publish_tp and not suppress_event:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_topology_description_changed,
|
||||
@ -591,7 +570,6 @@ class Topology:
|
||||
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_topology_description_changed,
|
||||
@ -745,7 +723,6 @@ class Topology:
|
||||
# Publish only after releasing the lock.
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._description = TopologyDescription(
|
||||
TOPOLOGY_TYPE.Unknown,
|
||||
{},
|
||||
@ -913,9 +890,7 @@ class Topology:
|
||||
# Clear the pool.
|
||||
await server.reset(service_id)
|
||||
elif isinstance(error, ConnectionFailure):
|
||||
if isinstance(error, WaitQueueTimeoutError) or (
|
||||
error.has_error_label("SystemOverloadedError")
|
||||
):
|
||||
if isinstance(error, WaitQueueTimeoutError):
|
||||
return
|
||||
# "Client MUST replace the server's description with type Unknown
|
||||
# ... MUST NOT request an immediate check of the server."
|
||||
@ -1137,3 +1112,16 @@ def _is_stale_server_description(current_sd: ServerDescription, new_sd: ServerDe
|
||||
if current_tv["processId"] != new_tv["processId"]:
|
||||
return False
|
||||
return current_tv["counter"] > new_tv["counter"]
|
||||
|
||||
|
||||
def _filter_servers(
|
||||
candidates: list[Server], deprioritized_servers: Optional[list[Server]] = None
|
||||
) -> list[Server]:
|
||||
"""Filter out deprioritized servers from a list of server candidates."""
|
||||
if not deprioritized_servers:
|
||||
return candidates
|
||||
|
||||
filtered = [server for server in candidates if server not in deprioritized_servers]
|
||||
|
||||
# If not possible to pick a prioritized server, return the original list
|
||||
return filtered or candidates
|
||||
|
||||
@ -159,7 +159,6 @@ def _build_credentials_tuple(
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"::1",
|
||||
"*.mongo.com",
|
||||
]
|
||||
allowed_hosts = properties.get("ALLOWED_HOSTS", default_allowed)
|
||||
if properties.get("ALLOWED_HOSTS", None) is not None and human_callback is None:
|
||||
|
||||
@ -235,16 +235,6 @@ class ClientOptions:
|
||||
self.__server_monitoring_mode = options.get(
|
||||
"servermonitoringmode", common.SERVER_MONITORING_MODE
|
||||
)
|
||||
self.__max_adaptive_retries = (
|
||||
options.get("max_adaptive_retries", common.MAX_ADAPTIVE_RETRIES)
|
||||
if "max_adaptive_retries" in options
|
||||
else options.get("maxadaptiveretries", common.MAX_ADAPTIVE_RETRIES)
|
||||
)
|
||||
self.__enable_overload_retargeting = (
|
||||
options.get("enable_overload_retargeting", common.ENABLE_OVERLOAD_RETARGETING)
|
||||
if "enable_overload_retargeting" in options
|
||||
else options.get("enableoverloadretargeting", common.ENABLE_OVERLOAD_RETARGETING)
|
||||
)
|
||||
|
||||
@property
|
||||
def _options(self) -> Mapping[str, Any]:
|
||||
@ -356,19 +346,3 @@ class ClientOptions:
|
||||
.. versionadded:: 4.5
|
||||
"""
|
||||
return self.__server_monitoring_mode
|
||||
|
||||
@property
|
||||
def max_adaptive_retries(self) -> int:
|
||||
"""The configured maxAdaptiveRetries option.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return self.__max_adaptive_retries
|
||||
|
||||
@property
|
||||
def enable_overload_retargeting(self) -> bool:
|
||||
"""The configured enableOverloadRetargeting option.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return self.__enable_overload_retargeting
|
||||
|
||||
@ -140,12 +140,6 @@ SRV_SERVICE_NAME = "mongodb"
|
||||
# Default value for serverMonitoringMode
|
||||
SERVER_MONITORING_MODE = "auto" # poll/stream/auto
|
||||
|
||||
# Default value for max adaptive retries
|
||||
MAX_ADAPTIVE_RETRIES = 2
|
||||
|
||||
# Default value for enableOverloadRetargeting
|
||||
ENABLE_OVERLOAD_RETARGETING = False
|
||||
|
||||
# Auth mechanism properties that must raise an error instead of warning if they invalidate.
|
||||
_MECH_PROP_MUST_RAISE = ["CANONICALIZE_HOST_NAME"]
|
||||
|
||||
@ -239,6 +233,13 @@ def validate_readable(option: str, value: Any) -> Optional[str]:
|
||||
return value
|
||||
|
||||
|
||||
def validate_positive_integer_or_none(option: str, value: Any) -> Optional[int]:
|
||||
"""Validate that 'value' is a positive integer or None."""
|
||||
if value is None:
|
||||
return value
|
||||
return validate_positive_integer(option, value)
|
||||
|
||||
|
||||
def validate_non_negative_integer_or_none(option: str, value: Any) -> Optional[int]:
|
||||
"""Validate that 'value' is a positive integer or 0 or None."""
|
||||
if value is None:
|
||||
@ -260,6 +261,20 @@ def validate_string_or_none(option: str, value: Any) -> Optional[str]:
|
||||
return validate_string(option, value)
|
||||
|
||||
|
||||
def validate_int_or_basestring(option: str, value: Any) -> Union[int, str]:
|
||||
"""Validates that 'value' is an integer or string."""
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
raise TypeError(
|
||||
f"Wrong type for {option}, value must be an integer or a string, not {type(value)}"
|
||||
)
|
||||
|
||||
|
||||
def validate_non_negative_int_or_basestring(option: Any, value: Any) -> Union[int, str]:
|
||||
"""Validates that 'value' is an integer or string."""
|
||||
if isinstance(value, int):
|
||||
@ -723,8 +738,6 @@ URI_OPTIONS_VALIDATOR_MAP: dict[str, Callable[[Any, Any], Any]] = {
|
||||
"srvmaxhosts": validate_non_negative_integer,
|
||||
"timeoutms": validate_timeoutms,
|
||||
"servermonitoringmode": validate_server_monitoring_mode,
|
||||
"maxadaptiveretries": validate_non_negative_integer,
|
||||
"enableoverloadretargeting": validate_boolean_or_string,
|
||||
}
|
||||
|
||||
# Dictionary where keys are the names of URI options specific to pymongo,
|
||||
@ -758,8 +771,6 @@ KW_VALIDATORS: dict[str, Callable[[Any, Any], Any]] = {
|
||||
"server_selector": validate_is_callable_or_none,
|
||||
"auto_encryption_opts": validate_auto_encryption_opts_or_none,
|
||||
"authoidcallowedhosts": validate_list,
|
||||
"max_adaptive_retries": validate_non_negative_integer,
|
||||
"enable_overload_retargeting": validate_boolean_or_string,
|
||||
}
|
||||
|
||||
# Dictionary where keys are any URI option name, and values are the
|
||||
@ -806,6 +817,16 @@ TIMEOUT_OPTIONS: list[str] = [
|
||||
"waitqueuetimeoutms",
|
||||
]
|
||||
|
||||
_AUTH_OPTIONS = frozenset(["authmechanismproperties"])
|
||||
|
||||
|
||||
def validate_auth_option(option: str, value: Any) -> tuple[str, Any]:
|
||||
"""Validate optional authentication parameters."""
|
||||
lower, value = validate(option, value)
|
||||
if lower not in _AUTH_OPTIONS:
|
||||
raise ConfigurationError(f"Unknown option: {option}. Must be in {_AUTH_OPTIONS}")
|
||||
return option, value
|
||||
|
||||
|
||||
def _get_validator(
|
||||
key: str, validators: dict[str, Callable[[Any, Any], Any]], normed_key: Optional[str] = None
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Any, Iterable, Optional, Union
|
||||
|
||||
@ -45,10 +44,7 @@ def _have_zlib() -> bool:
|
||||
|
||||
def _have_zstd() -> bool:
|
||||
try:
|
||||
if sys.version_info >= (3, 14):
|
||||
from compression import zstd
|
||||
else:
|
||||
from backports import zstd # noqa: F401
|
||||
import zstandard # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
@ -83,18 +79,11 @@ def validate_compressors(dummy: Any, value: Union[str, Iterable[str]]) -> list[s
|
||||
)
|
||||
elif compressor == "zstd" and not _have_zstd():
|
||||
compressors.remove(compressor)
|
||||
if sys.version_info >= (3, 14):
|
||||
warnings.warn(
|
||||
"Wire protocol compression with zstandard is not available. "
|
||||
"The compression.zstd module is not available.",
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
warnings.warn(
|
||||
"Wire protocol compression with zstandard is not available. "
|
||||
"You must install the backports.zstd module for zstandard support.",
|
||||
stacklevel=2,
|
||||
)
|
||||
warnings.warn(
|
||||
"Wire protocol compression with zstandard is not available. "
|
||||
"You must install the zstandard module for zstandard support.",
|
||||
stacklevel=2,
|
||||
)
|
||||
return compressors
|
||||
|
||||
|
||||
@ -155,12 +144,12 @@ class ZstdContext:
|
||||
|
||||
@staticmethod
|
||||
def compress(data: bytes) -> bytes:
|
||||
if sys.version_info >= (3, 14):
|
||||
from compression import zstd
|
||||
else:
|
||||
from backports import zstd
|
||||
# ZstdCompressor is not thread safe.
|
||||
# TODO: Use a pool?
|
||||
|
||||
return zstd.compress(data)
|
||||
import zstandard
|
||||
|
||||
return zstandard.ZstdCompressor().compress(data)
|
||||
|
||||
|
||||
def decompress(data: bytes | memoryview, compressor_id: int) -> bytes:
|
||||
@ -177,11 +166,10 @@ def decompress(data: bytes | memoryview, compressor_id: int) -> bytes:
|
||||
|
||||
return zlib.decompress(data)
|
||||
elif compressor_id == ZstdContext.compressor_id:
|
||||
if sys.version_info >= (3, 14):
|
||||
from compression import zstd
|
||||
else:
|
||||
from backports import zstd
|
||||
# ZstdDecompressor is not thread safe.
|
||||
# TODO: Use a pool?
|
||||
import zstandard
|
||||
|
||||
return zstd.decompress(data)
|
||||
return zstandard.ZstdDecompressor().decompress(data)
|
||||
else:
|
||||
raise ValueError("Unknown compressorId %d" % (compressor_id,))
|
||||
|
||||
@ -16,104 +16,7 @@
|
||||
"""Constants and types shared across all cursor classes."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, Mapping, Optional, Sequence, Tuple, Union
|
||||
|
||||
from pymongo.message import _CursorAddress
|
||||
from pymongo.typings import _Address, _DocumentType
|
||||
|
||||
|
||||
class _AgnosticCursorBase(Generic[_DocumentType], ABC):
|
||||
"""
|
||||
Shared IO-agnostic cursor base used by both async and sync cursor classes.
|
||||
All IO-specific behavior is implemented in subclasses.
|
||||
"""
|
||||
|
||||
# These are all typed more accurately in subclasses.
|
||||
_collection: Any
|
||||
_id: Optional[int]
|
||||
_data: Any
|
||||
_address: Optional[_Address]
|
||||
_sock_mgr: Any
|
||||
_session: Optional[Any]
|
||||
_killed: bool
|
||||
|
||||
@abstractmethod
|
||||
def _get_namespace(self) -> str:
|
||||
"""Return the full namespace (dbname.collname) for this cursor."""
|
||||
...
|
||||
|
||||
def __del__(self) -> None:
|
||||
self._die_no_lock()
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
"""Does this cursor have the potential to return more data?
|
||||
|
||||
This is mostly useful with `tailable cursors
|
||||
<https://www.mongodb.com/docs/manual/core/tailable-cursors/>`_
|
||||
since they will stop iterating even though they *may* return more
|
||||
results in the future.
|
||||
|
||||
With regular cursors, simply use an asynchronous for loop instead of :attr:`alive`::
|
||||
|
||||
async for doc in collection.find():
|
||||
print(doc)
|
||||
|
||||
.. note:: Even if :attr:`alive` is True, :meth:`next` can raise
|
||||
:exc:`StopIteration`. :attr:`alive` can also be True while iterating
|
||||
a cursor from a failed server. In this case :attr:`alive` will
|
||||
return False after :meth:`next` fails to retrieve the next batch
|
||||
of results from the server.
|
||||
"""
|
||||
return bool(len(self._data) or (not self._killed))
|
||||
|
||||
@property
|
||||
def cursor_id(self) -> Optional[int]:
|
||||
"""Returns the id of the cursor.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
"""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def address(self) -> Optional[_Address]:
|
||||
"""The (host, port) of the server used, or None.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Renamed from "conn_id".
|
||||
"""
|
||||
return self._address
|
||||
|
||||
def _prepare_to_die(self, already_killed: bool) -> tuple[int, Optional[_CursorAddress]]:
|
||||
self._killed = True
|
||||
if self._id and not already_killed:
|
||||
cursor_id = self._id
|
||||
assert self._address is not None
|
||||
address = _CursorAddress(self._address, self._get_namespace())
|
||||
else:
|
||||
# Skip killCursors.
|
||||
cursor_id = 0
|
||||
address = None
|
||||
return cursor_id, address
|
||||
|
||||
def _die_no_lock(self) -> None:
|
||||
"""Closes this cursor without acquiring a lock."""
|
||||
try:
|
||||
already_killed = self._killed
|
||||
except AttributeError:
|
||||
# ___init__ did not run to completion (or at all).
|
||||
return
|
||||
|
||||
cursor_id, address = self._prepare_to_die(already_killed)
|
||||
self._collection.database.client._cleanup_cursor_no_lock(
|
||||
cursor_id, address, self._sock_mgr, self._session
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
from typing import Any, Mapping, Sequence, Tuple, Union
|
||||
|
||||
# These errors mean that the server has already killed the cursor so there is
|
||||
# no need to send killCursors.
|
||||
|
||||
@ -1298,6 +1298,8 @@ def _batched_write_command_impl(
|
||||
|
||||
# Start of payload
|
||||
buf.seek(-1, 2)
|
||||
# Work around some Jython weirdness.
|
||||
buf.truncate()
|
||||
try:
|
||||
buf.write(_OP_MAP[operation])
|
||||
except KeyError:
|
||||
|
||||
@ -45,6 +45,7 @@ from cryptography.x509 import ExtendedKeyUsage as _ExtendedKeyUsage
|
||||
from cryptography.x509 import ExtensionNotFound as _ExtensionNotFound
|
||||
from cryptography.x509 import TLSFeature as _TLSFeature
|
||||
from cryptography.x509 import TLSFeatureType as _TLSFeatureType
|
||||
from cryptography.x509 import load_pem_x509_certificate as _load_pem_x509_certificate
|
||||
from cryptography.x509.ocsp import OCSPCertStatus as _OCSPCertStatus
|
||||
from cryptography.x509.ocsp import OCSPRequestBuilder as _OCSPRequestBuilder
|
||||
from cryptography.x509.ocsp import OCSPResponseStatus as _OCSPResponseStatus
|
||||
@ -101,6 +102,19 @@ _CERT_REGEX = _re.compile(
|
||||
)
|
||||
|
||||
|
||||
def _load_trusted_ca_certs(cafile: str) -> list[Certificate]:
|
||||
"""Parse the tlsCAFile into a list of certificates."""
|
||||
with open(cafile, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
# Load all the certs in the file.
|
||||
trusted_ca_certs = []
|
||||
backend = _default_backend()
|
||||
for cert_data in _re.findall(_CERT_REGEX, data):
|
||||
trusted_ca_certs.append(_load_pem_x509_certificate(cert_data, backend))
|
||||
return trusted_ca_certs
|
||||
|
||||
|
||||
def _get_issuer_cert(
|
||||
cert: Certificate, chain: Iterable[Certificate], trusted_ca_certs: Optional[list[Certificate]]
|
||||
) -> Optional[Certificate]:
|
||||
|
||||
@ -79,6 +79,17 @@ elif sys.platform == "win32":
|
||||
# Windows patch level (e.g. 10.0.17763-SP0).
|
||||
"version": ".".join(map(str, _ver[:3])) + f"-SP{_ver[-1] or '0'}",
|
||||
}
|
||||
elif sys.platform.startswith("java"):
|
||||
_name, _ver, _arch = platform.java_ver()[-1]
|
||||
_METADATA["os"] = {
|
||||
# Linux, Windows 7, Mac OS X, etc.
|
||||
"type": _name,
|
||||
"name": _name,
|
||||
# x86, x86_64, AMD64, etc.
|
||||
"architecture": _arch,
|
||||
# Linux kernel version, OSX version, etc.
|
||||
"version": _ver,
|
||||
}
|
||||
else:
|
||||
# Get potential alias (e.g. SunOS 5.11 becomes Solaris 2.11)
|
||||
_aliased = platform.system_alias(platform.system(), platform.release(), platform.version())
|
||||
@ -97,6 +108,14 @@ if platform.python_implementation().startswith("PyPy"):
|
||||
"(Python %s)" % ".".join(map(str, sys.version_info)),
|
||||
)
|
||||
)
|
||||
elif sys.platform.startswith("java"):
|
||||
_METADATA["platform"] = " ".join(
|
||||
(
|
||||
platform.python_implementation(),
|
||||
".".join(map(str, sys.version_info)),
|
||||
"(%s)" % " ".join((platform.system(), platform.release())),
|
||||
)
|
||||
)
|
||||
else:
|
||||
_METADATA["platform"] = " ".join(
|
||||
(platform.python_implementation(), ".".join(map(str, sys.version_info)))
|
||||
|
||||
@ -237,7 +237,8 @@ async def _async_create_connection(address: _Address, options: PoolOptions) -> s
|
||||
else:
|
||||
# This likely means we tried to connect to an IPv6 only
|
||||
# host with an OS/kernel or Python interpreter that doesn't
|
||||
# support IPv6.
|
||||
# support IPv6. The test case is Jython2.5.1 which doesn't
|
||||
# support IPv6 at all.
|
||||
raise OSError("getaddrinfo failed")
|
||||
|
||||
|
||||
@ -417,7 +418,8 @@ def _create_connection(address: _Address, options: PoolOptions) -> socket.socket
|
||||
else:
|
||||
# This likely means we tried to connect to an IPv6 only
|
||||
# host with an OS/kernel or Python interpreter that doesn't
|
||||
# support IPv6.
|
||||
# support IPv6. The test case is Jython2.5.1 which doesn't
|
||||
# support IPv6 at all.
|
||||
raise OSError("getaddrinfo failed")
|
||||
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ from OpenSSL import crypto as _crypto
|
||||
from pymongo.errors import ConfigurationError as _ConfigurationError
|
||||
from pymongo.errors import _CertificateError # type:ignore[attr-defined]
|
||||
from pymongo.ocsp_cache import _OCSPCache
|
||||
from pymongo.ocsp_support import _ocsp_callback
|
||||
from pymongo.ocsp_support import _load_trusted_ca_certs, _ocsp_callback
|
||||
from pymongo.socket_checker import SocketChecker as _SocketChecker
|
||||
from pymongo.socket_checker import _errno_from_exception
|
||||
from pymongo.write_concern import validate_boolean
|
||||
@ -322,6 +322,10 @@ class SSLContext:
|
||||
ssl.CERT_NONE.
|
||||
"""
|
||||
self._ctx.load_verify_locations(cafile, capath)
|
||||
# Manually load the CA certs when get_verified_chain is not available (pyopenssl<20).
|
||||
if not hasattr(_SSL.Connection, "get_verified_chain"):
|
||||
assert cafile is not None
|
||||
self._callback_data.trusted_ca_certs = _load_trusted_ca_certs(cafile)
|
||||
|
||||
def _load_certifi(self) -> None:
|
||||
"""Attempt to load CA certs from certifi."""
|
||||
@ -357,7 +361,7 @@ class SSLContext:
|
||||
try:
|
||||
for storename in ("CA", "ROOT"):
|
||||
self._load_wincerts(storename)
|
||||
except Exception:
|
||||
except PermissionError:
|
||||
# Fall back to certifi
|
||||
self._load_certifi()
|
||||
elif _sys.platform == "darwin":
|
||||
|
||||
@ -34,16 +34,16 @@ class Selection:
|
||||
|
||||
@classmethod
|
||||
def from_topology_description(cls, topology_description: TopologyDescription) -> Selection:
|
||||
candidate_servers = topology_description.candidate_servers
|
||||
known_servers = topology_description.known_servers
|
||||
primary = None
|
||||
for sd in candidate_servers:
|
||||
for sd in known_servers:
|
||||
if sd.server_type == SERVER_TYPE.RSPrimary:
|
||||
primary = sd
|
||||
break
|
||||
|
||||
return Selection(
|
||||
topology_description,
|
||||
topology_description.candidate_servers,
|
||||
topology_description.known_servers,
|
||||
topology_description.common_wire_version,
|
||||
primary,
|
||||
)
|
||||
|
||||
@ -17,9 +17,12 @@ from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import select
|
||||
import sys
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
_HAVE_POLL = hasattr(select, "poll")
|
||||
# PYTHON-2320: Jython does not fully support poll on SSL sockets,
|
||||
# https://bugs.jython.org/issue2900
|
||||
_HAVE_POLL = hasattr(select, "poll") and not sys.platform.startswith("java")
|
||||
_SelectError = getattr(select, "error", OSError)
|
||||
|
||||
|
||||
|
||||
@ -59,7 +59,6 @@ from pymongo.errors import (
|
||||
InvalidOperation,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
)
|
||||
from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES
|
||||
@ -562,17 +561,9 @@ class _ClientBulk:
|
||||
error, ConnectionFailure
|
||||
) and not isinstance(error, (NotPrimaryError, WaitQueueTimeoutError))
|
||||
|
||||
retryable_label_error = isinstance(
|
||||
error, PyMongoError
|
||||
) and error.has_error_label("RetryableError")
|
||||
|
||||
# Synthesize the full bulk result without modifying the
|
||||
# current one because this write operation may be retried.
|
||||
if retryable and (
|
||||
retryable_top_level_error
|
||||
or retryable_network_error
|
||||
or retryable_label_error
|
||||
):
|
||||
if retryable and (retryable_top_level_error or retryable_network_error):
|
||||
full = copy.deepcopy(full_result)
|
||||
_merge_command(self.ops, self.idx_offset, full, result)
|
||||
_throw_client_bulk_write_exception(full, self.verbose_results)
|
||||
|
||||
@ -136,11 +136,9 @@ Classes
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Mapping as _Mapping
|
||||
from contextvars import ContextVar, Token
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@ -161,9 +159,7 @@ from pymongo import _csot
|
||||
from pymongo.errors import (
|
||||
ConfigurationError,
|
||||
ConnectionFailure,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WTimeoutError,
|
||||
@ -172,7 +168,7 @@ from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES
|
||||
from pymongo.read_concern import ReadConcern
|
||||
from pymongo.read_preferences import ReadPreference, _ServerMode
|
||||
from pymongo.server_type import SERVER_TYPE
|
||||
from pymongo.synchronous.cursor_base import _ConnectionManager
|
||||
from pymongo.synchronous.cursor import _ConnectionManager
|
||||
from pymongo.write_concern import WriteConcern
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -184,28 +180,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
_SESSION: ContextVar[Optional[ClientSession]] = ContextVar("SESSION", default=None)
|
||||
|
||||
|
||||
class _BoundSessionContext:
|
||||
"""Context manager returned by ClientSession.bind() that manages bound state."""
|
||||
|
||||
def __init__(self, session: ClientSession, end_session: bool) -> None:
|
||||
self._session = session
|
||||
self._session_token: Optional[Token[ClientSession]] = None
|
||||
self._end_session = end_session
|
||||
|
||||
def __enter__(self) -> ClientSession:
|
||||
self._session_token = _SESSION.set(self._session) # type: ignore[assignment]
|
||||
return self._session
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
if self._session_token:
|
||||
_SESSION.reset(self._session_token) # type: ignore[arg-type]
|
||||
self._session_token = None
|
||||
if self._end_session:
|
||||
self._session.end_session()
|
||||
|
||||
|
||||
class SessionOptions:
|
||||
"""Options for a new :class:`ClientSession`.
|
||||
@ -429,7 +403,6 @@ class _Transaction:
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.client = client
|
||||
self.has_completed_command = False
|
||||
|
||||
def active(self) -> bool:
|
||||
return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS)
|
||||
@ -437,9 +410,6 @@ class _Transaction:
|
||||
def starting(self) -> bool:
|
||||
return self.state == _TxnState.STARTING
|
||||
|
||||
def set_starting(self) -> None:
|
||||
self.state = _TxnState.STARTING
|
||||
|
||||
@property
|
||||
def pinned_conn(self) -> Optional[Connection]:
|
||||
if self.active() and self.conn_mgr:
|
||||
@ -465,7 +435,6 @@ class _Transaction:
|
||||
self.sharded = False
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.has_completed_command = False
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.conn_mgr:
|
||||
@ -500,29 +469,11 @@ _UNKNOWN_COMMIT_ERROR_CODES: frozenset = _RETRYABLE_ERROR_CODES | frozenset( #
|
||||
# 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
|
||||
_BACKOFF_MAX = 0.500 # 500ms max backoff
|
||||
_BACKOFF_INITIAL = 0.005 # 5ms initial backoff
|
||||
|
||||
|
||||
def _within_time_limit(start_time: float, backoff: float = 0) -> bool:
|
||||
def _within_time_limit(start_time: float) -> bool:
|
||||
"""Are we within the with_transaction retry limit?"""
|
||||
remaining = _csot.remaining()
|
||||
if remaining is not None and remaining <= 0:
|
||||
return False
|
||||
return time.monotonic() + backoff - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
|
||||
|
||||
def _make_timeout_error(error: BaseException) -> PyMongoError:
|
||||
"""Convert error to a NetworkTimeout or ExecutionTimeout as appropriate."""
|
||||
if _csot.remaining() is not None:
|
||||
timeout_error: PyMongoError = ExecutionTimeout(
|
||||
str(error), 50, {"ok": 0, "errmsg": str(error), "code": 50}
|
||||
)
|
||||
else:
|
||||
timeout_error = NetworkTimeout(str(error))
|
||||
if isinstance(error, PyMongoError):
|
||||
timeout_error._error_labels = error._error_labels.copy()
|
||||
return timeout_error
|
||||
return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
@ -595,24 +546,6 @@ class ClientSession:
|
||||
if self._server_session is None:
|
||||
raise InvalidOperation("Cannot use ended session")
|
||||
|
||||
def bind(self, end_session: bool = True) -> _BoundSessionContext:
|
||||
"""Bind this session so it is implicitly passed to all database operations within the returned context.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with client.start_session() as s:
|
||||
with s.bind():
|
||||
# session=s is passed implicitly
|
||||
client.db.collection.insert_one({"x": 1})
|
||||
|
||||
:param end_session: Whether to end the session on exiting the returned context. Defaults to True.
|
||||
If set to False, :meth:`~pymongo.client_session.ClientSession.end_session()` must be called
|
||||
once the session is no longer used.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return _BoundSessionContext(self, end_session)
|
||||
|
||||
def __enter__(self) -> ClientSession:
|
||||
return self
|
||||
|
||||
@ -769,32 +702,21 @@ class ClientSession:
|
||||
https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback
|
||||
"""
|
||||
start_time = time.monotonic()
|
||||
retry = 0
|
||||
last_error: Optional[BaseException] = None
|
||||
while True:
|
||||
if retry: # Implement exponential backoff on retry.
|
||||
jitter = random.random() # noqa: S311
|
||||
backoff = jitter * min(_BACKOFF_INITIAL * (1.5**retry), _BACKOFF_MAX)
|
||||
if not _within_time_limit(start_time, backoff):
|
||||
assert last_error is not None
|
||||
raise _make_timeout_error(last_error) from last_error
|
||||
time.sleep(backoff)
|
||||
retry += 1
|
||||
self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms)
|
||||
try:
|
||||
ret = callback(self)
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException as exc:
|
||||
last_error = exc
|
||||
if self.in_transaction:
|
||||
self.abort_transaction()
|
||||
if isinstance(exc, PyMongoError) and exc.has_error_label(
|
||||
"TransientTransactionError"
|
||||
if (
|
||||
isinstance(exc, PyMongoError)
|
||||
and exc.has_error_label("TransientTransactionError")
|
||||
and _within_time_limit(start_time)
|
||||
):
|
||||
if _within_time_limit(start_time):
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
raise
|
||||
|
||||
if not self.in_transaction:
|
||||
@ -805,18 +727,17 @@ class ClientSession:
|
||||
try:
|
||||
self.commit_transaction()
|
||||
except PyMongoError as exc:
|
||||
last_error = exc
|
||||
if exc.has_error_label(
|
||||
"UnknownTransactionCommitResult"
|
||||
) and not _max_time_expired_error(exc):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from 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"):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
if exc.has_error_label("TransientTransactionError") and _within_time_limit(
|
||||
start_time
|
||||
):
|
||||
# Retry the entire transaction.
|
||||
break
|
||||
raise
|
||||
@ -1097,11 +1018,7 @@ class ClientSession:
|
||||
read_preference: _ServerMode,
|
||||
conn: Connection,
|
||||
) -> None:
|
||||
# getMores must be sent with a session if the cursor was opened with one
|
||||
operation = next(iter(command))
|
||||
if not conn.supports_sessions and (
|
||||
isinstance(self._server_session, _EmptyServerSession) or operation != "getMore"
|
||||
):
|
||||
if not conn.supports_sessions:
|
||||
if not self._implicit:
|
||||
raise ConfigurationError("Sessions are not supported by this MongoDB deployment")
|
||||
return
|
||||
|
||||
@ -21,6 +21,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
@ -571,6 +572,11 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
change_stream._initialize_cursor()
|
||||
return change_stream
|
||||
|
||||
def _conn_for_writes(
|
||||
self, session: Optional[ClientSession], operation: str
|
||||
) -> ContextManager[Connection]:
|
||||
return self._database.client._conn_for_writes(session, operation)
|
||||
|
||||
def _command(
|
||||
self,
|
||||
conn: Connection,
|
||||
@ -647,10 +653,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
if "size" in options:
|
||||
options["size"] = float(options["size"])
|
||||
cmd.update(options)
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
with self._conn_for_writes(session, operation=_Op.CREATE) as conn:
|
||||
if qev2_required and conn.max_wire_version < 21:
|
||||
raise ConfigurationError(
|
||||
"Driver support of Queryable Encryption is incompatible with server. "
|
||||
@ -667,8 +670,6 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.CREATE)
|
||||
|
||||
def _create(
|
||||
self,
|
||||
options: MutableMapping[str, Any],
|
||||
@ -2236,10 +2237,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
command (like maxTimeMS) can be passed as keyword arguments.
|
||||
"""
|
||||
names = []
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
with self._conn_for_writes(session, operation=_Op.CREATE_INDEXES) as conn:
|
||||
supports_quorum = conn.max_wire_version >= 9
|
||||
|
||||
def gen_indexes() -> Iterator[Mapping[str, Any]]:
|
||||
@ -2268,9 +2266,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
write_concern=self._write_concern_for(session),
|
||||
session=session,
|
||||
)
|
||||
return names
|
||||
|
||||
return self.database.client._retryable_write(False, inner, session, _Op.CREATE_INDEXES)
|
||||
return names
|
||||
|
||||
def create_index(
|
||||
self,
|
||||
@ -2423,6 +2419,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
kwargs["comment"] = comment
|
||||
self._drop_index("*", session=session, **kwargs)
|
||||
|
||||
@_csot.apply
|
||||
def drop_index(
|
||||
self,
|
||||
index_or_name: _IndexKeyHint,
|
||||
@ -2490,10 +2487,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
with self._conn_for_writes(session, operation=_Op.DROP_INDEXES) as conn:
|
||||
self._command(
|
||||
conn,
|
||||
cmd,
|
||||
@ -2503,8 +2497,6 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.DROP_INDEXES)
|
||||
|
||||
def list_indexes(
|
||||
self,
|
||||
session: Optional[ClientSession] = None,
|
||||
@ -2768,22 +2760,15 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd = {"createSearchIndexes": self.name, "indexes": list(gen_indexes())}
|
||||
cmd.update(kwargs)
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
with self._conn_for_writes(session, operation=_Op.CREATE_SEARCH_INDEXES) as conn:
|
||||
resp = self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
return [index["name"] for index in resp["indexesCreated"]]
|
||||
|
||||
return self.database.client._retryable_write(
|
||||
False, inner, session, _Op.CREATE_SEARCH_INDEXES
|
||||
)
|
||||
|
||||
def drop_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2809,21 +2794,15 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
with self._conn_for_writes(session, operation=_Op.DROP_SEARCH_INDEXES) as conn:
|
||||
self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.DROP_SEARCH_INDEXES)
|
||||
|
||||
def update_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2851,21 +2830,15 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
with self._conn_for_writes(session, operation=_Op.UPDATE_SEARCH_INDEX) as conn:
|
||||
self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.UPDATE_SEARCH_INDEX)
|
||||
|
||||
def options(
|
||||
self,
|
||||
session: Optional[ClientSession] = None,
|
||||
@ -2938,7 +2911,6 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
session,
|
||||
retryable=not cmd._performs_write,
|
||||
operation=_Op.AGGREGATE,
|
||||
is_aggregate_write=cmd._performs_write,
|
||||
)
|
||||
|
||||
def aggregate(
|
||||
@ -3144,21 +3116,17 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
write_concern = self._write_concern_for_cmd(cmd, session)
|
||||
client = self._database.client
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> MutableMapping[str, Any]:
|
||||
return conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
client=client,
|
||||
)
|
||||
|
||||
return client._retryable_write(False, inner, session, _Op.RENAME)
|
||||
with self._conn_for_writes(session, operation=_Op.RENAME) as conn:
|
||||
with self._database.client._tmp_session(session) as s:
|
||||
return conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=s,
|
||||
client=self._database.client,
|
||||
)
|
||||
|
||||
def distinct(
|
||||
self,
|
||||
@ -3335,7 +3303,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
let: Optional[Mapping[str, Any]] = None,
|
||||
comment: Optional[Any] = None,
|
||||
**kwargs: Any,
|
||||
) -> Optional[_DocumentType]:
|
||||
) -> _DocumentType:
|
||||
"""Finds a single document and deletes it, returning the document.
|
||||
|
||||
>>> db.test.count_documents({'x': 1})
|
||||
@ -3345,10 +3313,6 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
>>> db.test.count_documents({'x': 1})
|
||||
1
|
||||
|
||||
Returns ``None`` if no document matches the filter.
|
||||
|
||||
>>> db.test.find_one_and_delete({'_exists': False})
|
||||
|
||||
If multiple documents match *filter*, a *sort* can be applied.
|
||||
|
||||
>>> for doc in db.test.find({'x': 1}):
|
||||
@ -3431,22 +3395,10 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
let: Optional[Mapping[str, Any]] = None,
|
||||
comment: Optional[Any] = None,
|
||||
**kwargs: Any,
|
||||
) -> Optional[_DocumentType]:
|
||||
) -> _DocumentType:
|
||||
"""Finds a single document and replaces it, returning either the
|
||||
original or the replaced document.
|
||||
|
||||
>>> db.test.find_one({'x': 1})
|
||||
{'_id': 0, 'x': 1}
|
||||
>>> db.test.find_one_and_replace({'x': 1}, {'y': 2})
|
||||
{'_id': 0, 'x': 1}
|
||||
>>> db.test.find_one({'x': 1})
|
||||
>>> db.test.find_one({'y': 2})
|
||||
{'_id': 0, 'y': 2}
|
||||
|
||||
Returns ``None`` if no document matches the filter.
|
||||
|
||||
>>> db.test.find_one_and_replace({'_exists': False}, {'x': 1})
|
||||
|
||||
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.
|
||||
@ -3551,17 +3503,13 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
let: Optional[Mapping[str, Any]] = None,
|
||||
comment: Optional[Any] = None,
|
||||
**kwargs: Any,
|
||||
) -> Optional[_DocumentType]:
|
||||
) -> _DocumentType:
|
||||
"""Finds a single document and updates it, returning either the
|
||||
original or the updated document.
|
||||
|
||||
>>> db.test.find_one({'_id': 665})
|
||||
{'_id': 665, 'done': False, 'count': 25}
|
||||
>>> db.test.find_one_and_update(
|
||||
... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})
|
||||
{'_id': 665, 'done': False, 'count': 25}
|
||||
>>> db.test.find_one({'_id': 665})
|
||||
{'_id': 665, 'done': True, 'count': 26}
|
||||
{'_id': 665, 'done': False, 'count': 25}}
|
||||
|
||||
Returns ``None`` if no document matches the filter.
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ from collections import deque
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Generic,
|
||||
Iterator,
|
||||
Mapping,
|
||||
NoReturn,
|
||||
@ -28,11 +29,18 @@ from typing import (
|
||||
)
|
||||
|
||||
from bson import CodecOptions, _convert_raw_document_lists_to_streams
|
||||
from pymongo import _csot
|
||||
from pymongo.cursor_shared import _CURSOR_CLOSED_ERRORS
|
||||
from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure
|
||||
from pymongo.message import _GetMore, _OpMsg, _OpReply, _RawBatchGetMore
|
||||
from pymongo.message import (
|
||||
_CursorAddress,
|
||||
_GetMore,
|
||||
_OpMsg,
|
||||
_OpReply,
|
||||
_RawBatchGetMore,
|
||||
)
|
||||
from pymongo.response import PinnedResponse
|
||||
from pymongo.synchronous.cursor_base import _ConnectionManager, _CursorBase
|
||||
from pymongo.synchronous.cursor import _ConnectionManager
|
||||
from pymongo.typings import _Address, _DocumentOut, _DocumentType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -43,7 +51,7 @@ if TYPE_CHECKING:
|
||||
_IS_SYNC = True
|
||||
|
||||
|
||||
class CommandCursor(_CursorBase[_DocumentType]):
|
||||
class CommandCursor(Generic[_DocumentType]):
|
||||
"""A cursor / iterator over command cursors."""
|
||||
|
||||
_getmore_class = _GetMore
|
||||
@ -90,8 +98,8 @@ class CommandCursor(_CursorBase[_DocumentType]):
|
||||
f"max_await_time_ms must be an integer or None, not {type(max_await_time_ms)}"
|
||||
)
|
||||
|
||||
def _get_namespace(self) -> str:
|
||||
return self._ns
|
||||
def __del__(self) -> None:
|
||||
self._die_no_lock()
|
||||
|
||||
def batch_size(self, batch_size: int) -> CommandCursor[_DocumentType]:
|
||||
"""Limits the number of documents returned in one batch. Each batch
|
||||
@ -153,12 +161,94 @@ class CommandCursor(_CursorBase[_DocumentType]):
|
||||
) -> Sequence[_DocumentOut]:
|
||||
return response.unpack_response(cursor_id, codec_options, user_fields, legacy_response)
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
"""Does this cursor have the potential to return more data?
|
||||
|
||||
Even if :attr:`alive` is ``True``, :meth:`next` can raise
|
||||
:exc:`StopIteration`. Best to use a for loop::
|
||||
|
||||
for doc in collection.aggregate(pipeline):
|
||||
print(doc)
|
||||
|
||||
.. note:: :attr:`alive` can be True while iterating a cursor from
|
||||
a failed server. In this case :attr:`alive` will return False after
|
||||
:meth:`next` fails to retrieve the next batch of results from the
|
||||
server.
|
||||
"""
|
||||
return bool(len(self._data) or (not self._killed))
|
||||
|
||||
@property
|
||||
def cursor_id(self) -> int:
|
||||
"""Returns the id of the cursor."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def address(self) -> Optional[_Address]:
|
||||
"""The (host, port) of the server used, or None.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[ClientSession]:
|
||||
"""The cursor's :class:`~pymongo.client_session.ClientSession`, or None.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
"""
|
||||
if self._session and not self._session._implicit:
|
||||
return self._session
|
||||
return None
|
||||
|
||||
def _prepare_to_die(self) -> tuple[int, Optional[_CursorAddress]]:
|
||||
already_killed = self._killed
|
||||
self._killed = True
|
||||
if self._id and not already_killed:
|
||||
cursor_id = self._id
|
||||
assert self._address is not None
|
||||
address = _CursorAddress(self._address, self._ns)
|
||||
else:
|
||||
# Skip killCursors.
|
||||
cursor_id = 0
|
||||
address = None
|
||||
return cursor_id, address
|
||||
|
||||
def _die_no_lock(self) -> None:
|
||||
"""Closes this cursor without acquiring a lock."""
|
||||
cursor_id, address = self._prepare_to_die()
|
||||
self._collection.database.client._cleanup_cursor_no_lock(
|
||||
cursor_id, address, self._sock_mgr, self._session
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
def _die_lock(self) -> None:
|
||||
"""Closes this cursor."""
|
||||
cursor_id, address = self._prepare_to_die()
|
||||
self._collection.database.client._cleanup_cursor_lock(
|
||||
cursor_id,
|
||||
address,
|
||||
self._sock_mgr,
|
||||
self._session,
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
def _end_session(self) -> None:
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session._end_implicit_session()
|
||||
self._session = None
|
||||
|
||||
def close(self) -> None:
|
||||
"""Explicitly close / kill this cursor."""
|
||||
self._die_lock()
|
||||
|
||||
def _send_message(self, operation: _GetMore) -> None:
|
||||
"""Send a getmore message and handle the response."""
|
||||
client = self._collection.database.client
|
||||
@ -240,9 +330,6 @@ class CommandCursor(_CursorBase[_DocumentType]):
|
||||
def __iter__(self) -> Iterator[_DocumentType]:
|
||||
return self
|
||||
|
||||
def __enter__(self) -> CommandCursor[_DocumentType]:
|
||||
return self
|
||||
|
||||
def next(self) -> _DocumentType:
|
||||
"""Advance the cursor."""
|
||||
# Block until a document is returnable.
|
||||
@ -298,6 +385,41 @@ class CommandCursor(_CursorBase[_DocumentType]):
|
||||
"""
|
||||
return self._try_next(get_more_allowed=True)
|
||||
|
||||
def __enter__(self) -> CommandCursor[_DocumentType]:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
self.close()
|
||||
|
||||
@_csot.apply
|
||||
def to_list(self, length: Optional[int] = None) -> list[_DocumentType]:
|
||||
"""Converts the contents of this cursor to a list more efficiently than ``[doc for doc in cursor]``.
|
||||
|
||||
To use::
|
||||
|
||||
>>> cursor.to_list()
|
||||
|
||||
Or, so read at most n items from the cursor::
|
||||
|
||||
>>> cursor.to_list(n)
|
||||
|
||||
If the cursor is empty or has no more results, an empty list will be returned.
|
||||
|
||||
.. versionadded:: 4.9
|
||||
"""
|
||||
res: list[_DocumentType] = []
|
||||
remaining = length
|
||||
if isinstance(length, int) and length < 1:
|
||||
raise ValueError("to_list() length must be greater than 0")
|
||||
while self.alive:
|
||||
if not self._next_batch(res, remaining):
|
||||
break
|
||||
if length is not None:
|
||||
remaining = length - len(res)
|
||||
if remaining == 0:
|
||||
break
|
||||
return res
|
||||
|
||||
|
||||
class RawBatchCommandCursor(CommandCursor[_DocumentType]):
|
||||
_getmore_class = _RawBatchGetMore
|
||||
|
||||
@ -21,6 +21,7 @@ from collections import deque
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
@ -35,7 +36,7 @@ from typing import (
|
||||
from bson import RE_TYPE, _convert_raw_document_lists_to_streams
|
||||
from bson.code import Code
|
||||
from bson.son import SON
|
||||
from pymongo import helpers_shared
|
||||
from pymongo import _csot, helpers_shared
|
||||
from pymongo.collation import validate_collation_or_none
|
||||
from pymongo.common import (
|
||||
validate_is_document_type,
|
||||
@ -43,7 +44,9 @@ from pymongo.common import (
|
||||
)
|
||||
from pymongo.cursor_shared import _CURSOR_CLOSED_ERRORS, _QUERY_OPTIONS, CursorType, _Hint, _Sort
|
||||
from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure
|
||||
from pymongo.lock import _create_lock
|
||||
from pymongo.message import (
|
||||
_CursorAddress,
|
||||
_GetMore,
|
||||
_OpMsg,
|
||||
_OpReply,
|
||||
@ -52,8 +55,6 @@ from pymongo.message import (
|
||||
_RawBatchQuery,
|
||||
)
|
||||
from pymongo.response import PinnedResponse
|
||||
from pymongo.synchronous.cursor_base import _ConnectionManager, _CursorBase
|
||||
from pymongo.synchronous.helpers import next
|
||||
from pymongo.typings import _Address, _CollationIn, _DocumentOut, _DocumentType
|
||||
from pymongo.write_concern import validate_boolean
|
||||
|
||||
@ -64,11 +65,30 @@ if TYPE_CHECKING:
|
||||
from pymongo.read_preferences import _ServerMode
|
||||
from pymongo.synchronous.client_session import ClientSession
|
||||
from pymongo.synchronous.collection import Collection
|
||||
from pymongo.synchronous.pool import Connection
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
|
||||
class Cursor(_CursorBase[_DocumentType]):
|
||||
class _ConnectionManager:
|
||||
"""Used with exhaust cursors to ensure the connection is returned."""
|
||||
|
||||
def __init__(self, conn: Connection, more_to_come: bool):
|
||||
self.conn: Optional[Connection] = conn
|
||||
self.more_to_come = more_to_come
|
||||
self._lock = _create_lock()
|
||||
|
||||
def update_exhaust(self, more_to_come: bool) -> None:
|
||||
self.more_to_come = more_to_come
|
||||
|
||||
def close(self) -> None:
|
||||
"""Return this instance's connection to the connection pool."""
|
||||
if self.conn:
|
||||
self.conn.unpin()
|
||||
self.conn = None
|
||||
|
||||
|
||||
class Cursor(Generic[_DocumentType]):
|
||||
_query_class = _Query
|
||||
_getmore_class = _GetMore
|
||||
|
||||
@ -245,8 +265,8 @@ class Cursor(_CursorBase[_DocumentType]):
|
||||
"""The number of documents retrieved so far."""
|
||||
return self._retrieved
|
||||
|
||||
def _get_namespace(self) -> str:
|
||||
return f"{self._dbname}.{self._collname}"
|
||||
def __del__(self) -> None:
|
||||
self._die_no_lock()
|
||||
|
||||
def clone(self) -> Cursor[_DocumentType]:
|
||||
"""Get a clone of this cursor.
|
||||
@ -876,6 +896,55 @@ class Cursor(_CursorBase[_DocumentType]):
|
||||
self._read_preference = self._collection._read_preference_for(self.session)
|
||||
return self._read_preference
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
"""Does this cursor have the potential to return more data?
|
||||
|
||||
This is mostly useful with `tailable cursors
|
||||
<https://www.mongodb.com/docs/manual/core/tailable-cursors/>`_
|
||||
since they will stop iterating even though they *may* return more
|
||||
results in the future.
|
||||
|
||||
With regular cursors, simply use a for loop instead of :attr:`alive`::
|
||||
|
||||
for doc in collection.find():
|
||||
print(doc)
|
||||
|
||||
.. note:: Even if :attr:`alive` is True, :meth:`next` can raise
|
||||
:exc:`StopIteration`. :attr:`alive` can also be True while iterating
|
||||
a cursor from a failed server. In this case :attr:`alive` will
|
||||
return False after :meth:`next` fails to retrieve the next batch
|
||||
of results from the server.
|
||||
"""
|
||||
return bool(len(self._data) or (not self._killed))
|
||||
|
||||
@property
|
||||
def cursor_id(self) -> Optional[int]:
|
||||
"""Returns the id of the cursor
|
||||
|
||||
.. versionadded:: 2.2
|
||||
"""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def address(self) -> Optional[tuple[str, Any]]:
|
||||
"""The (host, port) of the server used, or None.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Renamed from "conn_id".
|
||||
"""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[ClientSession]:
|
||||
"""The cursor's :class:`~pymongo.client_session.ClientSession`, or None.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
"""
|
||||
if self._session and not self._session._implicit:
|
||||
return self._session
|
||||
return None
|
||||
|
||||
def __copy__(self) -> Cursor[_DocumentType]:
|
||||
"""Support function for `copy.copy()`.
|
||||
|
||||
@ -939,6 +1008,59 @@ class Cursor(_CursorBase[_DocumentType]):
|
||||
y[key] = value # type:ignore[index]
|
||||
return y
|
||||
|
||||
def _prepare_to_die(self, already_killed: bool) -> tuple[int, Optional[_CursorAddress]]:
|
||||
self._killed = True
|
||||
if self._id and not already_killed:
|
||||
cursor_id = self._id
|
||||
assert self._address is not None
|
||||
address = _CursorAddress(self._address, f"{self._dbname}.{self._collname}")
|
||||
else:
|
||||
# Skip killCursors.
|
||||
cursor_id = 0
|
||||
address = None
|
||||
return cursor_id, address
|
||||
|
||||
def _die_no_lock(self) -> None:
|
||||
"""Closes this cursor without acquiring a lock."""
|
||||
try:
|
||||
already_killed = self._killed
|
||||
except AttributeError:
|
||||
# ___init__ did not run to completion (or at all).
|
||||
return
|
||||
|
||||
cursor_id, address = self._prepare_to_die(already_killed)
|
||||
self._collection.database.client._cleanup_cursor_no_lock(
|
||||
cursor_id, address, self._sock_mgr, self._session
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
def _die_lock(self) -> None:
|
||||
"""Closes this cursor."""
|
||||
try:
|
||||
already_killed = self._killed
|
||||
except AttributeError:
|
||||
# ___init__ did not run to completion (or at all).
|
||||
return
|
||||
|
||||
cursor_id, address = self._prepare_to_die(already_killed)
|
||||
self._collection.database.client._cleanup_cursor_lock(
|
||||
cursor_id,
|
||||
address,
|
||||
self._sock_mgr,
|
||||
self._session,
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
def close(self) -> None:
|
||||
"""Explicitly close / kill this cursor."""
|
||||
self._die_lock()
|
||||
|
||||
def distinct(self, key: str) -> list[Any]:
|
||||
"""Get a list of distinct values for `key` among all documents
|
||||
in the result set of this query.
|
||||
@ -1171,8 +1293,40 @@ class Cursor(_CursorBase[_DocumentType]):
|
||||
def __enter__(self) -> Cursor[_DocumentType]:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
self.close()
|
||||
|
||||
class RawBatchCursor(Cursor[_DocumentType]):
|
||||
@_csot.apply
|
||||
def to_list(self, length: Optional[int] = None) -> list[_DocumentType]:
|
||||
"""Converts the contents of this cursor to a list more efficiently than ``[doc for doc in cursor]``.
|
||||
|
||||
To use::
|
||||
|
||||
>>> cursor.to_list()
|
||||
|
||||
Or, to read at most n items from the cursor::
|
||||
|
||||
>>> cursor.to_list(n)
|
||||
|
||||
If the cursor is empty or has no more results, an empty list will be returned.
|
||||
|
||||
.. versionadded:: 4.9
|
||||
"""
|
||||
res: list[_DocumentType] = []
|
||||
remaining = length
|
||||
if isinstance(length, int) and length < 1:
|
||||
raise ValueError("to_list() length must be greater than 0")
|
||||
while self.alive:
|
||||
if not self._next_batch(res, remaining):
|
||||
break
|
||||
if length is not None:
|
||||
remaining = length - len(res)
|
||||
if remaining == 0:
|
||||
break
|
||||
return res
|
||||
|
||||
|
||||
class RawBatchCursor(Cursor, Generic[_DocumentType]): # type: ignore[type-arg]
|
||||
"""A cursor / iterator over raw batches of BSON data from a query result."""
|
||||
|
||||
_query_class = _RawBatchQuery
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
# Copyright 2026-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.
|
||||
|
||||
"""Synchronous cursor base extending the shared agnostic cursor base."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from pymongo import _csot
|
||||
from pymongo.cursor_shared import _AgnosticCursorBase
|
||||
from pymongo.lock import _create_lock
|
||||
from pymongo.typings import _DocumentType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pymongo.synchronous.client_session import ClientSession
|
||||
from pymongo.synchronous.pool import Connection
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
|
||||
class _ConnectionManager:
|
||||
"""Used with exhaust cursors to ensure the connection is returned."""
|
||||
|
||||
def __init__(self, conn: Connection, more_to_come: bool):
|
||||
self.conn: Optional[Connection] = conn
|
||||
self.more_to_come = more_to_come
|
||||
self._lock = _create_lock()
|
||||
|
||||
def update_exhaust(self, more_to_come: bool) -> None:
|
||||
self.more_to_come = more_to_come
|
||||
|
||||
def close(self) -> None:
|
||||
"""Return this instance's connection to the connection pool."""
|
||||
if self.conn:
|
||||
self.conn.unpin()
|
||||
self.conn = None
|
||||
|
||||
|
||||
class _CursorBase(_AgnosticCursorBase[_DocumentType]):
|
||||
"""Synchronous cursor base class."""
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[ClientSession]:
|
||||
"""The cursor's :class:`~pymongo.client_session.ClientSession`, or None.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
"""
|
||||
if self._session and not self._session._implicit:
|
||||
return self._session
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def _next_batch(self, result: list, total: Optional[int] = None) -> bool: # type: ignore[type-arg]
|
||||
...
|
||||
|
||||
def _die_lock(self) -> None:
|
||||
"""Closes this cursor."""
|
||||
try:
|
||||
already_killed = self._killed
|
||||
except AttributeError:
|
||||
# ___init__ did not run to completion (or at all).
|
||||
return
|
||||
|
||||
cursor_id, address = self._prepare_to_die(already_killed)
|
||||
self._collection.database.client._cleanup_cursor_lock(
|
||||
cursor_id,
|
||||
address,
|
||||
self._sock_mgr,
|
||||
self._session,
|
||||
)
|
||||
if self._session and self._session._implicit:
|
||||
self._session._attached_to_cursor = False
|
||||
self._session = None
|
||||
self._sock_mgr = None
|
||||
|
||||
def close(self) -> None:
|
||||
"""Explicitly close / kill this cursor."""
|
||||
self._die_lock()
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
self.close()
|
||||
|
||||
@_csot.apply
|
||||
def to_list(self, length: Optional[int] = None) -> list[_DocumentType]:
|
||||
"""Converts the contents of this cursor to a list more efficiently than ``[doc for doc in cursor]``.
|
||||
|
||||
To use::
|
||||
|
||||
>>> cursor.to_list()
|
||||
|
||||
Or, to read at most n items from the cursor::
|
||||
|
||||
>>> cursor.to_list(n)
|
||||
|
||||
If the cursor is empty or has no more results, an empty list will be returned.
|
||||
|
||||
.. versionadded:: 4.9
|
||||
"""
|
||||
res: list[_DocumentType] = []
|
||||
remaining = length
|
||||
if isinstance(length, int) and length < 1:
|
||||
raise ValueError("to_list() length must be greater than 0")
|
||||
while self.alive:
|
||||
if not self._next_batch(res, remaining):
|
||||
break
|
||||
if length is not None:
|
||||
remaining = length - len(res)
|
||||
if remaining == 0:
|
||||
break
|
||||
return res
|
||||
@ -698,7 +698,7 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
.. versionadded:: 3.9
|
||||
|
||||
.. _aggregation pipeline:
|
||||
https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
|
||||
https://mongodb.com/docs/manual/reference/operator/aggregation-pipeline
|
||||
|
||||
.. _aggregate command:
|
||||
https://mongodb.com/docs/manual/reference/command/aggregate
|
||||
@ -931,15 +931,12 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (session and session._txn_read_preference()) or ReadPreference.PRIMARY
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession],
|
||||
_server: Server,
|
||||
conn: Connection,
|
||||
read_preference: _ServerMode,
|
||||
) -> Union[dict[str, Any], _CodecDocumentType]:
|
||||
with self._client._conn_for_reads(read_preference, session, operation=command_name) as (
|
||||
connection,
|
||||
read_preference,
|
||||
):
|
||||
return self._command(
|
||||
conn,
|
||||
connection,
|
||||
command,
|
||||
value,
|
||||
check,
|
||||
@ -950,10 +947,6 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return self._client._retryable_read(
|
||||
inner, read_preference, session, command_name, None, False, is_run_command=True
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
def cursor_command(
|
||||
self,
|
||||
@ -1021,17 +1014,15 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
with self._client._tmp_session(session) as tmp_session:
|
||||
opts = codec_options or DEFAULT_CODEC_OPTIONS
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (
|
||||
tmp_session and tmp_session._txn_read_preference()
|
||||
) or ReadPreference.PRIMARY
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession],
|
||||
_server: Server,
|
||||
conn: Connection,
|
||||
read_preference: _ServerMode,
|
||||
) -> CommandCursor[_DocumentType]:
|
||||
with self._client._conn_for_reads(read_preference, tmp_session, command_name) as (
|
||||
conn,
|
||||
read_preference,
|
||||
):
|
||||
response = self._command(
|
||||
conn,
|
||||
command,
|
||||
@ -1040,7 +1031,7 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
None,
|
||||
read_preference,
|
||||
opts,
|
||||
session=session,
|
||||
session=tmp_session,
|
||||
**kwargs,
|
||||
)
|
||||
coll = self.get_collection("$cmd", read_preference=read_preference)
|
||||
@ -1050,7 +1041,7 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
response["cursor"],
|
||||
conn.address,
|
||||
max_await_time_ms=max_await_time_ms,
|
||||
session=session,
|
||||
session=tmp_session,
|
||||
comment=comment,
|
||||
)
|
||||
cmd_cursor._maybe_pin_connection(conn)
|
||||
@ -1058,10 +1049,6 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
else:
|
||||
raise InvalidOperation("Command does not return a cursor.")
|
||||
|
||||
return self.client._retryable_read(
|
||||
inner, read_preference, tmp_session, command_name, None, False
|
||||
)
|
||||
|
||||
def _retryable_read_command(
|
||||
self,
|
||||
command: Union[str, MutableMapping[str, Any]],
|
||||
@ -1260,11 +1247,9 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
command["comment"] = comment
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> dict[str, Any]:
|
||||
with self._client._conn_for_writes(session, operation=_Op.DROP) as connection:
|
||||
return self._command(
|
||||
conn,
|
||||
connection,
|
||||
command,
|
||||
allowable_errors=["ns not found", 26],
|
||||
write_concern=self._write_concern_for(session),
|
||||
@ -1272,8 +1257,6 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
return self.client._retryable_write(False, inner, session, _Op.DROP)
|
||||
|
||||
@_csot.apply
|
||||
def drop_collection(
|
||||
self,
|
||||
|
||||
@ -710,10 +710,7 @@ class ClientEncryption(Generic[_DocumentType]):
|
||||
self._encryption = ExplicitEncrypter(
|
||||
self._io_callbacks,
|
||||
_create_mongocrypt_options(
|
||||
kms_providers=kms_providers,
|
||||
schema_map=None,
|
||||
key_expiration_ms=key_expiration_ms,
|
||||
bypass_encryption=True, # Don't load crypt_shared
|
||||
kms_providers=kms_providers, schema_map=None, key_expiration_ms=key_expiration_ms
|
||||
),
|
||||
)
|
||||
# Use the same key vault collection as the callback.
|
||||
|
||||
@ -16,12 +16,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import functools
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@ -29,8 +24,6 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from pymongo import _csot
|
||||
from pymongo.common import MAX_ADAPTIVE_RETRIES
|
||||
from pymongo.errors import (
|
||||
OperationFailure,
|
||||
)
|
||||
@ -43,7 +36,6 @@ F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def _handle_reauth(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
def inner(*args: Any, **kwargs: Any) -> Any:
|
||||
no_reauth = kwargs.pop("no_reauth", False)
|
||||
from pymongo.message import _BulkWriteContext
|
||||
@ -76,46 +68,6 @@ def _handle_reauth(func: F) -> F:
|
||||
return cast(F, inner)
|
||||
|
||||
|
||||
_BACKOFF_INITIAL = 0.1
|
||||
_BACKOFF_MAX = 10
|
||||
|
||||
|
||||
def _backoff(
|
||||
attempt: int, initial_delay: float = _BACKOFF_INITIAL, max_delay: float = _BACKOFF_MAX
|
||||
) -> float:
|
||||
jitter = random.random() # noqa: S311
|
||||
return jitter * min(initial_delay * (2**attempt), max_delay)
|
||||
|
||||
|
||||
class _RetryPolicy:
|
||||
"""A retry limiter that performs exponential backoff with jitter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attempts: int = MAX_ADAPTIVE_RETRIES,
|
||||
backoff_initial: float = _BACKOFF_INITIAL,
|
||||
backoff_max: float = _BACKOFF_MAX,
|
||||
):
|
||||
self.attempts = attempts
|
||||
self.backoff_initial = backoff_initial
|
||||
self.backoff_max = backoff_max
|
||||
|
||||
def backoff(self, attempt: int) -> float:
|
||||
"""Return the backoff duration for the given attempt."""
|
||||
return _backoff(max(0, attempt - 1), self.backoff_initial, self.backoff_max)
|
||||
|
||||
def should_retry(self, attempt: int, delay: float) -> bool:
|
||||
"""Return if we have retry attempts remaining and the next backoff would not exceed a timeout."""
|
||||
if attempt > self.attempts:
|
||||
return False
|
||||
|
||||
if _csot.get_timeout():
|
||||
if time.monotonic() + delay > _csot.get_deadline():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _getaddrinfo(
|
||||
host: Any, port: Any, **kwargs: Any
|
||||
) -> list[
|
||||
@ -132,17 +84,3 @@ def _getaddrinfo(
|
||||
return loop.getaddrinfo(host, port, **kwargs) # type: ignore[return-value]
|
||||
else:
|
||||
return socket.getaddrinfo(host, port, **kwargs)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
next = builtins.next
|
||||
iter = builtins.iter
|
||||
else:
|
||||
|
||||
def next(cls: Any) -> Any:
|
||||
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
|
||||
return cls.__next__()
|
||||
|
||||
def iter(cls: Any) -> Any:
|
||||
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
|
||||
return cls.__iter__()
|
||||
|
||||
@ -35,7 +35,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
import warnings
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
@ -109,11 +108,8 @@ from pymongo.server_type import SERVER_TYPE
|
||||
from pymongo.synchronous import client_session, database, uri_parser
|
||||
from pymongo.synchronous.change_stream import ChangeStream, ClusterChangeStream
|
||||
from pymongo.synchronous.client_bulk import _ClientBulk
|
||||
from pymongo.synchronous.client_session import _SESSION, _EmptyServerSession
|
||||
from pymongo.synchronous.client_session import _EmptyServerSession
|
||||
from pymongo.synchronous.command_cursor import CommandCursor
|
||||
from pymongo.synchronous.helpers import (
|
||||
_RetryPolicy,
|
||||
)
|
||||
from pymongo.synchronous.settings import TopologySettings
|
||||
from pymongo.synchronous.topology import Topology, _ErrorContext
|
||||
from pymongo.topology_description import TOPOLOGY_TYPE, TopologyDescription
|
||||
@ -145,7 +141,7 @@ if TYPE_CHECKING:
|
||||
from pymongo.server_selectors import Selection
|
||||
from pymongo.synchronous.bulk import _Bulk
|
||||
from pymongo.synchronous.client_session import ClientSession, _ServerSession
|
||||
from pymongo.synchronous.cursor_base import _ConnectionManager
|
||||
from pymongo.synchronous.cursor import _ConnectionManager
|
||||
from pymongo.synchronous.encryption import _Encrypter
|
||||
from pymongo.synchronous.pool import Connection
|
||||
from pymongo.synchronous.server import Server
|
||||
@ -426,8 +422,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
with the server. Currently supported options are "snappy", "zlib"
|
||||
and "zstd". Support for snappy requires the
|
||||
`python-snappy <https://pypi.org/project/python-snappy/>`_ package.
|
||||
zlib support requires the Python standard library zlib module. For
|
||||
Python before 3.14 zstd requires the `backports.zstd <https://pypi.org/project/backports.zstd/>`_
|
||||
zlib support requires the Python standard library zlib module. zstd
|
||||
requires the `zstandard <https://pypi.org/project/zstandard/>`_
|
||||
package. By default no compression is used. Compression support
|
||||
must also be enabled on the server. MongoDB 3.6+ supports snappy
|
||||
and zlib compression. MongoDB 4.2+ adds support for zstd.
|
||||
@ -614,18 +610,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
client to use Stable API. See `versioned API <https://www.mongodb.com/docs/manual/reference/stable-api/#what-is-the-stable-api--and-should-you-use-it->`_ for
|
||||
details.
|
||||
|
||||
| **Overload retry options:**
|
||||
|
||||
- `max_adaptive_retries`: (int) How many retries to allow for overload errors. Defaults to ``2``.
|
||||
- `enable_overload_retargeting`: (boolean) Whether overload retargeting is enabled for this client.
|
||||
If enabled, server overload errors will cause retry attempts to select a server that has not yet returned an overload error, if possible.
|
||||
Defaults to ``False``.
|
||||
|
||||
.. seealso:: The MongoDB documentation on `connections <https://dochub.mongodb.org/core/connections>`_.
|
||||
|
||||
.. versionchanged:: 4.17
|
||||
Added the ``max_adaptive_retries`` and ``enable_overload_retargeting`` URI and keyword arguments.
|
||||
|
||||
.. versionchanged:: 4.5
|
||||
Added the ``serverMonitoringMode`` keyword argument.
|
||||
|
||||
@ -893,14 +879,11 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self._options.read_concern,
|
||||
)
|
||||
|
||||
self._retry_policy = _RetryPolicy(attempts=self._options.max_adaptive_retries)
|
||||
|
||||
self._init_based_on_options(self._seeds, srv_max_hosts, srv_service_name)
|
||||
|
||||
self._opened = False
|
||||
self._closed = False
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
if not is_srv:
|
||||
self._init_background()
|
||||
|
||||
@ -1423,8 +1406,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
)
|
||||
|
||||
def _ensure_session(self, session: Optional[ClientSession] = None) -> Optional[ClientSession]:
|
||||
"""If provided session and bound session are None, lend a temporary session."""
|
||||
session = session or self._get_bound_session()
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
if session:
|
||||
return session
|
||||
|
||||
@ -2004,8 +1986,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref: Optional[_ServerMode] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Internal retryable helper for all client transactions.
|
||||
|
||||
@ -2017,8 +1997,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Server Address, defaults to None
|
||||
:param read_pref: Topology of read operation, defaults to None
|
||||
:param retryable: If the operation should be retried once, defaults to None
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
|
||||
:return: Output of the calling func()
|
||||
"""
|
||||
@ -2033,8 +2011,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address=address,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
).run()
|
||||
|
||||
def _retryable_read(
|
||||
@ -2046,8 +2022,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = True,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Execute an operation with consecutive retries if possible
|
||||
|
||||
@ -2063,8 +2037,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Optional address when sending a message, defaults to None
|
||||
:param retryable: if we should attempt retries
|
||||
(may not always be supported even if supplied), defaults to False
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False.
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
"""
|
||||
|
||||
# Ensure that the client supports retrying on reads and there is no session in
|
||||
@ -2083,8 +2055,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref=read_pref,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
)
|
||||
|
||||
def _retryable_write(
|
||||
@ -2293,14 +2263,11 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self, session: Optional[client_session.ClientSession]
|
||||
) -> Generator[Optional[client_session.ClientSession], None]:
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
if session is not None and not isinstance(session, client_session.ClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be a ClientSession or None, not {type(session)}"
|
||||
)
|
||||
|
||||
# Check for a bound session. If one exists, treat it as an explicitly passed session.
|
||||
session = session or self._get_bound_session()
|
||||
if session:
|
||||
if session is not None:
|
||||
if not isinstance(session, client_session.ClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be a ClientSession or None, not {type(session)}"
|
||||
)
|
||||
# Don't call end_session.
|
||||
yield session
|
||||
return
|
||||
@ -2328,18 +2295,6 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
if session is not None:
|
||||
session._process_response(reply)
|
||||
|
||||
def _get_bound_session(self) -> Optional[ClientSession]:
|
||||
bound_session = _SESSION.get()
|
||||
if bound_session:
|
||||
if bound_session.client is self:
|
||||
return bound_session
|
||||
else:
|
||||
raise InvalidOperation(
|
||||
"Only the client that created the bound session can perform operations within its context block. See <PLACEHOLDER> for more information."
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def server_info(self, session: Optional[client_session.ClientSession] = None) -> dict[str, Any]:
|
||||
"""Get information about the MongoDB server we're connected to.
|
||||
|
||||
@ -2473,13 +2428,15 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
f"name_or_database must be an instance of str or a Database, not {type(name)}"
|
||||
)
|
||||
|
||||
self[name].command(
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
with self._conn_for_writes(session, operation=_Op.DROP_DATABASE) as conn:
|
||||
self[name]._command(
|
||||
conn,
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
def bulk_write(
|
||||
@ -2763,15 +2720,12 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
):
|
||||
self._last_error: Optional[Exception] = None
|
||||
self._retrying = False
|
||||
self._always_retryable = False
|
||||
self._max_retries = float("inf") if _csot.get_timeout() is not None else 1
|
||||
self._multiple_retries = _csot.get_timeout() is not None
|
||||
self._client = mongo_client
|
||||
self._retry_policy = mongo_client._retry_policy
|
||||
|
||||
self._func = func
|
||||
self._bulk = bulk
|
||||
self._session = session
|
||||
@ -2787,8 +2741,6 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._operation = operation
|
||||
self._operation_id = operation_id
|
||||
self._attempt_number = 0
|
||||
self._is_run_command = is_run_command
|
||||
self._is_aggregate_write = is_aggregate_write
|
||||
|
||||
def run(self) -> T:
|
||||
"""Runs the supplied func() and attempts a retry
|
||||
@ -2808,13 +2760,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
while True:
|
||||
self._check_last_error(check_csot=True)
|
||||
try:
|
||||
res = self._read() if self._is_read else self._write()
|
||||
# Track whether the transaction has completed a command.
|
||||
# If we need to apply backpressure to the first command,
|
||||
# we will need to revert back to starting state.
|
||||
if self._session is not None and self._session.in_transaction:
|
||||
self._session._transaction.has_completed_command = True
|
||||
return res
|
||||
return self._read() if self._is_read else self._write()
|
||||
except ServerSelectionTimeoutError:
|
||||
# The application may think the write was never attempted
|
||||
# if we raise ServerSelectionTimeoutError on the retry
|
||||
@ -2825,80 +2771,37 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
# most likely be a waste of time.
|
||||
raise
|
||||
except PyMongoError as exc:
|
||||
always_retryable = False
|
||||
overloaded = False
|
||||
exc_to_check = exc
|
||||
|
||||
if self._is_run_command and not (
|
||||
self._client.options.retry_reads and self._client.options.retry_writes
|
||||
):
|
||||
raise
|
||||
if self._is_aggregate_write and not self._client.options.retry_writes:
|
||||
raise
|
||||
|
||||
# Execute specialized catch on read
|
||||
if self._is_read:
|
||||
if isinstance(exc, (ConnectionFailure, OperationFailure)):
|
||||
# ConnectionFailures do not supply a code property
|
||||
exc_code = getattr(exc, "code", None)
|
||||
overloaded = exc.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc.has_error_label("RetryableError") and overloaded
|
||||
if not self._client.options.retry_reads or (
|
||||
not always_retryable
|
||||
and (
|
||||
self._is_not_eligible_for_retry()
|
||||
or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
)
|
||||
)
|
||||
if self._is_not_eligible_for_retry() or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
):
|
||||
raise
|
||||
self._retrying = True
|
||||
self._last_error = exc
|
||||
self._attempt_number += 1
|
||||
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if (
|
||||
overloaded
|
||||
and self._session is not None
|
||||
and self._session.in_transaction
|
||||
):
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
else:
|
||||
raise
|
||||
|
||||
# Specialized catch on write operation
|
||||
if not self._is_read:
|
||||
if isinstance(exc, ClientBulkWriteException) and isinstance(
|
||||
exc.error, PyMongoError
|
||||
):
|
||||
exc_to_check = exc.error
|
||||
retryable_write_label = exc_to_check.has_error_label("RetryableWriteError")
|
||||
overloaded = exc_to_check.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc_to_check.has_error_label("RetryableError") and overloaded
|
||||
|
||||
# Always retry abortTransaction and commitTransaction up to once
|
||||
if self._operation not in ["abortTransaction", "commitTransaction"] and (
|
||||
not self._client.options.retry_writes
|
||||
or not (self._retryable or always_retryable)
|
||||
):
|
||||
if not self._retryable:
|
||||
raise
|
||||
if retryable_write_label or always_retryable:
|
||||
if isinstance(exc, ClientBulkWriteException) and exc.error:
|
||||
retryable_write_error_exc = isinstance(
|
||||
exc.error, PyMongoError
|
||||
) and exc.error.has_error_label("RetryableWriteError")
|
||||
else:
|
||||
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
|
||||
if retryable_write_error_exc:
|
||||
assert self._session
|
||||
self._session._unpin()
|
||||
if not always_retryable and (
|
||||
not retryable_write_label or self._is_not_eligible_for_retry()
|
||||
):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
if not retryable_write_error_exc or self._is_not_eligible_for_retry():
|
||||
if exc.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
@ -2907,39 +2810,17 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._bulk.retrying = True
|
||||
else:
|
||||
self._retrying = True
|
||||
if not exc_to_check.has_error_label("NoWritesPerformed"):
|
||||
if not exc.has_error_label("NoWritesPerformed"):
|
||||
self._last_error = exc
|
||||
if self._last_error is None:
|
||||
self._last_error = exc
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if overloaded and self._session is not None and self._session.in_transaction:
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
|
||||
if self._server is not None and (
|
||||
self._client.topology_description.topology_type_name == "Sharded"
|
||||
or (overloaded and self._client.options.enable_overload_retargeting)
|
||||
):
|
||||
if self._client.topology_description.topology_type == TOPOLOGY_TYPE.Sharded:
|
||||
self._deprioritized_servers.append(self._server)
|
||||
|
||||
self._always_retryable = always_retryable
|
||||
if overloaded:
|
||||
delay = self._retry_policy.backoff(self._attempt_number)
|
||||
if not self._retry_policy.should_retry(self._attempt_number, delay):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
time.sleep(delay)
|
||||
|
||||
def _is_not_eligible_for_retry(self) -> bool:
|
||||
"""Checks if the exchange is not eligible for retry"""
|
||||
return not self._retryable or (
|
||||
self._is_retrying() and self._attempt_number >= self._max_retries
|
||||
)
|
||||
return not self._retryable or (self._is_retrying() and not self._multiple_retries)
|
||||
|
||||
def _is_retrying(self) -> bool:
|
||||
"""Checks if the exchange is currently undergoing a retry"""
|
||||
@ -2998,7 +2879,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
and conn.supports_sessions
|
||||
)
|
||||
is_mongos = conn.is_mongos
|
||||
if not self._always_retryable and not sessions_supported:
|
||||
if not sessions_supported:
|
||||
# A retry is not possible because this server does
|
||||
# not support sessions raise the last error.
|
||||
self._check_last_error()
|
||||
@ -3030,7 +2911,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
conn,
|
||||
read_pref,
|
||||
):
|
||||
if self._retrying and not self._retryable and not self._always_retryable:
|
||||
if self._retrying and not self._retryable:
|
||||
self._check_last_error()
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
|
||||
@ -19,8 +19,6 @@ import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import weakref
|
||||
@ -51,12 +49,10 @@ from pymongo.errors import ( # type:ignore[attr-defined]
|
||||
DocumentTooLarge,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
_CertificateError,
|
||||
)
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
|
||||
@ -108,6 +104,21 @@ if TYPE_CHECKING:
|
||||
from pymongo.typings import _Address, _CollationIn
|
||||
from pymongo.write_concern import WriteConcern
|
||||
|
||||
try:
|
||||
from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl
|
||||
|
||||
def _set_non_inheritable_non_atomic(fd: int) -> None:
|
||||
"""Set the close-on-exec flag on the given file descriptor."""
|
||||
flags = fcntl(fd, F_GETFD)
|
||||
fcntl(fd, F_SETFD, flags | FD_CLOEXEC)
|
||||
|
||||
except ImportError:
|
||||
# Windows, various platforms we don't claim to support
|
||||
# (Jython, IronPython, ..), systems that don't provide
|
||||
# everything we need from fcntl, etc.
|
||||
def _set_non_inheritable_non_atomic(fd: int) -> None: # noqa: ARG001
|
||||
"""Dummy function for platforms that don't provide fcntl."""
|
||||
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
@ -254,7 +265,6 @@ class Connection:
|
||||
cmd = self.hello_cmd()
|
||||
performing_handshake = not self.performed_handshake
|
||||
awaitable = False
|
||||
cmd["backpressure"] = True
|
||||
if performing_handshake:
|
||||
self.performed_handshake = True
|
||||
cmd["client"] = self.opts.metadata
|
||||
@ -694,6 +704,8 @@ class PoolState:
|
||||
CLOSED = 3
|
||||
|
||||
|
||||
# Do *not* explicitly inherit from object or Jython won't call __del__
|
||||
# https://bugs.jython.org/issue1057
|
||||
class Pool:
|
||||
def __init__(
|
||||
self,
|
||||
@ -755,10 +767,14 @@ class Pool:
|
||||
# Enforces: maxConnecting
|
||||
# Also used for: clearing the wait queue
|
||||
self._max_connecting_cond = _create_condition(self.lock)
|
||||
self._pending = 0
|
||||
self._max_connecting = self.opts.max_connecting
|
||||
self._pending = 0
|
||||
self._client_id = client_id
|
||||
# Log before publishing event to prevent potential listener preemption in tests
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -768,11 +784,6 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
**self.opts.non_default_options,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
# Similar to active_sockets but includes threads in the wait queue.
|
||||
self.operation_count: int = 0
|
||||
# Retain references to pinned connections to prevent the CPython GC
|
||||
@ -787,6 +798,9 @@ class Pool:
|
||||
with self.lock:
|
||||
if self.state != PoolState.READY:
|
||||
self.state = PoolState.READY
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -795,9 +809,6 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
@ -858,6 +869,9 @@ class Pool:
|
||||
else:
|
||||
for conn in sockets:
|
||||
conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -866,11 +880,15 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
else:
|
||||
if old_state != PoolState.PAUSED:
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -880,13 +898,6 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
serviceId=service_id,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if not _IS_SYNC:
|
||||
asyncio.gather(
|
||||
*[conn.close_conn(ConnectionClosedReason.STALE) for conn in sockets], # type: ignore[func-returns-value]
|
||||
@ -988,21 +999,6 @@ class Pool:
|
||||
self.requests -= 1
|
||||
self.size_cond.notify()
|
||||
|
||||
def _handle_connection_error(self, error: BaseException) -> None:
|
||||
# Handle system overload condition for non-sdam pools.
|
||||
# Look for errors of type AutoReconnect and add error labels if appropriate.
|
||||
if self.is_sdam or type(error) not in (AutoReconnect, NetworkTimeout):
|
||||
return
|
||||
assert isinstance(error, AutoReconnect) # Appease type checker.
|
||||
# If the original error was a DNS, certificate, or SSL error, ignore it.
|
||||
if isinstance(error.__cause__, (_CertificateError, SSLErrors, socket.gaierror)):
|
||||
# End of file errors are excluded, because the server may have disconnected
|
||||
# during the handshake.
|
||||
if not isinstance(error.__cause__, (ssl.SSLEOFError, ssl.SSLZeroReturnError)):
|
||||
return
|
||||
error._add_error_label("SystemOverloadedError")
|
||||
error._add_error_label("RetryableError")
|
||||
|
||||
def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> Connection:
|
||||
"""Connect to Mongo and return a new Connection.
|
||||
|
||||
@ -1054,10 +1050,10 @@ class Pool:
|
||||
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
|
||||
error=ConnectionClosedReason.ERROR,
|
||||
)
|
||||
self._handle_connection_error(error)
|
||||
if isinstance(error, (IOError, OSError, *SSLErrors)):
|
||||
details = _get_timeout_details(self.opts)
|
||||
_raise_connection_failure(self.address, error, timeout_details=details)
|
||||
|
||||
raise
|
||||
|
||||
conn = Connection(networking_interface, self, self.address, conn_id, self.is_sdam) # type: ignore[arg-type]
|
||||
@ -1066,22 +1062,18 @@ class Pool:
|
||||
self.active_contexts.discard(tmp_context)
|
||||
if tmp_context.cancelled:
|
||||
conn.cancel_context.cancel()
|
||||
completed_hello = False
|
||||
try:
|
||||
if not self.is_sdam:
|
||||
conn.hello()
|
||||
completed_hello = True
|
||||
self.is_writable = conn.is_writable
|
||||
if handler:
|
||||
handler.contribute_socket(conn, completed_handshake=False)
|
||||
|
||||
conn.authenticate()
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException as e:
|
||||
except BaseException:
|
||||
with self.lock:
|
||||
self.active_contexts.discard(conn.cancel_context)
|
||||
if not completed_hello:
|
||||
self._handle_connection_error(e)
|
||||
conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
raise
|
||||
|
||||
@ -1410,8 +1402,8 @@ class Pool:
|
||||
:class:`~pymongo.errors.AutoReconnect` exceptions on server
|
||||
hiccups, etc. We only check if the socket was closed by an external
|
||||
error if it has been > 1 second since the socket was checked into the
|
||||
pool to keep performance reasonable -
|
||||
we can't avoid AutoReconnects completely anyway.
|
||||
pool, to keep performance reasonable - we can't avoid AutoReconnects
|
||||
completely anyway.
|
||||
"""
|
||||
idle_time_seconds = conn.idle_time_seconds()
|
||||
# If socket is idle, open a new one.
|
||||
@ -1422,9 +1414,8 @@ class Pool:
|
||||
conn.close_conn(ConnectionClosedReason.IDLE)
|
||||
return True
|
||||
|
||||
check_interval_seconds = self._check_interval_seconds
|
||||
if check_interval_seconds is not None and (
|
||||
check_interval_seconds == 0 or idle_time_seconds > check_interval_seconds
|
||||
if self._check_interval_seconds is not None and (
|
||||
self._check_interval_seconds == 0 or idle_time_seconds > self._check_interval_seconds
|
||||
):
|
||||
if conn.conn_closed():
|
||||
conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
|
||||
@ -111,7 +111,7 @@ class Topology:
|
||||
self._publish_tp = self._listeners is not None and self._listeners.enabled_for_topology
|
||||
|
||||
# Create events queue if there are publishers.
|
||||
self._events: queue.Queue[Any] | None = None
|
||||
self._events = None
|
||||
self.__events_executor: Any = None
|
||||
|
||||
if self._publish_server or self._publish_tp:
|
||||
@ -126,7 +126,6 @@ class Topology:
|
||||
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put((self._listeners.publish_topology_opened, (self._topology_id,)))
|
||||
self._settings = topology_settings
|
||||
topology_description = TopologyDescription(
|
||||
@ -144,7 +143,6 @@ class Topology:
|
||||
)
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_topology_description_changed,
|
||||
@ -163,7 +161,6 @@ class Topology:
|
||||
for seed in topology_settings.seeds:
|
||||
if self._publish_server:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put((self._listeners.publish_server_opened, (seed, self._topology_id)))
|
||||
if _SDAM_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
@ -268,7 +265,6 @@ class Topology:
|
||||
server_selection_timeout: Optional[float] = None,
|
||||
address: Optional[_Address] = None,
|
||||
operation_id: Optional[int] = None,
|
||||
deprioritized_servers: Optional[list[Server]] = None,
|
||||
) -> list[Server]:
|
||||
"""Return a list of Servers matching selector, or time out.
|
||||
|
||||
@ -296,12 +292,7 @@ class Topology:
|
||||
|
||||
with self._lock:
|
||||
server_descriptions = self._select_servers_loop(
|
||||
selector,
|
||||
server_timeout,
|
||||
operation,
|
||||
operation_id,
|
||||
address,
|
||||
deprioritized_servers=deprioritized_servers,
|
||||
selector, server_timeout, operation, operation_id, address
|
||||
)
|
||||
|
||||
return [
|
||||
@ -315,7 +306,6 @@ class Topology:
|
||||
operation: str,
|
||||
operation_id: Optional[int],
|
||||
address: Optional[_Address],
|
||||
deprioritized_servers: Optional[list[Server]] = None,
|
||||
) -> list[ServerDescription]:
|
||||
"""select_servers() guts. Hold the lock when calling this."""
|
||||
now = time.monotonic()
|
||||
@ -334,12 +324,7 @@ class Topology:
|
||||
)
|
||||
|
||||
server_descriptions = self._description.apply_selector(
|
||||
selector,
|
||||
address,
|
||||
custom_selector=self._settings.server_selector,
|
||||
deprioritized_servers=[server.description for server in deprioritized_servers]
|
||||
if deprioritized_servers
|
||||
else None,
|
||||
selector, address, custom_selector=self._settings.server_selector
|
||||
)
|
||||
|
||||
while not server_descriptions:
|
||||
@ -400,13 +385,9 @@ class Topology:
|
||||
operation_id: Optional[int] = None,
|
||||
) -> Server:
|
||||
servers = self.select_servers(
|
||||
selector,
|
||||
operation,
|
||||
server_selection_timeout,
|
||||
address,
|
||||
operation_id,
|
||||
deprioritized_servers,
|
||||
selector, operation, server_selection_timeout, address, operation_id
|
||||
)
|
||||
servers = _filter_servers(servers, deprioritized_servers)
|
||||
if len(servers) == 1:
|
||||
return servers[0]
|
||||
server1, server2 = random.sample(servers, 2)
|
||||
@ -510,7 +491,6 @@ class Topology:
|
||||
suppress_event = sd_old == server_description
|
||||
if self._publish_server and not suppress_event:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_server_description_changed,
|
||||
@ -523,7 +503,6 @@ class Topology:
|
||||
|
||||
if self._publish_tp and not suppress_event:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_topology_description_changed,
|
||||
@ -591,7 +570,6 @@ class Topology:
|
||||
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._events.put(
|
||||
(
|
||||
self._listeners.publish_topology_description_changed,
|
||||
@ -743,7 +721,6 @@ class Topology:
|
||||
# Publish only after releasing the lock.
|
||||
if self._publish_tp:
|
||||
assert self._events is not None
|
||||
assert self._listeners is not None
|
||||
self._description = TopologyDescription(
|
||||
TOPOLOGY_TYPE.Unknown,
|
||||
{},
|
||||
@ -911,9 +888,7 @@ class Topology:
|
||||
# Clear the pool.
|
||||
server.reset(service_id)
|
||||
elif isinstance(error, ConnectionFailure):
|
||||
if isinstance(error, WaitQueueTimeoutError) or (
|
||||
error.has_error_label("SystemOverloadedError")
|
||||
):
|
||||
if isinstance(error, WaitQueueTimeoutError):
|
||||
return
|
||||
# "Client MUST replace the server's description with type Unknown
|
||||
# ... MUST NOT request an immediate check of the server."
|
||||
@ -1135,3 +1110,16 @@ def _is_stale_server_description(current_sd: ServerDescription, new_sd: ServerDe
|
||||
if current_tv["processId"] != new_tv["processId"]:
|
||||
return False
|
||||
return current_tv["counter"] > new_tv["counter"]
|
||||
|
||||
|
||||
def _filter_servers(
|
||||
candidates: list[Server], deprioritized_servers: Optional[list[Server]] = None
|
||||
) -> list[Server]:
|
||||
"""Filter out deprioritized servers from a list of server candidates."""
|
||||
if not deprioritized_servers:
|
||||
return candidates
|
||||
|
||||
filtered = [server for server in candidates if server not in deprioritized_servers]
|
||||
|
||||
# If not possible to pick a prioritized server, return the original list
|
||||
return filtered or candidates
|
||||
|
||||
@ -85,7 +85,6 @@ class TopologyDescription:
|
||||
self._server_descriptions = server_descriptions
|
||||
self._max_set_version = max_set_version
|
||||
self._max_election_id = max_election_id
|
||||
self._candidate_servers = list(self._server_descriptions.values())
|
||||
|
||||
# The heartbeat_frequency is used in staleness estimates.
|
||||
self._topology_settings = topology_settings
|
||||
@ -249,11 +248,6 @@ class TopologyDescription:
|
||||
"""List of readable Servers."""
|
||||
return [s for s in self._server_descriptions.values() if s.is_readable]
|
||||
|
||||
@property
|
||||
def candidate_servers(self) -> list[ServerDescription]:
|
||||
"""List of Servers excluding deprioritized servers."""
|
||||
return self._candidate_servers
|
||||
|
||||
@property
|
||||
def common_wire_version(self) -> Optional[int]:
|
||||
"""Minimum of all servers' max wire versions, or None."""
|
||||
@ -289,27 +283,11 @@ class TopologyDescription:
|
||||
if (cast(float, s.round_trip_time) - fastest) <= threshold
|
||||
]
|
||||
|
||||
def _filter_servers(
|
||||
self, deprioritized_servers: Optional[list[ServerDescription]] = None
|
||||
) -> None:
|
||||
"""Filter out deprioritized servers from a list of server candidates."""
|
||||
if not deprioritized_servers:
|
||||
self._candidate_servers = self.known_servers
|
||||
else:
|
||||
deprioritized_addresses = {sd.address for sd in deprioritized_servers}
|
||||
filtered = [
|
||||
server
|
||||
for server in self.known_servers
|
||||
if server.address not in deprioritized_addresses
|
||||
]
|
||||
self._candidate_servers = filtered or self.known_servers
|
||||
|
||||
def apply_selector(
|
||||
self,
|
||||
selector: Any,
|
||||
address: Optional[_Address] = None,
|
||||
custom_selector: Optional[_ServerSelector] = None,
|
||||
deprioritized_servers: Optional[list[ServerDescription]] = None,
|
||||
) -> list[ServerDescription]:
|
||||
"""List of servers matching the provided selector(s).
|
||||
|
||||
@ -344,25 +322,16 @@ class TopologyDescription:
|
||||
if address:
|
||||
# Ignore selectors when explicit address is requested.
|
||||
description = self.server_descriptions().get(address)
|
||||
return [description] if description and description.is_server_type_known else []
|
||||
return [description] if description else []
|
||||
|
||||
self._filter_servers(deprioritized_servers)
|
||||
# Primary selection fast path.
|
||||
if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and type(selector) is Primary:
|
||||
for sd in self._candidate_servers:
|
||||
for sd in self._server_descriptions.values():
|
||||
if sd.server_type == SERVER_TYPE.RSPrimary:
|
||||
sds = [sd]
|
||||
if custom_selector:
|
||||
sds = custom_selector(sds)
|
||||
return sds
|
||||
# All primaries are deprioritized
|
||||
if deprioritized_servers:
|
||||
for sd in deprioritized_servers:
|
||||
if sd.server_type == SERVER_TYPE.RSPrimary:
|
||||
sds = [sd]
|
||||
if custom_selector:
|
||||
sds = custom_selector(sds)
|
||||
return sds
|
||||
# No primary found, return an empty list.
|
||||
return []
|
||||
|
||||
@ -370,11 +339,6 @@ class TopologyDescription:
|
||||
# Ignore read preference for sharded clusters.
|
||||
if self.topology_type != TOPOLOGY_TYPE.Sharded:
|
||||
selection = selector(selection)
|
||||
# No suitable servers found, apply preference again but include deprioritized servers.
|
||||
if not selection and deprioritized_servers:
|
||||
self._filter_servers(None)
|
||||
selection = Selection.from_topology_description(self)
|
||||
selection = selector(selection)
|
||||
|
||||
# Apply custom selector followed by localThresholdMS.
|
||||
if custom_selector is not None and selection:
|
||||
|
||||
@ -48,20 +48,21 @@ Tracker = "https://jira.mongodb.org/projects/PYTHON/issues"
|
||||
|
||||
[dependency-groups]
|
||||
dev = []
|
||||
pip = ["pip>=20.2"]
|
||||
gevent = ["gevent>=21.12"]
|
||||
pip = ["pip"]
|
||||
gevent = ["gevent>=20.6.0"]
|
||||
coverage = [
|
||||
"coverage[toml]>=5,<=7.10.7"
|
||||
"pytest-cov",
|
||||
"coverage>=5,<=7.10.6"
|
||||
]
|
||||
mockupdb = [
|
||||
"mockupdb@git+https://github.com/mongodb-labs/mongo-mockup-db@master"
|
||||
]
|
||||
perf = ["simplejson>=3.17.0"]
|
||||
typing = [
|
||||
"mypy==1.19.1",
|
||||
"pyright==1.1.408",
|
||||
"typing_extensions>=3.7.4.2",
|
||||
"pip>=20.2"
|
||||
"mypy==1.18.2",
|
||||
"pyright==1.1.406",
|
||||
"typing_extensions",
|
||||
"pip"
|
||||
]
|
||||
|
||||
# Used to call hatch_build.py
|
||||
@ -238,11 +239,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?)|dummy.*)$"
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
include = [
|
||||
"pymongo/*",
|
||||
"bson/*",
|
||||
"gridfs/*"
|
||||
]
|
||||
source = ["pymongo", "bson", "gridfs" ]
|
||||
relative_files = true
|
||||
|
||||
[tool.coverage.report]
|
||||
|
||||
@ -3,4 +3,4 @@ sphinx_rtd_theme>=2,<4
|
||||
readthedocs-sphinx-search~=0.3
|
||||
sphinxcontrib-shellcheck>=1,<2
|
||||
sphinx-autobuild>=2020.9.1
|
||||
furo==2025.12.19
|
||||
furo==2025.9.25
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
pykerberos>=1.2.4;os.name!='nt'
|
||||
pykerberos;os.name!='nt'
|
||||
winkerberos>=0.5.0;os.name=='nt'
|
||||
|
||||
@ -4,10 +4,9 @@
|
||||
# service_identity 18.1.0 introduced support for IP addr matching.
|
||||
# Fallback to certifi on Windows if we can't load CA certs from the system
|
||||
# store and just use certifi on macOS.
|
||||
# pyopenssl, cryptography, and service_identity must be set in tandem.
|
||||
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
|
||||
certifi>=2023.7.22;os.name=='nt' or sys_platform=='darwin'
|
||||
pyopenssl>=23.2.0
|
||||
requests>=2.23.0,<3.0
|
||||
cryptography>=42.0.0
|
||||
service_identity>=23.1.0
|
||||
pyopenssl>=17.2.0
|
||||
requests<3.0.0
|
||||
cryptography>=2.5
|
||||
service_identity>=18.1.0
|
||||
|
||||
@ -1 +1 @@
|
||||
python-snappy>=0.6.0
|
||||
python-snappy
|
||||
|
||||
@ -1 +1 @@
|
||||
backports.zstd>=1.0.0;python_version<'3.14'
|
||||
zstandard
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user