Compare commits
309 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a8e34c726 | ||
|
|
552b7bf47b | ||
|
|
a50550535d | ||
|
|
0adf6df131 | ||
|
|
f145c7db94 | ||
|
|
b6bac45c7e | ||
|
|
8dc7efade2 | ||
|
|
f4219bdca2 | ||
|
|
900d9c7910 | ||
|
|
575d75f4d3 | ||
|
|
c30eff1291 | ||
|
|
e67931dff7 | ||
|
|
64edd22d73 | ||
|
|
b3f1c4befb | ||
|
|
ab44a21b46 | ||
|
|
a13842f351 | ||
|
|
8363bf60ad | ||
|
|
5406febcd9 | ||
|
|
3491c08ef6 | ||
|
|
912ef337f9 | ||
|
|
b4e2c03a92 | ||
|
|
f31ba09713 | ||
|
|
5da91837d4 | ||
|
|
35e51a50f3 | ||
|
|
f41dd5c08b | ||
|
|
49e7a052e2 | ||
|
|
a2b0cd85e3 | ||
|
|
e1751ff253 | ||
|
|
ee20ef52ec | ||
|
|
08b806fd87 | ||
|
|
db4db928d3 | ||
|
|
ee851ba974 | ||
|
|
ce416a0944 | ||
|
|
daba50c797 | ||
|
|
c3428789fb | ||
|
|
ec9d95413c | ||
|
|
13085ff679 | ||
|
|
80c3ff2aee | ||
|
|
3d89d9faca | ||
|
|
b6cc22ffdd | ||
|
|
f303125cee | ||
|
|
38da6c3f9a | ||
|
|
926541fa4d | ||
|
|
f533157981 | ||
|
|
e028fe2a38 | ||
|
|
469a32a9dd | ||
|
|
84814b2a72 | ||
|
|
908102d776 | ||
|
|
edd0e0698f | ||
|
|
cbd82e75e7 | ||
|
|
6923641626 | ||
|
|
b60d266ad7 | ||
|
|
36676384bd | ||
|
|
0441761872 | ||
|
|
fdb6a3291f | ||
|
|
b1a0a1f104 | ||
|
|
f28ab12db0 | ||
|
|
d5e1777732 | ||
|
|
afc884d786 | ||
|
|
e077ebd926 | ||
|
|
543c4e532c | ||
|
|
182d8e2ea0 | ||
|
|
4c86d86bf1 | ||
|
|
fa56b563dd | ||
|
|
896f139ddc | ||
|
|
a89c5e3a89 | ||
|
|
db6dad95be | ||
|
|
a426ad91d7 | ||
|
|
1e7477b9df | ||
|
|
db28d14b6d | ||
|
|
12b3859903 | ||
|
|
b88415b8e8 | ||
|
|
cb01da6a50 | ||
|
|
32901018ca | ||
|
|
1be94d262d | ||
|
|
6585d9cb51 | ||
|
|
fdb1f7ea4a | ||
|
|
0cd9763423 | ||
|
|
2f263d4d3f | ||
|
|
e9658b2406 | ||
|
|
10dd20405b | ||
|
|
130067799c | ||
|
|
18c1f142b5 | ||
|
|
6ccaae5772 | ||
|
|
5b13ae006a | ||
|
|
c930c69776 | ||
|
|
b1ea391842 | ||
|
|
e5070789cc | ||
|
|
60289f0398 | ||
|
|
1e78bd4d46 | ||
|
|
029c74cb3a | ||
|
|
0ce7686c64 | ||
|
|
f9f48bab95 | ||
|
|
0cfba4994d | ||
|
|
f813437154 | ||
|
|
27ac7bd717 | ||
|
|
2f7946f523 | ||
|
|
da6d3d9e62 | ||
|
|
37632e70d6 | ||
|
|
a9923507c5 | ||
|
|
1496b8d2ff | ||
|
|
ab8b99a005 | ||
|
|
ae88b5a08f | ||
|
|
49e59d41b2 | ||
|
|
e7aab567bf | ||
|
|
2195866ba7 | ||
|
|
3093a7c7cb | ||
|
|
44baec9e9c | ||
|
|
bd6decb8c0 | ||
|
|
6011df9e37 | ||
|
|
8bf8263391 | ||
|
|
222a55f8cd | ||
|
|
3d76c84b2a | ||
|
|
881094015b | ||
|
|
42cf3407c8 | ||
|
|
1a434c7c59 | ||
|
|
cef27b18d9 | ||
|
|
a9c034426b | ||
|
|
0c5eec790b | ||
|
|
47da699a87 | ||
|
|
71e0c950e1 | ||
|
|
44a58f1650 | ||
|
|
63acab96cf | ||
|
|
eb25ce420e | ||
|
|
f278e471d1 | ||
|
|
5f00966f9c | ||
|
|
b607ef144c | ||
|
|
fd02550349 | ||
|
|
0c8a22b87d | ||
|
|
a5f6d638b9 | ||
|
|
ad1167d01e | ||
|
|
faa77eab43 | ||
|
|
6a796c8668 | ||
|
|
6d91859659 | ||
|
|
5eb1edf315 | ||
|
|
d595913117 | ||
|
|
89a4eaa36c | ||
|
|
491f5ba77f | ||
|
|
84772bd8a9 | ||
|
|
a2e39ada00 | ||
|
|
46974363b4 | ||
|
|
406bed0418 | ||
|
|
16a2fea219 | ||
|
|
52400e11a1 | ||
|
|
d47bd9cf95 | ||
|
|
6bdf07e726 | ||
|
|
e3910f868b | ||
|
|
215b3b1938 | ||
|
|
67384f0f08 | ||
|
|
b291807106 | ||
|
|
8d4518287c | ||
|
|
4839e523c8 | ||
|
|
e0767cf5a1 | ||
|
|
0d93ec48a5 | ||
|
|
1f308c841f | ||
|
|
eb0cedd969 | ||
|
|
fad2ccb0e7 | ||
|
|
448a4944ff | ||
|
|
4849eacc10 | ||
|
|
9e64ed1bd8 | ||
|
|
0049dc8896 | ||
|
|
51f7b408f3 | ||
|
|
29c4c2cc0f | ||
|
|
266caf02c4 | ||
|
|
6fe85436ae | ||
|
|
9603a85f21 | ||
|
|
ef59602e39 | ||
|
|
668bd8232a | ||
|
|
4936fe90bf | ||
|
|
dba0aa94ad | ||
|
|
a7a645f85f | ||
|
|
5787acc271 | ||
|
|
4b4d74971c | ||
|
|
4b4c949997 | ||
|
|
8cf65796da | ||
|
|
7a07c02814 | ||
|
|
eca38b730b | ||
|
|
32e183baa7 | ||
|
|
3da6e858d5 | ||
|
|
2b148867e7 | ||
|
|
527cbdd18a | ||
|
|
8879f2b951 | ||
|
|
d2653eecc6 | ||
|
|
1514e9b784 | ||
|
|
98e9f5ecc1 | ||
|
|
d7316afb63 | ||
|
|
7580309e99 | ||
|
|
47c5460d2e | ||
|
|
b84e1a7ce4 | ||
|
|
c0e0554a3b | ||
|
|
d63edf7aea | ||
|
|
b756bbd2a3 | ||
|
|
b2bba67b61 | ||
|
|
6656767850 | ||
|
|
cffb9069fd | ||
|
|
0d4c84e86f | ||
|
|
8c361be219 | ||
|
|
9892e1bbe9 | ||
|
|
cd4e5db997 | ||
|
|
3ebd93480a | ||
|
|
ddf9508e15 | ||
|
|
e08284bdca | ||
|
|
5e96353797 | ||
|
|
9a9a65c617 | ||
|
|
f7b94be0db | ||
|
|
db3d3c7022 | ||
|
|
3a26119eb3 | ||
|
|
d24b4a5697 | ||
|
|
e4b7eb52e6 | ||
|
|
37d327fbd8 | ||
|
|
b32da4b409 | ||
|
|
2a1523fa85 | ||
|
|
de332c553c | ||
|
|
bfa01c6a6c | ||
|
|
9dbccbee2c | ||
|
|
e44ece0b07 | ||
|
|
b83fcbb1a9 | ||
|
|
1ffdedc7a4 | ||
|
|
3c786f5cd9 | ||
|
|
f105789e12 | ||
|
|
61e90473e6 | ||
|
|
e79c19b4d2 | ||
|
|
578a532395 | ||
|
|
4e9b52b8d6 | ||
|
|
d88596cef1 | ||
|
|
ad16d6e880 | ||
|
|
bbb6f88fae | ||
|
|
d7074ba9ee | ||
|
|
d11cf20452 | ||
|
|
baec1e05f7 | ||
|
|
003ff56cbc | ||
|
|
cbe1b9e81b | ||
|
|
0249a08201 | ||
|
|
bfaab82e26 | ||
|
|
9f64dad687 | ||
|
|
9514a67270 | ||
|
|
59d94f397b | ||
|
|
ffb372aec7 | ||
|
|
06872f7f03 | ||
|
|
5a640daf92 | ||
|
|
f9b2f711c0 | ||
|
|
31cca98656 | ||
|
|
cf2630148a | ||
|
|
55d399b75a | ||
|
|
fed738df42 | ||
|
|
6ef91357b2 | ||
|
|
36bb704c76 | ||
|
|
71514b5989 | ||
|
|
3be7f76763 | ||
|
|
83fcf7cd08 | ||
|
|
4a29fbda69 | ||
|
|
1e67c5c02c | ||
|
|
ca3cbc3f31 | ||
|
|
84db915d91 | ||
|
|
5ce53dc175 | ||
|
|
e07a6b7e77 | ||
|
|
7b82b3582f | ||
|
|
f29c7b1f15 | ||
|
|
04f2cc0fa9 | ||
|
|
c77c15e369 | ||
|
|
11d3488026 | ||
|
|
d6ab555b81 | ||
|
|
c788c7e0c1 | ||
|
|
1d21d27dda | ||
|
|
dde8837fb2 | ||
|
|
947fbe33ee | ||
|
|
2eb18f18b2 | ||
|
|
0b2900d162 | ||
|
|
578c6c2ad2 | ||
|
|
ed26975926 | ||
|
|
0e407351a4 | ||
|
|
6a672d4dd3 | ||
|
|
0cb4b2f1a6 | ||
|
|
244f17d57b | ||
|
|
65f7c54208 | ||
|
|
e2bfa9a590 | ||
|
|
4ea0288eaa | ||
|
|
336163aaa0 | ||
|
|
8a94de1c1b | ||
|
|
87c015fbcf | ||
|
|
50ea82310d | ||
|
|
c16ef0a13e | ||
|
|
e51ac1fd1c | ||
|
|
c2aefc2eda | ||
|
|
54846cd110 | ||
|
|
a742aa22d4 | ||
|
|
dfd5573c19 | ||
|
|
8a8cb6f0af | ||
|
|
7e19515d7b | ||
|
|
f645036d71 | ||
|
|
9145521dfa | ||
|
|
673f821acb | ||
|
|
1bcb85f1c1 | ||
|
|
0dd5a5c794 | ||
|
|
f50ef65dd5 | ||
|
|
24e9da6a09 | ||
|
|
0f6647b49a | ||
|
|
536b1cb8ab | ||
|
|
6d33d4fb34 | ||
|
|
454c163788 | ||
|
|
958b3d11dc | ||
|
|
1366b9132e | ||
|
|
27593796fb | ||
|
|
b8460b6001 | ||
|
|
65089ead4c | ||
|
|
717fb47c17 | ||
|
|
106343a6a2 | ||
|
|
726a6fa98d | ||
|
|
a435a3e1c3 |
4
.codecov.yml
Normal file
4
.codecov.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# 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
|
||||||
@ -5,19 +5,12 @@
|
|||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
. .evergreen/utils.sh
|
# Set up the virtual env.
|
||||||
|
. .evergreen/scripts/setup-dev-env.sh
|
||||||
|
uv sync --group coverage
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
if [ -z "${PYTHON_BINARY:-}" ]; then
|
|
||||||
PYTHON_BINARY=$(find_python3)
|
|
||||||
fi
|
|
||||||
|
|
||||||
createvirtualenv "$PYTHON_BINARY" covenv
|
|
||||||
# Keep in sync with run-tests.sh
|
|
||||||
# coverage >=5 is needed for relative_files=true.
|
|
||||||
pip install -q "coverage[toml]>=5,<=7.5"
|
|
||||||
|
|
||||||
pip list
|
|
||||||
ls -la coverage/
|
ls -la coverage/
|
||||||
|
|
||||||
python -m coverage combine coverage/coverage.*
|
coverage combine coverage/coverage.*
|
||||||
python -m coverage html -d htmlcov
|
coverage html -d htmlcov
|
||||||
|
|||||||
@ -38,7 +38,28 @@ post:
|
|||||||
# Disabled, causing timeouts
|
# Disabled, causing timeouts
|
||||||
# - func: "upload working dir"
|
# - func: "upload working dir"
|
||||||
- func: "teardown system"
|
- func: "teardown system"
|
||||||
|
- func: "upload codecov"
|
||||||
- func: "upload coverage"
|
- func: "upload coverage"
|
||||||
- func: "upload mo artifacts"
|
- func: "upload mo artifacts"
|
||||||
- func: "upload test results"
|
- func: "upload test results"
|
||||||
- func: "cleanup"
|
- func: "cleanup"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: resync_specs
|
||||||
|
commands:
|
||||||
|
- command: subprocess.exec
|
||||||
|
params:
|
||||||
|
binary: bash
|
||||||
|
include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN]
|
||||||
|
args:
|
||||||
|
- .evergreen/scripts/resync-all-specs.sh
|
||||||
|
working_dir: src
|
||||||
|
|
||||||
|
buildvariants:
|
||||||
|
- name: resync_specs
|
||||||
|
display_name: "Resync Specs"
|
||||||
|
run_on: rhel80-small
|
||||||
|
cron: '0 16 * * MON'
|
||||||
|
patchable: true
|
||||||
|
tasks:
|
||||||
|
- name: resync_specs
|
||||||
|
|||||||
@ -101,8 +101,8 @@ functions:
|
|||||||
- AUTH
|
- AUTH
|
||||||
- SSL
|
- SSL
|
||||||
- ORCHESTRATION_FILE
|
- ORCHESTRATION_FILE
|
||||||
- PYTHON_BINARY
|
- UV_PYTHON
|
||||||
- PYTHON_VERSION
|
- TOOLCHAIN_VERSION
|
||||||
- STORAGE_ENGINE
|
- STORAGE_ENGINE
|
||||||
- REQUIRE_API_VERSION
|
- REQUIRE_API_VERSION
|
||||||
- DRIVERS_TOOLS
|
- DRIVERS_TOOLS
|
||||||
@ -134,10 +134,10 @@ functions:
|
|||||||
- AWS_SECRET_ACCESS_KEY
|
- AWS_SECRET_ACCESS_KEY
|
||||||
- AWS_SESSION_TOKEN
|
- AWS_SESSION_TOKEN
|
||||||
- COVERAGE
|
- COVERAGE
|
||||||
- PYTHON_BINARY
|
- UV_PYTHON
|
||||||
- LIBMONGOCRYPT_URL
|
- LIBMONGOCRYPT_URL
|
||||||
- MONGODB_URI
|
- MONGODB_URI
|
||||||
- PYTHON_VERSION
|
- TOOLCHAIN_VERSION
|
||||||
- DISABLE_TEST_COMMANDS
|
- DISABLE_TEST_COMMANDS
|
||||||
- GREEN_FRAMEWORK
|
- GREEN_FRAMEWORK
|
||||||
- NO_EXT
|
- NO_EXT
|
||||||
@ -145,11 +145,13 @@ functions:
|
|||||||
- MONGODB_API_VERSION
|
- MONGODB_API_VERSION
|
||||||
- REQUIRE_API_VERSION
|
- REQUIRE_API_VERSION
|
||||||
- DEBUG_LOG
|
- DEBUG_LOG
|
||||||
|
- DISABLE_FLAKY
|
||||||
- ORCHESTRATION_FILE
|
- ORCHESTRATION_FILE
|
||||||
- OCSP_SERVER_TYPE
|
- OCSP_SERVER_TYPE
|
||||||
- VERSION
|
- VERSION
|
||||||
- IS_WIN32
|
- IS_WIN32
|
||||||
- REQUIRE_FIPS
|
- REQUIRE_FIPS
|
||||||
|
- TEST_MIN_DEPS
|
||||||
type: test
|
type: test
|
||||||
- command: subprocess.exec
|
- command: subprocess.exec
|
||||||
params:
|
params:
|
||||||
@ -237,6 +239,40 @@ functions:
|
|||||||
working_dir: src
|
working_dir: src
|
||||||
type: test
|
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
|
||||||
upload coverage:
|
upload coverage:
|
||||||
- command: ec2.assume_role
|
- command: ec2.assume_role
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,27 +1,17 @@
|
|||||||
buildvariants:
|
buildvariants:
|
||||||
# Alternative hosts tests
|
# Alternative hosts tests
|
||||||
- name: openssl-1.0.2-rhel7-v5.0-python3.9
|
|
||||||
tasks:
|
|
||||||
- name: .test-no-toolchain
|
|
||||||
display_name: OpenSSL 1.0.2 RHEL7 v5.0 Python3.9
|
|
||||||
run_on:
|
|
||||||
- rhel79-small
|
|
||||||
batchtime: 10080
|
|
||||||
expansions:
|
|
||||||
VERSION: "5.0"
|
|
||||||
PYTHON_VERSION: "3.9"
|
|
||||||
PYTHON_BINARY: /opt/python/3.9/bin/python3
|
|
||||||
- name: other-hosts-rhel9-fips-latest
|
- name: other-hosts-rhel9-fips-latest
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-no-toolchain
|
- name: .test-no-toolchain
|
||||||
display_name: Other hosts RHEL9-FIPS latest
|
display_name: Other hosts RHEL9-FIPS latest
|
||||||
run_on:
|
run_on:
|
||||||
- rhel92-fips
|
- rhel92-fips
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
VERSION: latest
|
VERSION: latest
|
||||||
NO_EXT: "1"
|
NO_EXT: "1"
|
||||||
REQUIRE_FIPS: "1"
|
REQUIRE_FIPS: "1"
|
||||||
|
UV_PYTHON: /usr/bin/python3.11
|
||||||
tags: []
|
tags: []
|
||||||
- name: other-hosts-rhel8-zseries-latest
|
- name: other-hosts-rhel8-zseries-latest
|
||||||
tasks:
|
tasks:
|
||||||
@ -29,7 +19,7 @@ buildvariants:
|
|||||||
display_name: Other hosts RHEL8-zseries latest
|
display_name: Other hosts RHEL8-zseries latest
|
||||||
run_on:
|
run_on:
|
||||||
- rhel8-zseries-small
|
- rhel8-zseries-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
VERSION: latest
|
VERSION: latest
|
||||||
NO_EXT: "1"
|
NO_EXT: "1"
|
||||||
@ -40,7 +30,7 @@ buildvariants:
|
|||||||
display_name: Other hosts RHEL8-POWER8 latest
|
display_name: Other hosts RHEL8-POWER8 latest
|
||||||
run_on:
|
run_on:
|
||||||
- rhel8-power-small
|
- rhel8-power-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
VERSION: latest
|
VERSION: latest
|
||||||
NO_EXT: "1"
|
NO_EXT: "1"
|
||||||
@ -51,7 +41,7 @@ buildvariants:
|
|||||||
display_name: Other hosts RHEL8-arm64 latest
|
display_name: Other hosts RHEL8-arm64 latest
|
||||||
run_on:
|
run_on:
|
||||||
- rhel82-arm64-small
|
- rhel82-arm64-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
VERSION: latest
|
VERSION: latest
|
||||||
NO_EXT: "1"
|
NO_EXT: "1"
|
||||||
@ -62,7 +52,7 @@ buildvariants:
|
|||||||
display_name: Other hosts Amazon2023 latest
|
display_name: Other hosts Amazon2023 latest
|
||||||
run_on:
|
run_on:
|
||||||
- amazon2023-arm64-latest-large-m8g
|
- amazon2023-arm64-latest-large-m8g
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
VERSION: latest
|
VERSION: latest
|
||||||
NO_EXT: "1"
|
NO_EXT: "1"
|
||||||
@ -75,41 +65,39 @@ buildvariants:
|
|||||||
display_name: Atlas connect RHEL8
|
display_name: Atlas connect RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
tags: [pr]
|
|
||||||
|
|
||||||
# Atlas data lake tests
|
|
||||||
- name: atlas-data-lake-ubuntu-22
|
|
||||||
tasks:
|
|
||||||
- name: .test-no-orchestration
|
|
||||||
display_name: Atlas Data Lake Ubuntu-22
|
|
||||||
run_on:
|
|
||||||
- ubuntu2204-small
|
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: data_lake
|
TEST_NAME: atlas_connect
|
||||||
tags: [pr]
|
tags: [pr]
|
||||||
|
|
||||||
# Aws auth tests
|
# Aws auth tests
|
||||||
- name: auth-aws-ubuntu-20
|
- name: auth-aws-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
- name: .auth-aws
|
- name: .auth-aws
|
||||||
display_name: Auth AWS Ubuntu-20
|
display_name: Auth AWS RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- ubuntu2004-small
|
- rhel87-small
|
||||||
tags: []
|
tags: []
|
||||||
- name: auth-aws-win64
|
- name: auth-aws-win64
|
||||||
tasks:
|
tasks:
|
||||||
- name: .auth-aws !.auth-aws-ecs
|
- name: .auth-aws
|
||||||
display_name: Auth AWS Win64
|
display_name: Auth AWS Win64
|
||||||
run_on:
|
run_on:
|
||||||
- windows-64-vsMulti-small
|
- windows-2022-latest-small
|
||||||
tags: []
|
tags: []
|
||||||
- name: auth-aws-macos
|
- name: auth-aws-macos
|
||||||
tasks:
|
tasks:
|
||||||
- name: .auth-aws !.auth-aws-web-identity !.auth-aws-ecs !.auth-aws-ec2
|
- name: .auth-aws !.auth-aws-web-identity !.auth-aws-ec2
|
||||||
display_name: Auth AWS macOS
|
display_name: Auth AWS macOS
|
||||||
run_on:
|
run_on:
|
||||||
- macos-14
|
- macos-14
|
||||||
tags: [pr]
|
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
|
# Aws lambda tests
|
||||||
- name: faas-lambda
|
- name: faas-lambda
|
||||||
@ -146,12 +134,21 @@ buildvariants:
|
|||||||
COMPRESSOR: zlib
|
COMPRESSOR: zlib
|
||||||
- name: compression-zstd-rhel8
|
- name: compression-zstd-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-standard !.server-4.0
|
- name: .test-standard !.server-4.2
|
||||||
display_name: Compression zstd RHEL8
|
display_name: Compression zstd RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
expansions:
|
expansions:
|
||||||
COMPRESSOR: zstd
|
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
|
# Coverage report tests
|
||||||
- name: coverage-report
|
- name: coverage-report
|
||||||
@ -162,17 +159,16 @@ buildvariants:
|
|||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
|
||||||
# Disable test commands tests
|
# Disable test commands tests
|
||||||
- name: disable-test-commands-rhel8-python3.9
|
- name: disable-test-commands-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-standard .server-latest
|
- name: .test-standard .server-latest
|
||||||
display_name: Disable test commands RHEL8 Python3.9
|
display_name: Disable test commands RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
expansions:
|
expansions:
|
||||||
AUTH: auth
|
AUTH: auth
|
||||||
SSL: ssl
|
SSL: ssl
|
||||||
DISABLE_TEST_COMMANDS: "1"
|
DISABLE_TEST_COMMANDS: "1"
|
||||||
PYTHON_BINARY: /opt/python/3.9/bin/python3
|
|
||||||
|
|
||||||
# Doctests tests
|
# Doctests tests
|
||||||
- name: doctests-rhel8
|
- name: doctests-rhel8
|
||||||
@ -191,7 +187,7 @@ buildvariants:
|
|||||||
display_name: Encryption RHEL8
|
display_name: Encryption RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: encryption
|
TEST_NAME: encryption
|
||||||
tags: [encryption_tag]
|
tags: [encryption_tag]
|
||||||
@ -201,7 +197,7 @@ buildvariants:
|
|||||||
display_name: Encryption macOS
|
display_name: Encryption macOS
|
||||||
run_on:
|
run_on:
|
||||||
- macos-14
|
- macos-14
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: encryption
|
TEST_NAME: encryption
|
||||||
tags: [encryption_tag]
|
tags: [encryption_tag]
|
||||||
@ -210,8 +206,8 @@ buildvariants:
|
|||||||
- name: .test-non-standard !.pypy
|
- name: .test-non-standard !.pypy
|
||||||
display_name: Encryption Win64
|
display_name: Encryption Win64
|
||||||
run_on:
|
run_on:
|
||||||
- windows-64-vsMulti-small
|
- windows-2022-latest-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: encryption
|
TEST_NAME: encryption
|
||||||
tags: [encryption_tag]
|
tags: [encryption_tag]
|
||||||
@ -221,7 +217,7 @@ buildvariants:
|
|||||||
display_name: Encryption crypt_shared RHEL8
|
display_name: Encryption crypt_shared RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: encryption
|
TEST_NAME: encryption
|
||||||
TEST_CRYPT_SHARED: "true"
|
TEST_CRYPT_SHARED: "true"
|
||||||
@ -232,7 +228,7 @@ buildvariants:
|
|||||||
display_name: Encryption crypt_shared macOS
|
display_name: Encryption crypt_shared macOS
|
||||||
run_on:
|
run_on:
|
||||||
- macos-14
|
- macos-14
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: encryption
|
TEST_NAME: encryption
|
||||||
TEST_CRYPT_SHARED: "true"
|
TEST_CRYPT_SHARED: "true"
|
||||||
@ -242,8 +238,8 @@ buildvariants:
|
|||||||
- name: .test-non-standard !.pypy
|
- name: .test-non-standard !.pypy
|
||||||
display_name: Encryption crypt_shared Win64
|
display_name: Encryption crypt_shared Win64
|
||||||
run_on:
|
run_on:
|
||||||
- windows-64-vsMulti-small
|
- windows-2022-latest-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: encryption
|
TEST_NAME: encryption
|
||||||
TEST_CRYPT_SHARED: "true"
|
TEST_CRYPT_SHARED: "true"
|
||||||
@ -254,7 +250,7 @@ buildvariants:
|
|||||||
display_name: Encryption PyOpenSSL RHEL8
|
display_name: Encryption PyOpenSSL RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: encryption
|
TEST_NAME: encryption
|
||||||
SUB_TEST_NAME: pyopenssl
|
SUB_TEST_NAME: pyopenssl
|
||||||
@ -263,7 +259,7 @@ buildvariants:
|
|||||||
# Enterprise auth tests
|
# Enterprise auth tests
|
||||||
- name: auth-enterprise-rhel8
|
- name: auth-enterprise-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-non-standard .auth
|
- name: .test-standard-auth .auth !.free-threaded
|
||||||
display_name: Auth Enterprise RHEL8
|
display_name: Auth Enterprise RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
@ -272,7 +268,7 @@ buildvariants:
|
|||||||
AUTH: auth
|
AUTH: auth
|
||||||
- name: auth-enterprise-macos
|
- name: auth-enterprise-macos
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-non-standard !.pypy .auth
|
- name: .test-standard-auth !.pypy .auth !.free-threaded
|
||||||
display_name: Auth Enterprise macOS
|
display_name: Auth Enterprise macOS
|
||||||
run_on:
|
run_on:
|
||||||
- macos-14
|
- macos-14
|
||||||
@ -281,64 +277,23 @@ buildvariants:
|
|||||||
AUTH: auth
|
AUTH: auth
|
||||||
- name: auth-enterprise-win64
|
- name: auth-enterprise-win64
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-non-standard !.pypy .auth
|
- name: .test-standard-auth !.pypy .auth !.free-threaded
|
||||||
display_name: Auth Enterprise Win64
|
display_name: Auth Enterprise Win64
|
||||||
run_on:
|
run_on:
|
||||||
- windows-64-vsMulti-small
|
- windows-2022-latest-small
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: enterprise_auth
|
TEST_NAME: enterprise_auth
|
||||||
AUTH: auth
|
AUTH: auth
|
||||||
|
|
||||||
# Free threaded tests
|
|
||||||
- name: free-threaded-rhel8-python3.13t
|
|
||||||
tasks:
|
|
||||||
- name: .free-threading
|
|
||||||
display_name: Free-threaded RHEL8 Python3.13t
|
|
||||||
run_on:
|
|
||||||
- rhel87-small
|
|
||||||
expansions:
|
|
||||||
PYTHON_BINARY: /opt/python/3.13t/bin/python3
|
|
||||||
tags: [pr]
|
|
||||||
- name: free-threaded-macos-python3.13t
|
|
||||||
tasks:
|
|
||||||
- name: .free-threading
|
|
||||||
display_name: Free-threaded macOS Python3.13t
|
|
||||||
run_on:
|
|
||||||
- macos-14
|
|
||||||
expansions:
|
|
||||||
PYTHON_BINARY: /Library/Frameworks/PythonT.Framework/Versions/3.13/bin/python3t
|
|
||||||
tags: []
|
|
||||||
- name: free-threaded-macos-arm64-python3.13t
|
|
||||||
tasks:
|
|
||||||
- name: .free-threading
|
|
||||||
display_name: Free-threaded macOS Arm64 Python3.13t
|
|
||||||
run_on:
|
|
||||||
- macos-14-arm64
|
|
||||||
expansions:
|
|
||||||
PYTHON_BINARY: /Library/Frameworks/PythonT.Framework/Versions/3.13/bin/python3t
|
|
||||||
tags: []
|
|
||||||
|
|
||||||
# Green framework tests
|
# Green framework tests
|
||||||
- name: green-eventlet-rhel8
|
|
||||||
tasks:
|
|
||||||
- name: .test-standard .standalone-noauth-nossl .python-3.9
|
|
||||||
display_name: Green Eventlet RHEL8
|
|
||||||
run_on:
|
|
||||||
- rhel87-small
|
|
||||||
expansions:
|
|
||||||
GREEN_FRAMEWORK: eventlet
|
|
||||||
AUTH: auth
|
|
||||||
SSL: ssl
|
|
||||||
- name: green-gevent-rhel8
|
- name: green-gevent-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-standard .standalone-noauth-nossl
|
- name: .test-standard .sync !.free-threaded
|
||||||
display_name: Green Gevent RHEL8
|
display_name: Green Gevent RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
expansions:
|
expansions:
|
||||||
GREEN_FRAMEWORK: gevent
|
GREEN_FRAMEWORK: gevent
|
||||||
AUTH: auth
|
|
||||||
SSL: ssl
|
|
||||||
|
|
||||||
# Import time tests
|
# Import time tests
|
||||||
- name: import-time
|
- name: import-time
|
||||||
@ -352,10 +307,10 @@ buildvariants:
|
|||||||
- name: kms
|
- name: kms
|
||||||
tasks:
|
tasks:
|
||||||
- name: test-gcpkms
|
- name: test-gcpkms
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
- name: test-gcpkms-fail
|
- name: test-gcpkms-fail
|
||||||
- name: test-azurekms
|
- name: test-azurekms
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
- name: test-azurekms-fail
|
- name: test-azurekms-fail
|
||||||
display_name: KMS
|
display_name: KMS
|
||||||
run_on:
|
run_on:
|
||||||
@ -372,10 +327,18 @@ buildvariants:
|
|||||||
display_name: Load Balancer
|
display_name: Load Balancer
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
TEST_NAME: load_balancer
|
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
|
# Mockupdb tests
|
||||||
- name: mockupdb-rhel8
|
- name: mockupdb-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
@ -404,6 +367,8 @@ buildvariants:
|
|||||||
display_name: No C Ext RHEL8
|
display_name: No C Ext RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
NO_EXT: "1"
|
||||||
|
|
||||||
# No server tests
|
# No server tests
|
||||||
- name: no-server-rhel8
|
- name: no-server-rhel8
|
||||||
@ -428,7 +393,7 @@ buildvariants:
|
|||||||
- name: .ocsp-rsa !.ocsp-staple .4.4
|
- name: .ocsp-rsa !.ocsp-staple .4.4
|
||||||
display_name: OCSP Win64
|
display_name: OCSP Win64
|
||||||
run_on:
|
run_on:
|
||||||
- windows-64-vsMulti-small
|
- windows-2022-latest-small
|
||||||
batchtime: 10080
|
batchtime: 10080
|
||||||
- name: ocsp-macos
|
- name: ocsp-macos
|
||||||
tasks:
|
tasks:
|
||||||
@ -446,14 +411,16 @@ buildvariants:
|
|||||||
display_name: Auth OIDC Ubuntu-22
|
display_name: Auth OIDC Ubuntu-22
|
||||||
run_on:
|
run_on:
|
||||||
- ubuntu2204-small
|
- ubuntu2204-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
- name: auth-oidc-local-ubuntu-22
|
- name: auth-oidc-local-ubuntu-22
|
||||||
tasks:
|
tasks:
|
||||||
- name: "!.auth_oidc_remote .auth_oidc"
|
- name: "!.auth_oidc_remote .auth_oidc"
|
||||||
display_name: Auth OIDC Local Ubuntu-22
|
display_name: Auth OIDC Local Ubuntu-22
|
||||||
run_on:
|
run_on:
|
||||||
- ubuntu2204-small
|
- ubuntu2204-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
|
expansions:
|
||||||
|
COVERAGE: "1"
|
||||||
tags: [pr]
|
tags: [pr]
|
||||||
- name: auth-oidc-macos
|
- name: auth-oidc-macos
|
||||||
tasks:
|
tasks:
|
||||||
@ -461,14 +428,14 @@ buildvariants:
|
|||||||
display_name: Auth OIDC macOS
|
display_name: Auth OIDC macOS
|
||||||
run_on:
|
run_on:
|
||||||
- macos-14
|
- macos-14
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
- name: auth-oidc-win64
|
- name: auth-oidc-win64
|
||||||
tasks:
|
tasks:
|
||||||
- name: "!.auth_oidc_remote .auth_oidc"
|
- name: "!.auth_oidc_remote .auth_oidc"
|
||||||
display_name: Auth OIDC Win64
|
display_name: Auth OIDC Win64
|
||||||
run_on:
|
run_on:
|
||||||
- windows-64-vsMulti-small
|
- windows-2022-latest-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
|
|
||||||
# Perf tests
|
# Perf tests
|
||||||
- name: performance-benchmarks
|
- name: performance-benchmarks
|
||||||
@ -477,7 +444,7 @@ buildvariants:
|
|||||||
display_name: Performance Benchmarks
|
display_name: Performance Benchmarks
|
||||||
run_on:
|
run_on:
|
||||||
- rhel90-dbx-perf-large
|
- rhel90-dbx-perf-large
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
|
|
||||||
# Pyopenssl tests
|
# Pyopenssl tests
|
||||||
- name: pyopenssl-rhel8
|
- name: pyopenssl-rhel8
|
||||||
@ -487,7 +454,7 @@ buildvariants:
|
|||||||
display_name: PyOpenSSL RHEL8
|
display_name: PyOpenSSL RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
SUB_TEST_NAME: pyopenssl
|
SUB_TEST_NAME: pyopenssl
|
||||||
- name: pyopenssl-macos
|
- name: pyopenssl-macos
|
||||||
@ -497,7 +464,7 @@ buildvariants:
|
|||||||
display_name: PyOpenSSL macOS
|
display_name: PyOpenSSL macOS
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
SUB_TEST_NAME: pyopenssl
|
SUB_TEST_NAME: pyopenssl
|
||||||
- name: pyopenssl-win64
|
- name: pyopenssl-win64
|
||||||
@ -506,35 +473,28 @@ buildvariants:
|
|||||||
- name: .test-standard !.pypy .async .replica_set-noauth-ssl
|
- name: .test-standard !.pypy .async .replica_set-noauth-ssl
|
||||||
display_name: PyOpenSSL Win64
|
display_name: PyOpenSSL Win64
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- windows-2022-latest-small
|
||||||
batchtime: 10080
|
batchtime: 1440
|
||||||
expansions:
|
expansions:
|
||||||
SUB_TEST_NAME: pyopenssl
|
SUB_TEST_NAME: pyopenssl
|
||||||
|
|
||||||
# Search index tests
|
# Search index tests
|
||||||
- name: search-index-helpers-rhel8-python3.9
|
- name: search-index-helpers-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
- name: .search_index
|
- name: .search_index
|
||||||
display_name: Search Index Helpers RHEL8 Python3.9
|
display_name: Search Index Helpers RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
expansions:
|
|
||||||
PYTHON_BINARY: /opt/python/3.9/bin/python3
|
|
||||||
|
|
||||||
# Server version tests
|
# Server version tests
|
||||||
- name: mongodb-v4.0
|
|
||||||
tasks:
|
|
||||||
- name: .server-version
|
|
||||||
display_name: "* MongoDB v4.0"
|
|
||||||
run_on:
|
|
||||||
- rhel87-small
|
|
||||||
tags: [coverage_tag]
|
|
||||||
- name: mongodb-v4.2
|
- name: mongodb-v4.2
|
||||||
tasks:
|
tasks:
|
||||||
- name: .server-version
|
- name: .server-version
|
||||||
display_name: "* MongoDB v4.2"
|
display_name: "* MongoDB v4.2"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: "4.2"
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
- name: mongodb-v4.4
|
- name: mongodb-v4.4
|
||||||
tasks:
|
tasks:
|
||||||
@ -542,6 +502,8 @@ buildvariants:
|
|||||||
display_name: "* MongoDB v4.4"
|
display_name: "* MongoDB v4.4"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: "4.4"
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
- name: mongodb-v5.0
|
- name: mongodb-v5.0
|
||||||
tasks:
|
tasks:
|
||||||
@ -549,6 +511,8 @@ buildvariants:
|
|||||||
display_name: "* MongoDB v5.0"
|
display_name: "* MongoDB v5.0"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: "5.0"
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
- name: mongodb-v6.0
|
- name: mongodb-v6.0
|
||||||
tasks:
|
tasks:
|
||||||
@ -556,6 +520,8 @@ buildvariants:
|
|||||||
display_name: "* MongoDB v6.0"
|
display_name: "* MongoDB v6.0"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: "6.0"
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
- name: mongodb-v7.0
|
- name: mongodb-v7.0
|
||||||
tasks:
|
tasks:
|
||||||
@ -563,6 +529,8 @@ buildvariants:
|
|||||||
display_name: "* MongoDB v7.0"
|
display_name: "* MongoDB v7.0"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: "7.0"
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
- name: mongodb-v8.0
|
- name: mongodb-v8.0
|
||||||
tasks:
|
tasks:
|
||||||
@ -570,6 +538,8 @@ buildvariants:
|
|||||||
display_name: "* MongoDB v8.0"
|
display_name: "* MongoDB v8.0"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: "8.0"
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
- name: mongodb-rapid
|
- name: mongodb-rapid
|
||||||
tasks:
|
tasks:
|
||||||
@ -577,6 +547,8 @@ buildvariants:
|
|||||||
display_name: "* MongoDB rapid"
|
display_name: "* MongoDB rapid"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: rapid
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
- name: mongodb-latest
|
- name: mongodb-latest
|
||||||
tasks:
|
tasks:
|
||||||
@ -584,28 +556,10 @@ buildvariants:
|
|||||||
display_name: "* MongoDB latest"
|
display_name: "* MongoDB latest"
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- rhel87-small
|
||||||
|
expansions:
|
||||||
|
VERSION: latest
|
||||||
tags: [coverage_tag]
|
tags: [coverage_tag]
|
||||||
|
|
||||||
# Serverless tests
|
|
||||||
- name: serverless-rhel8-python3.9
|
|
||||||
tasks:
|
|
||||||
- name: .serverless
|
|
||||||
display_name: Serverless RHEL8 Python3.9
|
|
||||||
run_on:
|
|
||||||
- rhel87-small
|
|
||||||
batchtime: 10080
|
|
||||||
expansions:
|
|
||||||
PYTHON_BINARY: /opt/python/3.9/bin/python3
|
|
||||||
- name: serverless-rhel8-python3.13
|
|
||||||
tasks:
|
|
||||||
- name: .serverless
|
|
||||||
display_name: Serverless RHEL8 Python3.13
|
|
||||||
run_on:
|
|
||||||
- rhel87-small
|
|
||||||
batchtime: 10080
|
|
||||||
expansions:
|
|
||||||
PYTHON_BINARY: /opt/python/3.13/bin/python3
|
|
||||||
|
|
||||||
# Stable api tests
|
# Stable api tests
|
||||||
- name: stable-api-require-v1-rhel8-auth
|
- name: stable-api-require-v1-rhel8-auth
|
||||||
tasks:
|
tasks:
|
||||||
@ -661,9 +615,10 @@ buildvariants:
|
|||||||
- name: test-win64
|
- name: test-win64
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-standard !.pypy
|
- name: .test-standard !.pypy
|
||||||
|
- name: .test-no-orchestration !.pypy
|
||||||
display_name: "* Test Win64"
|
display_name: "* Test Win64"
|
||||||
run_on:
|
run_on:
|
||||||
- windows-64-vsMulti-small
|
- windows-2022-latest-small
|
||||||
tags: [standard-non-linux]
|
tags: [standard-non-linux]
|
||||||
- name: test-win32
|
- name: test-win32
|
||||||
tasks:
|
tasks:
|
||||||
@ -684,11 +639,42 @@ buildvariants:
|
|||||||
- rhel87-small
|
- rhel87-small
|
||||||
expansions:
|
expansions:
|
||||||
STORAGE_ENGINE: inmemory
|
STORAGE_ENGINE: inmemory
|
||||||
- name: storage-mmapv1-rhel8
|
|
||||||
|
# Test numpy tests
|
||||||
|
- name: test-numpy-rhel8
|
||||||
tasks:
|
tasks:
|
||||||
- name: .test-standard !.sharded_cluster-auth-ssl .server-4.0
|
- name: .test-numpy
|
||||||
display_name: Storage MMAPv1 RHEL8
|
display_name: Test Numpy RHEL8
|
||||||
run_on:
|
run_on:
|
||||||
- rhel87-small
|
- 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:
|
expansions:
|
||||||
STORAGE_ENGINE: mmapv1
|
IS_WIN32: "1"
|
||||||
|
tags: [binary, vector]
|
||||||
|
|||||||
56
.evergreen/remove-unimplemented-tests.sh
Executable file
56
.evergreen/remove-unimplemented-tests.sh
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
PYMONGO=$(dirname "$(cd "$(dirname "$0")" || exit; pwd)")
|
||||||
|
|
||||||
|
rm $PYMONGO/test/transactions/legacy/errors-client.json # PYTHON-1894
|
||||||
|
rm $PYMONGO/test/connection_monitoring/wait-queue-fairness.json # PYTHON-1873
|
||||||
|
rm $PYMONGO/test/discovery_and_monitoring/unified/pool-clear-application-error.json # PYTHON-4918
|
||||||
|
rm $PYMONGO/test/discovery_and_monitoring/unified/pool-clear-checkout-error.json # PYTHON-4918
|
||||||
|
rm $PYMONGO/test/discovery_and_monitoring/unified/pool-clear-min-pool-size-error.json # PYTHON-4918
|
||||||
|
rm $PYMONGO/test/client-side-encryption/spec/unified/client-bulkWrite-qe.json # PYTHON-4929
|
||||||
|
|
||||||
|
# Python doesn't implement DRIVERS-3064
|
||||||
|
rm $PYMONGO/test/collection_management/listCollections-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/aggregate-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/bulkWrite-deleteMany-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/bulkWrite-deleteOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/bulkWrite-replaceOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/bulkWrite-updateMany-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/bulkWrite-updateOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/client-bulkWrite-delete-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/client-bulkWrite-replaceOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/client-bulkWrite-update-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/count-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/countDocuments-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/db-aggregate-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/deleteMany-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/deleteOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/distinct-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/estimatedDocumentCount-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/find-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/findOneAndDelete-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/findOneAndReplace-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/findOneAndUpdate-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/insertMany-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/insertOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/replaceOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/updateMany-rawdata.json
|
||||||
|
rm $PYMONGO/test/crud/unified/updateOne-rawdata.json
|
||||||
|
rm $PYMONGO/test/index_management/index-rawdata.json
|
||||||
|
|
||||||
|
# PyMongo does not support modifyCollection
|
||||||
|
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
|
||||||
|
|
||||||
|
# PYTHON-3359 - Remove Database and Collection level timeout override
|
||||||
|
rm $PYMONGO/test/csot/override-collection-timeoutMS.json
|
||||||
|
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"
|
||||||
@ -45,9 +45,12 @@ then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure the JSON files are up to date.
|
# Ensure the JSON files are up to date.
|
||||||
cd $SPECS/source
|
if ! [ -n "${CI:-}" ]
|
||||||
make
|
then
|
||||||
cd -
|
cd $SPECS/source
|
||||||
|
make
|
||||||
|
cd -
|
||||||
|
fi
|
||||||
# cpjson unified-test-format/tests/invalid unified-test-format/invalid
|
# cpjson unified-test-format/tests/invalid unified-test-format/invalid
|
||||||
# * param1: Path to spec tests dir in specifications repo
|
# * param1: Path to spec tests dir in specifications repo
|
||||||
# * param2: Path to where the corresponding tests live in Python.
|
# * param2: Path to where the corresponding tests live in Python.
|
||||||
@ -73,9 +76,6 @@ do
|
|||||||
auth)
|
auth)
|
||||||
cpjson auth/tests/ auth
|
cpjson auth/tests/ auth
|
||||||
;;
|
;;
|
||||||
atlas-data-lake-testing|data_lake)
|
|
||||||
cpjson atlas-data-lake-testing/tests/ data_lake
|
|
||||||
;;
|
|
||||||
bson-binary-vector|bson_binary_vector)
|
bson-binary-vector|bson_binary_vector)
|
||||||
cpjson bson-binary-vector/tests/ bson_binary_vector
|
cpjson bson-binary-vector/tests/ bson_binary_vector
|
||||||
;;
|
;;
|
||||||
@ -94,6 +94,9 @@ do
|
|||||||
change-streams|change_streams)
|
change-streams|change_streams)
|
||||||
cpjson change-streams/tests/ change_streams/
|
cpjson change-streams/tests/ change_streams/
|
||||||
;;
|
;;
|
||||||
|
client-backpressure|client_backpressure)
|
||||||
|
cpjson client-backpressure/tests client-backpressure
|
||||||
|
;;
|
||||||
client-side-encryption|csfle|fle)
|
client-side-encryption|csfle|fle)
|
||||||
cpjson client-side-encryption/tests/ client-side-encryption/spec
|
cpjson client-side-encryption/tests/ client-side-encryption/spec
|
||||||
cpjson client-side-encryption/corpus/ client-side-encryption/corpus
|
cpjson client-side-encryption/corpus/ client-side-encryption/corpus
|
||||||
@ -110,7 +113,6 @@ do
|
|||||||
cmap|CMAP|connection-monitoring-and-pooling)
|
cmap|CMAP|connection-monitoring-and-pooling)
|
||||||
cpjson connection-monitoring-and-pooling/tests/logging connection_logging
|
cpjson connection-monitoring-and-pooling/tests/logging connection_logging
|
||||||
cpjson connection-monitoring-and-pooling/tests/cmap-format connection_monitoring
|
cpjson connection-monitoring-and-pooling/tests/cmap-format connection_monitoring
|
||||||
rm $PYMONGO/test/connection_monitoring/wait-queue-fairness.json # PYTHON-1873
|
|
||||||
;;
|
;;
|
||||||
apm|APM|command-monitoring|command_monitoring)
|
apm|APM|command-monitoring|command_monitoring)
|
||||||
cpjson command-logging-and-monitoring/tests/monitoring command_monitoring
|
cpjson command-logging-and-monitoring/tests/monitoring command_monitoring
|
||||||
@ -131,6 +133,9 @@ do
|
|||||||
gridfs)
|
gridfs)
|
||||||
cpjson gridfs/tests gridfs
|
cpjson gridfs/tests gridfs
|
||||||
;;
|
;;
|
||||||
|
handshake)
|
||||||
|
cpjson mongodb-handshake/tests handshake
|
||||||
|
;;
|
||||||
index|index-management)
|
index|index-management)
|
||||||
cpjson index-management/tests index_management
|
cpjson index-management/tests index_management
|
||||||
;;
|
;;
|
||||||
@ -171,7 +176,7 @@ do
|
|||||||
;;
|
;;
|
||||||
server-selection|server_selection)
|
server-selection|server_selection)
|
||||||
cpjson server-selection/tests/ server_selection
|
cpjson server-selection/tests/ server_selection
|
||||||
rm -rf $PYMONGO/test/server_selection/logging
|
rm -rf $PYMONGO/test/server_selection/logging # these tests live in server_selection_logging
|
||||||
cpjson server-selection/tests/logging server_selection_logging
|
cpjson server-selection/tests/logging server_selection_logging
|
||||||
;;
|
;;
|
||||||
server-selection-logging|server_selection_logging)
|
server-selection-logging|server_selection_logging)
|
||||||
@ -183,7 +188,6 @@ do
|
|||||||
transactions|transactions-convenient-api)
|
transactions|transactions-convenient-api)
|
||||||
cpjson transactions/tests/ transactions
|
cpjson transactions/tests/ transactions
|
||||||
cpjson transactions-convenient-api/tests/ transactions-convenient-api
|
cpjson transactions-convenient-api/tests/ transactions-convenient-api
|
||||||
rm $PYMONGO/test/transactions/legacy/errors-client.json # PYTHON-1894
|
|
||||||
;;
|
;;
|
||||||
unified|unified-test-format)
|
unified|unified-test-format)
|
||||||
cpjson unified-test-format/tests/ unified-test-format/
|
cpjson unified-test-format/tests/ unified-test-format/
|
||||||
|
|||||||
@ -19,15 +19,14 @@ fi
|
|||||||
# Now we can safely enable xtrace
|
# Now we can safely enable xtrace
|
||||||
set -o xtrace
|
set -o xtrace
|
||||||
|
|
||||||
# Install python with pip.
|
# Install a c compiler.
|
||||||
PYTHON_VER="python3.9"
|
|
||||||
apt-get -qq update < /dev/null > /dev/null
|
apt-get -qq update < /dev/null > /dev/null
|
||||||
apt-get -qq install $PYTHON_VER $PYTHON_VER-venv build-essential $PYTHON_VER-dev -y < /dev/null > /dev/null
|
apt-get -q install -y build-essential
|
||||||
|
|
||||||
export PYTHON_BINARY=$PYTHON_VER
|
|
||||||
export SET_XTRACE_ON=1
|
export SET_XTRACE_ON=1
|
||||||
cd src
|
cd src
|
||||||
rm -rf .venv
|
rm -rf .venv
|
||||||
rm -f .evergreen/scripts/test-env.sh || true
|
rm -f .evergreen/scripts/test-env.sh || true
|
||||||
|
rm -f .evergreen/scripts/env.sh || true
|
||||||
bash ./.evergreen/just.sh setup-tests auth_aws ecs-remote
|
bash ./.evergreen/just.sh setup-tests auth_aws ecs-remote
|
||||||
bash .evergreen/just.sh run-tests
|
bash .evergreen/just.sh run-tests
|
||||||
|
|||||||
@ -8,7 +8,9 @@ if [ ${OIDC_ENV} == "k8s" ]; then
|
|||||||
SUB_TEST_NAME=$K8S_VARIANT-remote
|
SUB_TEST_NAME=$K8S_VARIANT-remote
|
||||||
else
|
else
|
||||||
SUB_TEST_NAME=$OIDC_ENV-remote
|
SUB_TEST_NAME=$OIDC_ENV-remote
|
||||||
|
sudo apt-get install -y python3-dev build-essential
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bash ./.evergreen/just.sh setup-tests auth_oidc $SUB_TEST_NAME
|
bash ./.evergreen/just.sh setup-tests auth_oidc $SUB_TEST_NAME
|
||||||
bash ./.evergreen/just.sh run-tests "${@:1}"
|
bash ./.evergreen/just.sh run-tests "${@:1}"
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,8 @@ SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0})
|
|||||||
SCRIPT_DIR="$( cd -- "$SCRIPT_DIR" > /dev/null 2>&1 && pwd )"
|
SCRIPT_DIR="$( cd -- "$SCRIPT_DIR" > /dev/null 2>&1 && pwd )"
|
||||||
ROOT_DIR="$(dirname $SCRIPT_DIR)"
|
ROOT_DIR="$(dirname $SCRIPT_DIR)"
|
||||||
|
|
||||||
pushd $ROOT_DIR
|
PREV_DIR=$(pwd)
|
||||||
|
cd $ROOT_DIR
|
||||||
|
|
||||||
# Try to source the env file.
|
# Try to source the env file.
|
||||||
if [ -f $SCRIPT_DIR/scripts/env.sh ]; then
|
if [ -f $SCRIPT_DIR/scripts/env.sh ]; then
|
||||||
@ -25,14 +26,20 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# List the packages.
|
cleanup_tests() {
|
||||||
uv sync ${UV_ARGS} --reinstall
|
# Avoid leaving the lock file in a changed state when we change the resolution type.
|
||||||
uv pip list
|
if [ -n "${TEST_MIN_DEPS:-}" ]; then
|
||||||
|
git checkout uv.lock || true
|
||||||
|
fi
|
||||||
|
cd $PREV_DIR
|
||||||
|
}
|
||||||
|
|
||||||
# Ensure we go back to base environment after the test.
|
trap "cleanup_tests" SIGINT ERR
|
||||||
trap "uv sync" EXIT HUP
|
|
||||||
|
|
||||||
# Start the test runner.
|
# Start the test runner.
|
||||||
uv run ${UV_ARGS} .evergreen/scripts/run_tests.py "$@"
|
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."
|
||||||
|
|
||||||
popd
|
cleanup_tests
|
||||||
|
|||||||
@ -11,11 +11,10 @@ pushd $HERE/../.. >/dev/null
|
|||||||
BASE_SHA="$1"
|
BASE_SHA="$1"
|
||||||
HEAD_SHA="$2"
|
HEAD_SHA="$2"
|
||||||
|
|
||||||
. .evergreen/utils.sh
|
# Set up the virtual env.
|
||||||
|
. $HERE/setup-dev-env.sh
|
||||||
if [ -z "${PYTHON_BINARY:-}" ]; then
|
uv venv --seed
|
||||||
PYTHON_BINARY=$(find_python3)
|
source .venv/bin/activate
|
||||||
fi
|
|
||||||
|
|
||||||
# Use the previous commit if this was not a PR run.
|
# Use the previous commit if this was not a PR run.
|
||||||
if [ "$BASE_SHA" == "$HEAD_SHA" ]; then
|
if [ "$BASE_SHA" == "$HEAD_SHA" ]; then
|
||||||
@ -24,7 +23,6 @@ fi
|
|||||||
|
|
||||||
function get_import_time() {
|
function get_import_time() {
|
||||||
local log_file
|
local log_file
|
||||||
createvirtualenv "$PYTHON_BINARY" import-venv
|
|
||||||
python -m pip install -q ".[aws,encryption,gssapi,ocsp,snappy,zstd]"
|
python -m pip install -q ".[aws,encryption,gssapi,ocsp,snappy,zstd]"
|
||||||
# Import once to cache modules
|
# Import once to cache modules
|
||||||
python -c "import pymongo"
|
python -c "import pymongo"
|
||||||
|
|||||||
50
.evergreen/scripts/create-spec-pr.sh
Executable file
50
.evergreen/scripts/create-spec-pr.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
tools="$(realpath -s "../drivers-tools")"
|
||||||
|
pushd $tools/.evergreen/github_app || exit
|
||||||
|
|
||||||
|
owner="mongodb"
|
||||||
|
repo="mongo-python-driver"
|
||||||
|
|
||||||
|
# Bootstrap the app.
|
||||||
|
echo "bootstrapping"
|
||||||
|
source utils.sh
|
||||||
|
bootstrap drivers/comment-bot
|
||||||
|
|
||||||
|
# Run the app.
|
||||||
|
source ./secrets-export.sh
|
||||||
|
|
||||||
|
# Get a github access token for the git checkout.
|
||||||
|
echo "Getting github token..."
|
||||||
|
|
||||||
|
token=$(bash ./get-access-token.sh $repo $owner)
|
||||||
|
if [ -z "${token}" ]; then
|
||||||
|
echo "Failed to get github access token!"
|
||||||
|
popd || exit
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Getting github token... done."
|
||||||
|
popd || exit
|
||||||
|
|
||||||
|
# Make the git checkout and create a new branch.
|
||||||
|
echo "Creating the git checkout..."
|
||||||
|
branch="spec-resync-"$(date '+%m-%d-%Y')
|
||||||
|
|
||||||
|
git remote set-url origin https://x-access-token:${token}@github.com/$owner/$repo.git
|
||||||
|
git checkout -b $branch "origin/master"
|
||||||
|
git add ./test
|
||||||
|
git commit -am "resyncing specs $(date '+%m-%d-%Y')"
|
||||||
|
echo "Creating the git checkout... done."
|
||||||
|
|
||||||
|
git push origin $branch
|
||||||
|
resp=$(curl -L \
|
||||||
|
-X POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
-d "{\"title\":\"[Spec Resync] $(date '+%m-%d-%Y')\",\"body\":\"$(cat "$1")\",\"head\":\"${branch}\",\"base\":\"master\"}" \
|
||||||
|
--url https://api.github.com/repos/$owner/$repo/pulls)
|
||||||
|
echo $resp | jq '.html_url'
|
||||||
|
echo "Creating the PR... done."
|
||||||
|
|
||||||
|
rm -rf $tools
|
||||||
@ -7,6 +7,7 @@ from itertools import product
|
|||||||
from generate_config_utils import (
|
from generate_config_utils import (
|
||||||
ALL_PYTHONS,
|
ALL_PYTHONS,
|
||||||
ALL_VERSIONS,
|
ALL_VERSIONS,
|
||||||
|
BATCHTIME_DAY,
|
||||||
BATCHTIME_WEEK,
|
BATCHTIME_WEEK,
|
||||||
C_EXTS,
|
C_EXTS,
|
||||||
CPYTHONS,
|
CPYTHONS,
|
||||||
@ -25,7 +26,6 @@ from generate_config_utils import (
|
|||||||
get_task_name,
|
get_task_name,
|
||||||
get_variant_name,
|
get_variant_name,
|
||||||
get_versions_from,
|
get_versions_from,
|
||||||
get_versions_until,
|
|
||||||
handle_c_ext,
|
handle_c_ext,
|
||||||
write_functions_to_file,
|
write_functions_to_file,
|
||||||
write_tasks_to_file,
|
write_tasks_to_file,
|
||||||
@ -75,7 +75,11 @@ def create_server_version_variants() -> list[BuildVariant]:
|
|||||||
for version in ALL_VERSIONS:
|
for version in ALL_VERSIONS:
|
||||||
display_name = get_variant_name("* MongoDB", version=version)
|
display_name = get_variant_name("* MongoDB", version=version)
|
||||||
variant = create_variant(
|
variant = create_variant(
|
||||||
[".server-version"], display_name, host=DEFAULT_HOST, tags=["coverage_tag"]
|
[".server-version"],
|
||||||
|
display_name,
|
||||||
|
version=version,
|
||||||
|
host=DEFAULT_HOST,
|
||||||
|
tags=["coverage_tag"],
|
||||||
)
|
)
|
||||||
variants.append(variant)
|
variants.append(variant)
|
||||||
return variants
|
return variants
|
||||||
@ -93,6 +97,8 @@ def create_standard_nonlinux_variants() -> list[BuildVariant]:
|
|||||||
tasks = [
|
tasks = [
|
||||||
f".test-standard !.pypy .server-{version}" for version in get_versions_from("6.0")
|
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]
|
host = HOSTS[host_name]
|
||||||
tags = ["standard-non-linux"]
|
tags = ["standard-non-linux"]
|
||||||
expansions = dict()
|
expansions = dict()
|
||||||
@ -105,28 +111,10 @@ def create_standard_nonlinux_variants() -> list[BuildVariant]:
|
|||||||
return variants
|
return variants
|
||||||
|
|
||||||
|
|
||||||
def create_free_threaded_variants() -> list[BuildVariant]:
|
|
||||||
variants = []
|
|
||||||
for host_name in ("rhel8", "macos", "macos-arm64", "win64"):
|
|
||||||
if host_name == "win64":
|
|
||||||
# TODO: PYTHON-5027
|
|
||||||
continue
|
|
||||||
tasks = [".free-threading"]
|
|
||||||
tags = []
|
|
||||||
if host_name == "rhel8":
|
|
||||||
tags.append("pr")
|
|
||||||
host = HOSTS[host_name]
|
|
||||||
python = "3.13t"
|
|
||||||
display_name = get_variant_name("Free-threaded", host, python=python)
|
|
||||||
variant = create_variant(tasks, display_name, tags=tags, python=python, host=host)
|
|
||||||
variants.append(variant)
|
|
||||||
return variants
|
|
||||||
|
|
||||||
|
|
||||||
def create_encryption_variants() -> list[BuildVariant]:
|
def create_encryption_variants() -> list[BuildVariant]:
|
||||||
variants = []
|
variants = []
|
||||||
tags = ["encryption_tag"]
|
tags = ["encryption_tag"]
|
||||||
batchtime = BATCHTIME_WEEK
|
batchtime = BATCHTIME_DAY
|
||||||
|
|
||||||
def get_encryption_expansions(encryption):
|
def get_encryption_expansions(encryption):
|
||||||
expansions = dict(TEST_NAME="encryption")
|
expansions = dict(TEST_NAME="encryption")
|
||||||
@ -183,7 +171,7 @@ def create_load_balancer_variants():
|
|||||||
tasks,
|
tasks,
|
||||||
"Load Balancer",
|
"Load Balancer",
|
||||||
host=DEFAULT_HOST,
|
host=DEFAULT_HOST,
|
||||||
batchtime=BATCHTIME_WEEK,
|
batchtime=BATCHTIME_DAY,
|
||||||
expansions=expansions,
|
expansions=expansions,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@ -196,7 +184,7 @@ def create_compression_variants():
|
|||||||
for compressor in "snappy", "zlib", "zstd":
|
for compressor in "snappy", "zlib", "zstd":
|
||||||
expansions = dict(COMPRESSOR=compressor)
|
expansions = dict(COMPRESSOR=compressor)
|
||||||
if compressor == "zstd":
|
if compressor == "zstd":
|
||||||
tasks = [".test-standard !.server-4.0"]
|
tasks = [".test-standard !.server-4.2"]
|
||||||
else:
|
else:
|
||||||
tasks = [".test-standard"]
|
tasks = [".test-standard"]
|
||||||
display_name = get_variant_name(f"Compression {compressor}", host)
|
display_name = get_variant_name(f"Compression {compressor}", host)
|
||||||
@ -208,6 +196,22 @@ def create_compression_variants():
|
|||||||
expansions=expansions,
|
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
|
return variants
|
||||||
|
|
||||||
|
|
||||||
@ -216,9 +220,13 @@ def create_enterprise_auth_variants():
|
|||||||
for host in ["rhel8", "macos", "win64"]:
|
for host in ["rhel8", "macos", "win64"]:
|
||||||
expansions = dict(TEST_NAME="enterprise_auth", AUTH="auth")
|
expansions = dict(TEST_NAME="enterprise_auth", AUTH="auth")
|
||||||
display_name = get_variant_name("Auth Enterprise", host)
|
display_name = get_variant_name("Auth Enterprise", host)
|
||||||
tasks = [".test-non-standard .auth"]
|
tasks = [".test-standard-auth .auth !.free-threaded"]
|
||||||
if host != "rhel8":
|
# https://jira.mongodb.org/browse/PYTHON-5586
|
||||||
tasks = [".test-non-standard !.pypy .auth"]
|
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"]
|
||||||
variant = create_variant(tasks, display_name, host=host, expansions=expansions)
|
variant = create_variant(tasks, display_name, host=host, expansions=expansions)
|
||||||
variants.append(variant)
|
variants.append(variant)
|
||||||
return variants
|
return variants
|
||||||
@ -226,7 +234,7 @@ def create_enterprise_auth_variants():
|
|||||||
|
|
||||||
def create_pyopenssl_variants():
|
def create_pyopenssl_variants():
|
||||||
base_name = "PyOpenSSL"
|
base_name = "PyOpenSSL"
|
||||||
batchtime = BATCHTIME_WEEK
|
batchtime = BATCHTIME_DAY
|
||||||
expansions = dict(SUB_TEST_NAME="pyopenssl")
|
expansions = dict(SUB_TEST_NAME="pyopenssl")
|
||||||
variants = []
|
variants = []
|
||||||
|
|
||||||
@ -249,16 +257,11 @@ def create_pyopenssl_variants():
|
|||||||
|
|
||||||
def create_storage_engine_variants():
|
def create_storage_engine_variants():
|
||||||
host = DEFAULT_HOST
|
host = DEFAULT_HOST
|
||||||
engines = ["InMemory", "MMAPv1"]
|
engines = ["InMemory"]
|
||||||
variants = []
|
variants = []
|
||||||
for engine in engines:
|
for engine in engines:
|
||||||
expansions = dict(STORAGE_ENGINE=engine.lower())
|
expansions = dict(STORAGE_ENGINE=engine.lower())
|
||||||
if engine == engines[0]:
|
tasks = [".test-standard .standalone-noauth-nossl"]
|
||||||
tasks = [".test-standard .standalone-noauth-nossl"]
|
|
||||||
else:
|
|
||||||
# MongoDB 4.2 drops support for MMAPv1
|
|
||||||
versions = get_versions_until("4.0")
|
|
||||||
tasks = [f".test-standard !.sharded_cluster-auth-ssl .server-{v}" for v in versions]
|
|
||||||
display_name = get_variant_name(f"Storage {engine}", host)
|
display_name = get_variant_name(f"Storage {engine}", host)
|
||||||
variant = create_variant(tasks, display_name, host=host, expansions=expansions)
|
variant = create_variant(tasks, display_name, host=host, expansions=expansions)
|
||||||
variants.append(variant)
|
variants.append(variant)
|
||||||
@ -305,13 +308,9 @@ def create_stable_api_variants():
|
|||||||
def create_green_framework_variants():
|
def create_green_framework_variants():
|
||||||
variants = []
|
variants = []
|
||||||
host = DEFAULT_HOST
|
host = DEFAULT_HOST
|
||||||
for framework in ["eventlet", "gevent"]:
|
for framework in ["gevent"]:
|
||||||
tasks = [".test-standard .standalone-noauth-nossl"]
|
tasks = [".test-standard .sync !.free-threaded"]
|
||||||
if framework == "eventlet":
|
expansions = dict(GREEN_FRAMEWORK=framework)
|
||||||
# Eventlet has issues with dnspython > 2.0 and newer versions of CPython
|
|
||||||
# https://jira.mongodb.org/browse/PYTHON-5284
|
|
||||||
tasks = [".test-standard .standalone-noauth-nossl .python-3.9"]
|
|
||||||
expansions = dict(GREEN_FRAMEWORK=framework, AUTH="auth", SSL="ssl")
|
|
||||||
display_name = get_variant_name(f"Green {framework.capitalize()}", host)
|
display_name = get_variant_name(f"Green {framework.capitalize()}", host)
|
||||||
variant = create_variant(tasks, display_name, host=host, expansions=expansions)
|
variant = create_variant(tasks, display_name, host=host, expansions=expansions)
|
||||||
variants.append(variant)
|
variants.append(variant)
|
||||||
@ -324,15 +323,7 @@ def create_no_c_ext_variants():
|
|||||||
expansions = dict()
|
expansions = dict()
|
||||||
handle_c_ext(C_EXTS[0], expansions)
|
handle_c_ext(C_EXTS[0], expansions)
|
||||||
display_name = get_variant_name("No C Ext", host)
|
display_name = get_variant_name("No C Ext", host)
|
||||||
return [create_variant(tasks, display_name, host=host)]
|
return [create_variant(tasks, display_name, host=host, expansions=expansions)]
|
||||||
|
|
||||||
|
|
||||||
def create_atlas_data_lake_variants():
|
|
||||||
host = HOSTS["ubuntu22"]
|
|
||||||
tasks = [".test-no-orchestration"]
|
|
||||||
expansions = dict(TEST_NAME="data_lake")
|
|
||||||
display_name = get_variant_name("Atlas Data Lake", host)
|
|
||||||
return [create_variant(tasks, display_name, tags=["pr"], host=host, expansions=expansions)]
|
|
||||||
|
|
||||||
|
|
||||||
def create_mod_wsgi_variants():
|
def create_mod_wsgi_variants():
|
||||||
@ -346,27 +337,44 @@ def create_mod_wsgi_variants():
|
|||||||
def create_disable_test_commands_variants():
|
def create_disable_test_commands_variants():
|
||||||
host = DEFAULT_HOST
|
host = DEFAULT_HOST
|
||||||
expansions = dict(AUTH="auth", SSL="ssl", DISABLE_TEST_COMMANDS="1")
|
expansions = dict(AUTH="auth", SSL="ssl", DISABLE_TEST_COMMANDS="1")
|
||||||
python = CPYTHONS[0]
|
display_name = get_variant_name("Disable test commands", host)
|
||||||
display_name = get_variant_name("Disable test commands", host, python=python)
|
|
||||||
tasks = [".test-standard .server-latest"]
|
tasks = [".test-standard .server-latest"]
|
||||||
return [create_variant(tasks, display_name, host=host, python=python, expansions=expansions)]
|
return [create_variant(tasks, display_name, host=host, expansions=expansions)]
|
||||||
|
|
||||||
|
|
||||||
def create_serverless_variants():
|
def create_test_numpy_tasks():
|
||||||
host = DEFAULT_HOST
|
tasks = []
|
||||||
batchtime = BATCHTIME_WEEK
|
for python in MIN_MAX_PYTHON:
|
||||||
tasks = [".serverless"]
|
tags = ["binary", "vector", f"python-{python}", "test-numpy"]
|
||||||
base_name = "Serverless"
|
vars = dict(TOOLCHAIN_VERSION=python)
|
||||||
return [
|
if python == MIN_MAX_PYTHON[-1]:
|
||||||
create_variant(
|
tags.append("pr")
|
||||||
tasks,
|
vars["COVERAGE"] = "1"
|
||||||
get_variant_name(base_name, host, python=python),
|
task_name = get_task_name("test-numpy", python=python, **vars)
|
||||||
host=host,
|
test_func = FunctionCall(func="test numpy", vars=vars)
|
||||||
python=python,
|
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||||
batchtime=batchtime,
|
return tasks
|
||||||
)
|
|
||||||
for python in MIN_MAX_PYTHON
|
|
||||||
]
|
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():
|
def create_oidc_auth_variants():
|
||||||
@ -382,7 +390,7 @@ def create_oidc_auth_variants():
|
|||||||
tasks,
|
tasks,
|
||||||
get_variant_name("Auth OIDC", host),
|
get_variant_name("Auth OIDC", host),
|
||||||
host=host,
|
host=host,
|
||||||
batchtime=BATCHTIME_WEEK,
|
batchtime=BATCHTIME_DAY,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Add a specific local test to run on PRs.
|
# Add a specific local test to run on PRs.
|
||||||
@ -394,7 +402,8 @@ def create_oidc_auth_variants():
|
|||||||
get_variant_name("Auth OIDC Local", host),
|
get_variant_name("Auth OIDC Local", host),
|
||||||
tags=["pr"],
|
tags=["pr"],
|
||||||
host=host,
|
host=host,
|
||||||
batchtime=BATCHTIME_WEEK,
|
batchtime=BATCHTIME_DAY,
|
||||||
|
expansions=dict(COVERAGE="1"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return variants
|
return variants
|
||||||
@ -402,12 +411,10 @@ def create_oidc_auth_variants():
|
|||||||
|
|
||||||
def create_search_index_variants():
|
def create_search_index_variants():
|
||||||
host = DEFAULT_HOST
|
host = DEFAULT_HOST
|
||||||
python = CPYTHONS[0]
|
|
||||||
return [
|
return [
|
||||||
create_variant(
|
create_variant(
|
||||||
[".search_index"],
|
[".search_index"],
|
||||||
get_variant_name("Search Index Helpers", host, python=python),
|
get_variant_name("Search Index Helpers", host),
|
||||||
python=python,
|
|
||||||
host=host,
|
host=host,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@ -448,6 +455,7 @@ def create_atlas_connect_variants():
|
|||||||
get_variant_name("Atlas connect", host),
|
get_variant_name("Atlas connect", host),
|
||||||
tags=["pr"],
|
tags=["pr"],
|
||||||
host=DEFAULT_HOST,
|
host=DEFAULT_HOST,
|
||||||
|
expansions=dict(TEST_NAME="atlas_connect"),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -458,9 +466,9 @@ def create_coverage_report_variants():
|
|||||||
|
|
||||||
def create_kms_variants():
|
def create_kms_variants():
|
||||||
tasks = []
|
tasks = []
|
||||||
tasks.append(EvgTaskRef(name="test-gcpkms", batchtime=BATCHTIME_WEEK))
|
tasks.append(EvgTaskRef(name="test-gcpkms", batchtime=BATCHTIME_DAY))
|
||||||
tasks.append("test-gcpkms-fail")
|
tasks.append("test-gcpkms-fail")
|
||||||
tasks.append(EvgTaskRef(name="test-azurekms", batchtime=BATCHTIME_WEEK))
|
tasks.append(EvgTaskRef(name="test-azurekms", batchtime=BATCHTIME_DAY))
|
||||||
tasks.append("test-azurekms-fail")
|
tasks.append("test-azurekms-fail")
|
||||||
return [create_variant(tasks, "KMS", host=HOSTS["debian11"])]
|
return [create_variant(tasks, "KMS", host=HOSTS["debian11"])]
|
||||||
|
|
||||||
@ -475,23 +483,21 @@ def create_backport_pr_variants():
|
|||||||
|
|
||||||
def create_perf_variants():
|
def create_perf_variants():
|
||||||
host = HOSTS["perf"]
|
host = HOSTS["perf"]
|
||||||
return [
|
return [create_variant([".perf"], "Performance Benchmarks", host=host, batchtime=BATCHTIME_DAY)]
|
||||||
create_variant([".perf"], "Performance Benchmarks", host=host, batchtime=BATCHTIME_WEEK)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_aws_auth_variants():
|
def create_aws_auth_variants():
|
||||||
variants = []
|
variants = []
|
||||||
|
|
||||||
for host_name in ["ubuntu20", "win64", "macos"]:
|
for host_name in ["rhel8", "win64", "macos"]:
|
||||||
expansions = dict()
|
expansions = dict()
|
||||||
tasks = [".auth-aws"]
|
tasks = [".auth-aws"]
|
||||||
tags = []
|
tags = []
|
||||||
if host_name == "macos":
|
if host_name == "macos":
|
||||||
tasks = [".auth-aws !.auth-aws-web-identity !.auth-aws-ecs !.auth-aws-ec2"]
|
tasks = [".auth-aws !.auth-aws-web-identity !.auth-aws-ec2"]
|
||||||
tags = ["pr"]
|
tags = ["pr"]
|
||||||
elif host_name == "win64":
|
elif host_name == "win64":
|
||||||
tasks = [".auth-aws !.auth-aws-ecs"]
|
tasks = [".auth-aws"]
|
||||||
host = HOSTS[host_name]
|
host = HOSTS[host_name]
|
||||||
variant = create_variant(
|
variant = create_variant(
|
||||||
tasks,
|
tasks,
|
||||||
@ -501,9 +507,25 @@ def create_aws_auth_variants():
|
|||||||
expansions=expansions,
|
expansions=expansions,
|
||||||
)
|
)
|
||||||
variants.append(variant)
|
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
|
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():
|
def create_no_server_variants():
|
||||||
host = HOSTS["rhel8"]
|
host = HOSTS["rhel8"]
|
||||||
name = get_variant_name("No server", host=host)
|
name = get_variant_name("No server", host=host)
|
||||||
@ -511,22 +533,9 @@ def create_no_server_variants():
|
|||||||
|
|
||||||
|
|
||||||
def create_alternative_hosts_variants():
|
def create_alternative_hosts_variants():
|
||||||
batchtime = BATCHTIME_WEEK
|
batchtime = BATCHTIME_DAY
|
||||||
variants = []
|
variants = []
|
||||||
|
|
||||||
host = HOSTS["rhel7"]
|
|
||||||
version = "5.0"
|
|
||||||
variants.append(
|
|
||||||
create_variant(
|
|
||||||
[".test-no-toolchain"],
|
|
||||||
get_variant_name("OpenSSL 1.0.2", host, python=CPYTHONS[0], version=version),
|
|
||||||
host=host,
|
|
||||||
python=CPYTHONS[0],
|
|
||||||
batchtime=batchtime,
|
|
||||||
expansions=dict(VERSION=version, PYTHON_VERSION=CPYTHONS[0]),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
version = "latest"
|
version = "latest"
|
||||||
for host_name in OTHER_HOSTS:
|
for host_name in OTHER_HOSTS:
|
||||||
expansions = dict(VERSION="latest")
|
expansions = dict(VERSION="latest")
|
||||||
@ -535,6 +544,8 @@ def create_alternative_hosts_variants():
|
|||||||
tags = []
|
tags = []
|
||||||
if "fips" in host_name.lower():
|
if "fips" in host_name.lower():
|
||||||
expansions["REQUIRE_FIPS"] = "1"
|
expansions["REQUIRE_FIPS"] = "1"
|
||||||
|
# Use explicit Python 3.11 binary on the host since the default python3 is 3.9.
|
||||||
|
expansions["UV_PYTHON"] = "/usr/bin/python3.11"
|
||||||
if "amazon" in host_name.lower():
|
if "amazon" in host_name.lower():
|
||||||
tags.append("pr")
|
tags.append("pr")
|
||||||
variants.append(
|
variants.append(
|
||||||
@ -562,22 +573,20 @@ def create_aws_lambda_variants():
|
|||||||
|
|
||||||
def create_server_version_tasks():
|
def create_server_version_tasks():
|
||||||
tasks = []
|
tasks = []
|
||||||
task_inputs = []
|
task_combos = set()
|
||||||
# All combinations of topology, auth, ssl, and sync should be tested.
|
# All combinations of topology, auth, ssl, and sync should be tested.
|
||||||
for (topology, auth, ssl, sync), python in zip_cycle(
|
for (topology, auth, ssl, sync), python in zip_cycle(
|
||||||
list(product(TOPOLOGIES, ["auth", "noauth"], ["ssl", "nossl"], SYNCS)), ALL_PYTHONS
|
list(product(TOPOLOGIES, ["auth", "noauth"], ["ssl", "nossl"], SYNCS)), ALL_PYTHONS
|
||||||
):
|
):
|
||||||
task_inputs.append((topology, auth, ssl, sync, python))
|
task_combos.add((topology, auth, ssl, sync, python))
|
||||||
|
|
||||||
# Every python should be tested with sharded cluster, auth, ssl, with sync and async.
|
# Every python should be tested with sharded cluster, auth, ssl, with sync and async.
|
||||||
for python, sync in product(ALL_PYTHONS, SYNCS):
|
for python, sync in product(ALL_PYTHONS, SYNCS):
|
||||||
task_input = ("sharded_cluster", "auth", "ssl", sync, python)
|
task_combos.add(("sharded_cluster", "auth", "ssl", sync, python))
|
||||||
if task_input not in task_inputs:
|
|
||||||
task_inputs.append(task_input)
|
|
||||||
|
|
||||||
# Assemble the tasks.
|
# Assemble the tasks.
|
||||||
seen = set()
|
seen = set()
|
||||||
for topology, auth, ssl, sync, python in task_inputs:
|
for topology, auth, ssl, sync, python in sorted(task_combos):
|
||||||
combo = f"{topology}-{auth}-{ssl}"
|
combo = f"{topology}-{auth}-{ssl}"
|
||||||
tags = ["server-version", f"python-{python}", combo, sync]
|
tags = ["server-version", f"python-{python}", combo, sync]
|
||||||
if combo in [
|
if combo in [
|
||||||
@ -590,12 +599,21 @@ def create_server_version_tasks():
|
|||||||
seen.add(combo)
|
seen.add(combo)
|
||||||
tags.append("pr")
|
tags.append("pr")
|
||||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
|
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
|
||||||
if python not in PYPYS:
|
if python == ALL_PYTHONS[0]:
|
||||||
|
expansions["TEST_MIN_DEPS"] = "1"
|
||||||
|
if "t" in python:
|
||||||
|
tags.append("free-threaded")
|
||||||
|
if "pr" in tags:
|
||||||
expansions["COVERAGE"] = "1"
|
expansions["COVERAGE"] = "1"
|
||||||
name = get_task_name("test-server-version", python=python, sync=sync, **expansions)
|
name = get_task_name(
|
||||||
|
"test-server-version",
|
||||||
|
python=python,
|
||||||
|
sync=sync,
|
||||||
|
**expansions,
|
||||||
|
)
|
||||||
server_func = FunctionCall(func="run server", vars=expansions)
|
server_func = FunctionCall(func="run server", vars=expansions)
|
||||||
test_vars = expansions.copy()
|
test_vars = expansions.copy()
|
||||||
test_vars["PYTHON_VERSION"] = python
|
test_vars["TOOLCHAIN_VERSION"] = python
|
||||||
test_vars["TEST_NAME"] = f"default_{sync}"
|
test_vars["TEST_NAME"] = f"default_{sync}"
|
||||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||||
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
|
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
|
||||||
@ -624,15 +642,15 @@ def create_no_toolchain_tasks():
|
|||||||
def create_test_non_standard_tasks():
|
def create_test_non_standard_tasks():
|
||||||
"""For variants that set a TEST_NAME."""
|
"""For variants that set a TEST_NAME."""
|
||||||
tasks = []
|
tasks = []
|
||||||
task_combos = []
|
task_combos = set()
|
||||||
# For each version and topology, rotate through the CPythons.
|
# For each version and topology, rotate through the CPythons.
|
||||||
for (version, topology), python in zip_cycle(list(product(ALL_VERSIONS, TOPOLOGIES)), CPYTHONS):
|
for (version, topology), python in zip_cycle(list(product(ALL_VERSIONS, TOPOLOGIES)), CPYTHONS):
|
||||||
pr = version == "latest"
|
pr = version == "latest"
|
||||||
task_combos.append((version, topology, python, pr))
|
task_combos.add((version, topology, python, pr))
|
||||||
# For each PyPy and topology, rotate through the the versions.
|
# For each PyPy and topology, rotate through the MongoDB versions.
|
||||||
for (python, topology), version in zip_cycle(list(product(PYPYS, TOPOLOGIES)), ALL_VERSIONS):
|
for (python, topology), version in zip_cycle(list(product(PYPYS, TOPOLOGIES)), ALL_VERSIONS):
|
||||||
task_combos.append((version, topology, python, False))
|
task_combos.add((version, topology, python, False))
|
||||||
for version, topology, python, pr in task_combos:
|
for version, topology, python, pr in sorted(task_combos):
|
||||||
auth, ssl = get_standard_auth_ssl(topology)
|
auth, ssl = get_standard_auth_ssl(topology)
|
||||||
tags = [
|
tags = [
|
||||||
"test-non-standard",
|
"test-non-standard",
|
||||||
@ -641,15 +659,65 @@ def create_test_non_standard_tasks():
|
|||||||
f"{topology}-{auth}-{ssl}",
|
f"{topology}-{auth}-{ssl}",
|
||||||
auth,
|
auth,
|
||||||
]
|
]
|
||||||
|
if "t" in python:
|
||||||
|
tags.append("free-threaded")
|
||||||
if python in PYPYS:
|
if python in PYPYS:
|
||||||
tags.append("pypy")
|
tags.append("pypy")
|
||||||
if pr:
|
if pr:
|
||||||
tags.append("pr")
|
tags.append("pr")
|
||||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
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)
|
name = get_task_name("test-non-standard", python=python, **expansions)
|
||||||
server_func = FunctionCall(func="run server", vars=expansions)
|
server_func = FunctionCall(func="run server", vars=expansions)
|
||||||
test_vars = expansions.copy()
|
test_vars = expansions.copy()
|
||||||
test_vars["PYTHON_VERSION"] = python
|
test_vars["TOOLCHAIN_VERSION"] = python
|
||||||
|
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_test_standard_auth_tasks():
|
||||||
|
"""We only use auth on sharded clusters"""
|
||||||
|
tasks = []
|
||||||
|
task_combos = set()
|
||||||
|
# Rotate through the CPython and MongoDB versions
|
||||||
|
for (version, topology), python in zip_cycle(
|
||||||
|
list(product(ALL_VERSIONS, ["sharded_cluster"])), CPYTHONS
|
||||||
|
):
|
||||||
|
pr = version == "latest"
|
||||||
|
task_combos.add((version, topology, python, pr))
|
||||||
|
# Rotate through each PyPy and MongoDB versions.
|
||||||
|
for (python, topology), version in zip_cycle(
|
||||||
|
list(product(PYPYS, ["sharded_cluster"])), ALL_VERSIONS
|
||||||
|
):
|
||||||
|
task_combos.add((version, topology, python, False))
|
||||||
|
for version, topology, python, pr in sorted(task_combos):
|
||||||
|
auth, ssl = get_standard_auth_ssl(topology)
|
||||||
|
tags = [
|
||||||
|
"test-standard-auth",
|
||||||
|
f"server-{version}",
|
||||||
|
f"python-{python}",
|
||||||
|
f"{topology}-{auth}-{ssl}",
|
||||||
|
auth,
|
||||||
|
]
|
||||||
|
if "t" in python:
|
||||||
|
tags.append("free-threaded")
|
||||||
|
if python in PYPYS:
|
||||||
|
tags.append("pypy")
|
||||||
|
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()
|
||||||
|
test_vars["TOOLCHAIN_VERSION"] = python
|
||||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||||
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
|
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
|
||||||
return tasks
|
return tasks
|
||||||
@ -658,20 +726,15 @@ def create_test_non_standard_tasks():
|
|||||||
def create_standard_tasks():
|
def create_standard_tasks():
|
||||||
"""For variants that do not set a TEST_NAME."""
|
"""For variants that do not set a TEST_NAME."""
|
||||||
tasks = []
|
tasks = []
|
||||||
task_combos = []
|
task_combos = set()
|
||||||
# For each version and topology, rotate through the CPythons and sync/async.
|
# For each python and topology and sync/async, rotate through the the versions.
|
||||||
for (version, topology), python, sync in zip_cycle(
|
for (python, topology, sync), version in zip_cycle(
|
||||||
list(product(ALL_VERSIONS, TOPOLOGIES)), CPYTHONS, SYNCS
|
list(product(CPYTHONS + PYPYS, TOPOLOGIES, SYNCS)), ALL_VERSIONS
|
||||||
):
|
):
|
||||||
pr = version == "latest"
|
pr = version == "latest" and python not in PYPYS
|
||||||
task_combos.append((version, topology, python, sync, pr))
|
task_combos.add((version, topology, python, sync, pr))
|
||||||
# For each PyPy and topology, rotate through the the versions and sync/async.
|
|
||||||
for (python, topology), version, sync in zip_cycle(
|
|
||||||
list(product(PYPYS, TOPOLOGIES)), ALL_VERSIONS, SYNCS
|
|
||||||
):
|
|
||||||
task_combos.append((version, topology, python, sync, False))
|
|
||||||
|
|
||||||
for version, topology, python, sync, pr in task_combos:
|
for version, topology, python, sync, pr in sorted(task_combos):
|
||||||
auth, ssl = get_standard_auth_ssl(topology)
|
auth, ssl = get_standard_auth_ssl(topology)
|
||||||
tags = [
|
tags = [
|
||||||
"test-standard",
|
"test-standard",
|
||||||
@ -680,15 +743,21 @@ def create_standard_tasks():
|
|||||||
f"{topology}-{auth}-{ssl}",
|
f"{topology}-{auth}-{ssl}",
|
||||||
sync,
|
sync,
|
||||||
]
|
]
|
||||||
|
if "t" in python:
|
||||||
|
tags.append("free-threaded")
|
||||||
if python in PYPYS:
|
if python in PYPYS:
|
||||||
tags.append("pypy")
|
tags.append("pypy")
|
||||||
if pr:
|
if pr:
|
||||||
tags.append("pr")
|
tags.append("pr")
|
||||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
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)
|
name = get_task_name("test-standard", python=python, sync=sync, **expansions)
|
||||||
server_func = FunctionCall(func="run server", vars=expansions)
|
server_func = FunctionCall(func="run server", vars=expansions)
|
||||||
test_vars = expansions.copy()
|
test_vars = expansions.copy()
|
||||||
test_vars["PYTHON_VERSION"] = python
|
test_vars["TOOLCHAIN_VERSION"] = python
|
||||||
test_vars["TEST_NAME"] = f"default_{sync}"
|
test_vars["TEST_NAME"] = f"default_{sync}"
|
||||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||||
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
|
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
|
||||||
@ -702,9 +771,11 @@ def create_no_orchestration_tasks():
|
|||||||
"test-no-orchestration",
|
"test-no-orchestration",
|
||||||
f"python-{python}",
|
f"python-{python}",
|
||||||
]
|
]
|
||||||
name = get_task_name("test-no-orchestration", python=python)
|
|
||||||
assume_func = FunctionCall(func="assume ec2 role")
|
assume_func = FunctionCall(func="assume ec2 role")
|
||||||
test_vars = dict(PYTHON_VERSION=python)
|
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)
|
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||||
commands = [assume_func, test_func]
|
commands = [assume_func, test_func]
|
||||||
tasks.append(EvgTask(name=name, tags=tags, commands=commands))
|
tasks.append(EvgTask(name=name, tags=tags, commands=commands))
|
||||||
@ -741,17 +812,23 @@ def create_aws_tasks():
|
|||||||
"env-creds",
|
"env-creds",
|
||||||
"session-creds",
|
"session-creds",
|
||||||
"web-identity",
|
"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):
|
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_name = f"test-auth-aws-{version}"
|
||||||
base_tags = ["auth-aws"]
|
base_tags = ["auth-aws"]
|
||||||
server_vars = dict(AUTH_AWS="1", VERSION=version)
|
server_vars = dict(AUTH_AWS="1", VERSION=version)
|
||||||
server_func = FunctionCall(func="run server", vars=server_vars)
|
server_func = FunctionCall(func="run server", vars=server_vars)
|
||||||
assume_func = FunctionCall(func="assume ec2 role")
|
|
||||||
tags = [*base_tags, f"auth-aws-{test_type}"]
|
tags = [*base_tags, f"auth-aws-{test_type}"]
|
||||||
name = get_task_name(f"{base_name}-{test_type}", python=python)
|
if "t" in python:
|
||||||
test_vars = dict(TEST_NAME="auth_aws", SUB_TEST_NAME=test_type, PYTHON_VERSION=python)
|
tags.append("free-threaded")
|
||||||
|
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)
|
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||||
funcs = [server_func, assume_func, test_func]
|
funcs = [server_func, assume_func, test_func]
|
||||||
tasks.append(EvgTask(name=name, tags=tags, commands=funcs))
|
tasks.append(EvgTask(name=name, tags=tags, commands=funcs))
|
||||||
@ -763,12 +840,24 @@ def create_aws_tasks():
|
|||||||
TEST_NAME="auth_aws",
|
TEST_NAME="auth_aws",
|
||||||
SUB_TEST_NAME="web-identity",
|
SUB_TEST_NAME="web-identity",
|
||||||
AWS_ROLE_SESSION_NAME="test",
|
AWS_ROLE_SESSION_NAME="test",
|
||||||
PYTHON_VERSION=python,
|
TOOLCHAIN_VERSION=python,
|
||||||
)
|
)
|
||||||
|
if "t" in python:
|
||||||
|
tags.append("free-threaded")
|
||||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||||
funcs = [server_func, assume_func, test_func]
|
funcs = [server_func, assume_func, test_func]
|
||||||
tasks.append(EvgTask(name=name, tags=tags, commands=funcs))
|
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
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
@ -776,11 +865,11 @@ def create_oidc_tasks():
|
|||||||
tasks = []
|
tasks = []
|
||||||
for sub_test in ["default", "azure", "gcp", "eks", "aks", "gke"]:
|
for sub_test in ["default", "azure", "gcp", "eks", "aks", "gke"]:
|
||||||
vars = dict(TEST_NAME="auth_oidc", SUB_TEST_NAME=sub_test)
|
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"]
|
tags = ["auth_oidc"]
|
||||||
if sub_test != "default":
|
if sub_test != "default":
|
||||||
tags.append("auth_oidc_remote")
|
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]))
|
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||||
|
|
||||||
return tasks
|
return tasks
|
||||||
@ -791,15 +880,19 @@ def create_mod_wsgi_tasks():
|
|||||||
for (test, topology), python in zip_cycle(
|
for (test, topology), python in zip_cycle(
|
||||||
product(["standalone", "embedded-mode"], ["standalone", "replica_set"]), CPYTHONS
|
product(["standalone", "embedded-mode"], ["standalone", "replica_set"]), CPYTHONS
|
||||||
):
|
):
|
||||||
|
if "t" in python:
|
||||||
|
continue
|
||||||
if test == "standalone":
|
if test == "standalone":
|
||||||
task_name = "mod-wsgi-"
|
task_name = "mod-wsgi-"
|
||||||
else:
|
else:
|
||||||
task_name = "mod-wsgi-embedded-mode-"
|
task_name = "mod-wsgi-embedded-mode-"
|
||||||
task_name += topology.replace("_", "-")
|
task_name += topology.replace("_", "-")
|
||||||
task_name = get_task_name(task_name, python=python)
|
task_name = get_task_name(task_name, python=python)
|
||||||
server_vars = dict(TOPOLOGY=topology, PYTHON_VERSION=python)
|
server_vars = dict(TOPOLOGY=topology, TOOLCHAIN_VERSION=python)
|
||||||
server_func = FunctionCall(func="run server", vars=server_vars)
|
server_func = FunctionCall(func="run server", vars=server_vars)
|
||||||
vars = dict(TEST_NAME="mod_wsgi", SUB_TEST_NAME=test.split("-")[0], PYTHON_VERSION=python)
|
vars = dict(
|
||||||
|
TEST_NAME="mod_wsgi", SUB_TEST_NAME=test.split("-")[0], TOOLCHAIN_VERSION=python
|
||||||
|
)
|
||||||
test_func = FunctionCall(func="run tests", vars=vars)
|
test_func = FunctionCall(func="run tests", vars=vars)
|
||||||
tags = ["mod_wsgi", "pr"]
|
tags = ["mod_wsgi", "pr"]
|
||||||
commands = [server_func, test_func]
|
commands = [server_func, test_func]
|
||||||
@ -821,21 +914,40 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
|
|||||||
ORCHESTRATION_FILE=file_name,
|
ORCHESTRATION_FILE=file_name,
|
||||||
OCSP_SERVER_TYPE=server_type,
|
OCSP_SERVER_TYPE=server_type,
|
||||||
TEST_NAME="ocsp",
|
TEST_NAME="ocsp",
|
||||||
PYTHON_VERSION=python,
|
TOOLCHAIN_VERSION=python,
|
||||||
VERSION=version,
|
VERSION=version,
|
||||||
)
|
)
|
||||||
test_func = FunctionCall(func="run tests", vars=vars)
|
if python == ALL_PYTHONS[0]:
|
||||||
|
vars["TEST_MIN_DEPS"] = "1"
|
||||||
tags = ["ocsp", f"ocsp-{algo}", version]
|
tags = ["ocsp", f"ocsp-{algo}", version]
|
||||||
if "disableStapling" not in variant:
|
if "disableStapling" not in variant:
|
||||||
tags.append("ocsp-staple")
|
tags.append("ocsp-staple")
|
||||||
if algo == "valid-cert-server-staples" and version == "latest":
|
if base_task_name == "valid-cert-server-staples" and version == "latest":
|
||||||
tags.append("pr")
|
tags.append("pr")
|
||||||
|
if "TEST_MIN_DEPS" not in vars:
|
||||||
task_name = get_task_name(
|
vars["COVERAGE"] = "1"
|
||||||
f"test-ocsp-{algo}-{base_task_name}", python=python, version=version
|
test_func = FunctionCall(func="run tests", vars=vars)
|
||||||
)
|
task_name = get_task_name(f"test-ocsp-{algo}-{base_task_name}", **vars)
|
||||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
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
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
@ -852,7 +964,7 @@ def create_aws_lambda_tasks():
|
|||||||
def create_search_index_tasks():
|
def create_search_index_tasks():
|
||||||
assume_func = FunctionCall(func="assume ec2 role")
|
assume_func = FunctionCall(func="assume ec2 role")
|
||||||
server_func = FunctionCall(func="run server", vars=dict(TEST_NAME="search_index"))
|
server_func = FunctionCall(func="run server", vars=dict(TEST_NAME="search_index"))
|
||||||
vars = dict(TEST_NAME="search_index")
|
vars = dict(TEST_NAME="search_index", TOOLCHAIN_VERSION=CPYTHONS[0])
|
||||||
test_func = FunctionCall(func="run tests", vars=vars)
|
test_func = FunctionCall(func="run tests", vars=vars)
|
||||||
task_name = "test-search-index-helpers"
|
task_name = "test-search-index-helpers"
|
||||||
tags = ["search_index"]
|
tags = ["search_index"]
|
||||||
@ -919,8 +1031,10 @@ def create_backport_pr_tasks():
|
|||||||
"mongo-python-driver",
|
"mongo-python-driver",
|
||||||
"${github_commit}",
|
"${github_commit}",
|
||||||
]
|
]
|
||||||
cmd = get_subprocess_exec(args=args)
|
include_expansions = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
|
||||||
return [EvgTask(name=name, commands=[cmd], allowed_requesters=["commit"])]
|
cmd = get_subprocess_exec(args=args, include_expansions_in_env=include_expansions)
|
||||||
|
assume_func = FunctionCall(func="assume ec2 role")
|
||||||
|
return [EvgTask(name=name, commands=[assume_func, cmd], allowed_requesters=["commit"])]
|
||||||
|
|
||||||
|
|
||||||
def create_ocsp_tasks():
|
def create_ocsp_tasks():
|
||||||
@ -959,23 +1073,6 @@ def create_ocsp_tasks():
|
|||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
def create_free_threading_tasks():
|
|
||||||
vars = dict(VERSION="8.0", TOPOLOGY="replica_set")
|
|
||||||
server_func = FunctionCall(func="run server", vars=vars)
|
|
||||||
test_func = FunctionCall(func="run tests")
|
|
||||||
task_name = "test-free-threading"
|
|
||||||
tags = ["free-threading"]
|
|
||||||
return [EvgTask(name=task_name, tags=tags, commands=[server_func, test_func])]
|
|
||||||
|
|
||||||
|
|
||||||
def create_serverless_tasks():
|
|
||||||
vars = dict(TEST_NAME="serverless", AUTH="auth", SSL="ssl")
|
|
||||||
test_func = FunctionCall(func="run tests", vars=vars)
|
|
||||||
tags = ["serverless"]
|
|
||||||
task_name = "test-serverless"
|
|
||||||
return [EvgTask(name=task_name, tags=tags, commands=[test_func])]
|
|
||||||
|
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# Functions
|
# Functions
|
||||||
##############
|
##############
|
||||||
@ -996,6 +1093,26 @@ def create_upload_coverage_func():
|
|||||||
return "upload coverage", [get_assume_role(), cmd]
|
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():
|
def create_download_and_merge_coverage_func():
|
||||||
include_expansions = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
|
include_expansions = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
|
||||||
args = [
|
args = [
|
||||||
@ -1078,8 +1195,8 @@ def create_run_server_func():
|
|||||||
"AUTH",
|
"AUTH",
|
||||||
"SSL",
|
"SSL",
|
||||||
"ORCHESTRATION_FILE",
|
"ORCHESTRATION_FILE",
|
||||||
"PYTHON_BINARY",
|
"UV_PYTHON",
|
||||||
"PYTHON_VERSION",
|
"TOOLCHAIN_VERSION",
|
||||||
"STORAGE_ENGINE",
|
"STORAGE_ENGINE",
|
||||||
"REQUIRE_API_VERSION",
|
"REQUIRE_API_VERSION",
|
||||||
"DRIVERS_TOOLS",
|
"DRIVERS_TOOLS",
|
||||||
@ -1103,10 +1220,10 @@ def create_run_tests_func():
|
|||||||
"AWS_SECRET_ACCESS_KEY",
|
"AWS_SECRET_ACCESS_KEY",
|
||||||
"AWS_SESSION_TOKEN",
|
"AWS_SESSION_TOKEN",
|
||||||
"COVERAGE",
|
"COVERAGE",
|
||||||
"PYTHON_BINARY",
|
"UV_PYTHON",
|
||||||
"LIBMONGOCRYPT_URL",
|
"LIBMONGOCRYPT_URL",
|
||||||
"MONGODB_URI",
|
"MONGODB_URI",
|
||||||
"PYTHON_VERSION",
|
"TOOLCHAIN_VERSION",
|
||||||
"DISABLE_TEST_COMMANDS",
|
"DISABLE_TEST_COMMANDS",
|
||||||
"GREEN_FRAMEWORK",
|
"GREEN_FRAMEWORK",
|
||||||
"NO_EXT",
|
"NO_EXT",
|
||||||
@ -1114,11 +1231,13 @@ def create_run_tests_func():
|
|||||||
"MONGODB_API_VERSION",
|
"MONGODB_API_VERSION",
|
||||||
"REQUIRE_API_VERSION",
|
"REQUIRE_API_VERSION",
|
||||||
"DEBUG_LOG",
|
"DEBUG_LOG",
|
||||||
|
"DISABLE_FLAKY",
|
||||||
"ORCHESTRATION_FILE",
|
"ORCHESTRATION_FILE",
|
||||||
"OCSP_SERVER_TYPE",
|
"OCSP_SERVER_TYPE",
|
||||||
"VERSION",
|
"VERSION",
|
||||||
"IS_WIN32",
|
"IS_WIN32",
|
||||||
"REQUIRE_FIPS",
|
"REQUIRE_FIPS",
|
||||||
|
"TEST_MIN_DEPS",
|
||||||
]
|
]
|
||||||
args = [".evergreen/just.sh", "setup-tests", "${TEST_NAME}", "${SUB_TEST_NAME}"]
|
args = [".evergreen/just.sh", "setup-tests", "${TEST_NAME}", "${SUB_TEST_NAME}"]
|
||||||
setup_cmd = get_subprocess_exec(include_expansions_in_env=includes, args=args)
|
setup_cmd = get_subprocess_exec(include_expansions_in_env=includes, args=args)
|
||||||
@ -1126,6 +1245,14 @@ def create_run_tests_func():
|
|||||||
return "run tests", [setup_cmd, test_cmd]
|
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():
|
def create_cleanup_func():
|
||||||
cmd = get_subprocess_exec(args=[".evergreen/scripts/cleanup.sh"])
|
cmd = get_subprocess_exec(args=[".evergreen/scripts/cleanup.sh"])
|
||||||
return "cleanup", [cmd]
|
return "cleanup", [cmd]
|
||||||
|
|||||||
@ -21,12 +21,14 @@ from shrub.v3.shrub_service import ShrubService
|
|||||||
# Globals
|
# Globals
|
||||||
##############
|
##############
|
||||||
|
|
||||||
ALL_VERSIONS = ["4.0", "4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
|
ALL_VERSIONS = ["4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
|
||||||
CPYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
CPYTHONS = ["3.10", "3.11", "3.12", "3.13", "3.14t", "3.14"]
|
||||||
PYPYS = ["pypy3.10"]
|
PYPYS = ["pypy3.11"]
|
||||||
|
MIN_SUPPORT_VERSIONS = ["3.9", "pypy3.9", "pypy3.10"]
|
||||||
ALL_PYTHONS = CPYTHONS + PYPYS
|
ALL_PYTHONS = CPYTHONS + PYPYS
|
||||||
MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]]
|
MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]]
|
||||||
BATCHTIME_WEEK = 10080
|
BATCHTIME_WEEK = 10080
|
||||||
|
BATCHTIME_DAY = 1440
|
||||||
AUTH_SSLS = [("auth", "ssl"), ("noauth", "ssl"), ("noauth", "nossl")]
|
AUTH_SSLS = [("auth", "ssl"), ("noauth", "ssl"), ("noauth", "nossl")]
|
||||||
TOPOLOGIES = ["standalone", "replica_set", "sharded_cluster"]
|
TOPOLOGIES = ["standalone", "replica_set", "sharded_cluster"]
|
||||||
C_EXTS = ["without_ext", "with_ext"]
|
C_EXTS = ["without_ext", "with_ext"]
|
||||||
@ -41,6 +43,7 @@ DISPLAY_LOOKUP = dict(
|
|||||||
sync={"sync": "Sync", "async": "Async"},
|
sync={"sync": "Sync", "async": "Async"},
|
||||||
coverage={"1": "cov"},
|
coverage={"1": "cov"},
|
||||||
no_ext={"1": "No C"},
|
no_ext={"1": "No C"},
|
||||||
|
test_min_deps={"1": "Min Deps"},
|
||||||
)
|
)
|
||||||
HOSTS = dict()
|
HOSTS = dict()
|
||||||
|
|
||||||
@ -56,12 +59,12 @@ class Host:
|
|||||||
# Hosts with toolchains.
|
# Hosts with toolchains.
|
||||||
HOSTS["rhel8"] = Host("rhel8", "rhel87-small", "RHEL8", dict())
|
HOSTS["rhel8"] = Host("rhel8", "rhel87-small", "RHEL8", dict())
|
||||||
HOSTS["win64"] = Host("win64", "windows-64-vsMulti-small", "Win64", 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["win32"] = Host("win32", "windows-64-vsMulti-small", "Win32", dict())
|
||||||
HOSTS["macos"] = Host("macos", "macos-14", "macOS", dict())
|
HOSTS["macos"] = Host("macos", "macos-14", "macOS", dict())
|
||||||
HOSTS["macos-arm64"] = Host("macos-arm64", "macos-14-arm64", "macOS Arm64", 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["ubuntu22"] = Host("ubuntu22", "ubuntu2204-small", "Ubuntu-22", dict())
|
||||||
HOSTS["rhel7"] = Host("rhel7", "rhel79-small", "RHEL7", dict())
|
HOSTS["ubuntu24"] = Host("ubuntu24", "ubuntu2404-small", "Ubuntu-24", dict())
|
||||||
HOSTS["perf"] = Host("perf", "rhel90-dbx-perf-large", "", dict())
|
HOSTS["perf"] = Host("perf", "rhel90-dbx-perf-large", "", dict())
|
||||||
HOSTS["debian11"] = Host("debian11", "debian11-small", "Debian11", dict())
|
HOSTS["debian11"] = Host("debian11", "debian11-small", "Debian11", dict())
|
||||||
DEFAULT_HOST = HOSTS["rhel8"]
|
DEFAULT_HOST = HOSTS["rhel8"]
|
||||||
@ -131,44 +134,25 @@ def create_variant(
|
|||||||
*,
|
*,
|
||||||
version: str | None = None,
|
version: str | None = None,
|
||||||
host: Host | str | None = None,
|
host: Host | str | None = None,
|
||||||
python: str | None = None,
|
|
||||||
expansions: dict | None = None,
|
expansions: dict | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> BuildVariant:
|
) -> BuildVariant:
|
||||||
expansions = expansions and expansions.copy() or dict()
|
expansions = expansions and expansions.copy() or dict()
|
||||||
if version:
|
if version:
|
||||||
expansions["VERSION"] = version
|
expansions["VERSION"] = version
|
||||||
if python:
|
# 8.0+ Windows builds must run on win-latest
|
||||||
expansions["PYTHON_BINARY"] = get_python_binary(python, host)
|
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(
|
return create_variant_generic(
|
||||||
tasks, display_name, version=version, host=host, expansions=expansions, **kwargs
|
tasks, display_name, version=version, host=host, expansions=expansions, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_python_binary(python: str, host: Host) -> str:
|
|
||||||
"""Get the appropriate python binary given a python version and host."""
|
|
||||||
name = host.name
|
|
||||||
if name in ["win64", "win32"]:
|
|
||||||
if name == "win32":
|
|
||||||
base = "C:/python/32"
|
|
||||||
else:
|
|
||||||
base = "C:/python"
|
|
||||||
python = python.replace(".", "")
|
|
||||||
if python == "313t":
|
|
||||||
return f"{base}/Python313/python3.13t.exe"
|
|
||||||
return f"{base}/Python{python}/python.exe"
|
|
||||||
|
|
||||||
if name in ["rhel8", "ubuntu22", "ubuntu20", "rhel7"]:
|
|
||||||
return f"/opt/python/{python}/bin/python3"
|
|
||||||
|
|
||||||
if name in ["macos", "macos-arm64"]:
|
|
||||||
if python == "3.13t":
|
|
||||||
return "/Library/Frameworks/PythonT.Framework/Versions/3.13/bin/python3t"
|
|
||||||
return f"/Library/Frameworks/Python.Framework/Versions/{python}/bin/python3"
|
|
||||||
|
|
||||||
raise ValueError(f"no match found for python {python} on {name}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_versions_from(min_version: str) -> list[str]:
|
def get_versions_from(min_version: str) -> list[str]:
|
||||||
"""Get all server versions starting from a minimum version."""
|
"""Get all server versions starting from a minimum version."""
|
||||||
min_version_float = float(min_version)
|
min_version_float = float(min_version)
|
||||||
@ -197,12 +181,12 @@ def get_common_name(base: str, sep: str, **kwargs) -> str:
|
|||||||
display_name = f"{display_name}{sep}{version}"
|
display_name = f"{display_name}{sep}{version}"
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
name = value
|
name = value
|
||||||
if key.lower() == "python":
|
if key.lower() in ["python", "toolchain_version"]:
|
||||||
if not value.startswith("pypy"):
|
if not value.startswith("pypy"):
|
||||||
name = f"Python{value}"
|
name = f"Python{value}"
|
||||||
else:
|
else:
|
||||||
name = f"PyPy{value.replace('pypy', '')}"
|
name = f"PyPy{value.replace('pypy', '')}"
|
||||||
elif key.lower() in DISPLAY_LOOKUP:
|
elif key.lower() in DISPLAY_LOOKUP and value in DISPLAY_LOOKUP[key.lower()]:
|
||||||
name = DISPLAY_LOOKUP[key.lower()][value]
|
name = DISPLAY_LOOKUP[key.lower()][value]
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
@ -274,7 +258,7 @@ def generate_yaml(tasks=None, variants=None):
|
|||||||
out = ShrubService.generate_yaml(project)
|
out = ShrubService.generate_yaml(project)
|
||||||
# Dedent by two spaces to match what we use in config.yml
|
# Dedent by two spaces to match what we use in config.yml
|
||||||
lines = [line[2:] for line in out.splitlines()]
|
lines = [line[2:] for line in out.splitlines()]
|
||||||
print("\n".join(lines)) # noqa: T201
|
print("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
##################
|
##################
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Install the dependencies needed for an evergreen run.
|
# Install the necessary dependencies.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
HERE=$(dirname ${BASH_SOURCE:-$0})
|
HERE=$(dirname ${BASH_SOURCE:-$0})
|
||||||
@ -13,61 +13,24 @@ fi
|
|||||||
# Set up the default bin directory.
|
# Set up the default bin directory.
|
||||||
if [ -z "${PYMONGO_BIN_DIR:-}" ]; then
|
if [ -z "${PYMONGO_BIN_DIR:-}" ]; then
|
||||||
PYMONGO_BIN_DIR="$HOME/.local/bin"
|
PYMONGO_BIN_DIR="$HOME/.local/bin"
|
||||||
export PATH="$PYMONGO_BIN_DIR:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Helper function to pip install a dependency using a temporary python env.
|
|
||||||
function _pip_install() {
|
|
||||||
_HERE=$(dirname ${BASH_SOURCE:-$0})
|
|
||||||
. $_HERE/../utils.sh
|
|
||||||
_VENV_PATH=$(mktemp -d)
|
|
||||||
if [ "Windows_NT" = "${OS:-}" ]; then
|
|
||||||
_VENV_PATH=$(cygpath -m $_VENV_PATH)
|
|
||||||
fi
|
|
||||||
echo "Installing $2 using pip..."
|
|
||||||
createvirtualenv "$(find_python3)" $_VENV_PATH
|
|
||||||
python -m pip install $1
|
|
||||||
_suffix=""
|
|
||||||
if [ "Windows_NT" = "${OS:-}" ]; then
|
|
||||||
_suffix=".exe"
|
|
||||||
fi
|
|
||||||
ln -s "$(which $2)" $PYMONGO_BIN_DIR/${2}${_suffix}
|
|
||||||
# uv also comes with a uvx binary.
|
|
||||||
if [ $2 == "uv" ]; then
|
|
||||||
ln -s "$(which uvx)" $PYMONGO_BIN_DIR/uvx${_suffix}
|
|
||||||
fi
|
|
||||||
echo "Installed to ${PYMONGO_BIN_DIR}"
|
|
||||||
echo "Installing $2 using pip... done."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure just is installed.
|
|
||||||
if ! command -v just &>/dev/null; then
|
|
||||||
# On most systems we can install directly.
|
|
||||||
_TARGET=""
|
|
||||||
if [ "Windows_NT" = "${OS:-}" ]; then
|
|
||||||
_TARGET="--target x86_64-pc-windows-msvc"
|
|
||||||
fi
|
|
||||||
_BIN_DIR=$PYMONGO_BIN_DIR
|
|
||||||
echo "Installing just..."
|
|
||||||
mkdir -p "$_BIN_DIR" 2>/dev/null || true
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- $_TARGET --to "$_BIN_DIR" || {
|
|
||||||
_pip_install rust-just just
|
|
||||||
}
|
|
||||||
echo "Installing just... done."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure uv is installed.
|
# Ensure uv is installed.
|
||||||
if ! command -v uv &>/dev/null; then
|
if ! command -v uv &>/dev/null; then
|
||||||
_BIN_DIR=$PYMONGO_BIN_DIR
|
_BIN_DIR=$PYMONGO_BIN_DIR
|
||||||
|
mkdir -p ${_BIN_DIR}
|
||||||
echo "Installing uv..."
|
echo "Installing uv..."
|
||||||
# On most systems we can install directly.
|
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="$_BIN_DIR" INSTALLER_NO_MODIFY_PATH=1 sh
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="$_BIN_DIR" INSTALLER_NO_MODIFY_PATH=1 sh || {
|
|
||||||
_pip_install uv uv
|
|
||||||
}
|
|
||||||
if [ "Windows_NT" = "${OS:-}" ]; then
|
if [ "Windows_NT" = "${OS:-}" ]; then
|
||||||
chmod +x "$(cygpath -u $_BIN_DIR)/uv.exe"
|
chmod +x "$(cygpath -u $_BIN_DIR)/uv.exe"
|
||||||
fi
|
fi
|
||||||
|
export PATH="$PYMONGO_BIN_DIR:$PATH"
|
||||||
echo "Installing uv... done."
|
echo "Installing uv... done."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Ensure just is installed.
|
||||||
|
if ! command -v just &>/dev/null; then
|
||||||
|
uv tool install rust-just
|
||||||
|
fi
|
||||||
|
|
||||||
popd > /dev/null
|
popd > /dev/null
|
||||||
|
|||||||
@ -30,6 +30,9 @@ def _setup_azure_vm(base_env: dict[str, str]) -> None:
|
|||||||
env["AZUREKMS_CMD"] = "tar xf mongo-python-driver.tgz"
|
env["AZUREKMS_CMD"] = "tar xf mongo-python-driver.tgz"
|
||||||
run_command(f"{azure_dir}/run-command.sh", env=env)
|
run_command(f"{azure_dir}/run-command.sh", env=env)
|
||||||
|
|
||||||
|
env["AZUREKMS_CMD"] = "sudo apt-get install -y python3-dev build-essential"
|
||||||
|
run_command(f"{azure_dir}/run-command.sh", env=env)
|
||||||
|
|
||||||
env["AZUREKMS_CMD"] = "bash .evergreen/just.sh setup-tests kms azure-remote"
|
env["AZUREKMS_CMD"] = "bash .evergreen/just.sh setup-tests kms azure-remote"
|
||||||
run_command(f"{azure_dir}/run-command.sh", env=env)
|
run_command(f"{azure_dir}/run-command.sh", env=env)
|
||||||
LOGGER.info("Setting up Azure VM... done.")
|
LOGGER.info("Setting up Azure VM... done.")
|
||||||
@ -47,6 +50,9 @@ def _setup_gcp_vm(base_env: dict[str, str]) -> None:
|
|||||||
env["GCPKMS_CMD"] = "tar xf mongo-python-driver.tgz"
|
env["GCPKMS_CMD"] = "tar xf mongo-python-driver.tgz"
|
||||||
run_command(f"{gcp_dir}/run-command.sh", env=env)
|
run_command(f"{gcp_dir}/run-command.sh", env=env)
|
||||||
|
|
||||||
|
env["GCPKMS_CMD"] = "sudo apt-get install -y python3-dev build-essential"
|
||||||
|
run_command(f"{gcp_dir}/run-command.sh", env=env)
|
||||||
|
|
||||||
env["GCPKMS_CMD"] = "bash ./.evergreen/just.sh setup-tests kms gcp-remote"
|
env["GCPKMS_CMD"] = "bash ./.evergreen/just.sh setup-tests kms gcp-remote"
|
||||||
run_command(f"{gcp_dir}/run-command.sh", env=env)
|
run_command(f"{gcp_dir}/run-command.sh", env=env)
|
||||||
LOGGER.info("Setting up GCP VM...")
|
LOGGER.info("Setting up GCP VM...")
|
||||||
@ -92,6 +98,13 @@ def setup_kms(sub_test_name: str) -> None:
|
|||||||
if sub_test_target == "azure":
|
if sub_test_target == "azure":
|
||||||
os.environ["AZUREKMS_VMNAME_PREFIX"] = "PYTHON_DRIVER"
|
os.environ["AZUREKMS_VMNAME_PREFIX"] = "PYTHON_DRIVER"
|
||||||
|
|
||||||
|
# Found using "az vm image list --output table"
|
||||||
|
os.environ[
|
||||||
|
"AZUREKMS_IMAGE"
|
||||||
|
] = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest"
|
||||||
|
else:
|
||||||
|
os.environ["GCPKMS_IMAGEFAMILY"] = "debian-12"
|
||||||
|
|
||||||
run_command("./setup.sh", cwd=kms_dir)
|
run_command("./setup.sh", cwd=kms_dir)
|
||||||
base_env = _load_kms_config(sub_test_target)
|
base_env = _load_kms_config(sub_test_target)
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from utils import DRIVERS_TOOLS, TMP_DRIVER_FILE, create_archive, read_env, run_command, write_env
|
from utils import (
|
||||||
|
DRIVERS_TOOLS,
|
||||||
|
TMP_DRIVER_FILE,
|
||||||
|
create_archive,
|
||||||
|
read_env,
|
||||||
|
run_command,
|
||||||
|
write_env,
|
||||||
|
)
|
||||||
|
|
||||||
K8S_NAMES = ["aks", "gke", "eks"]
|
K8S_NAMES = ["aks", "gke", "eks"]
|
||||||
K8S_REMOTE_NAMES = [f"{n}-remote" for n in K8S_NAMES]
|
K8S_REMOTE_NAMES = [f"{n}-remote" for n in K8S_NAMES]
|
||||||
@ -35,6 +42,11 @@ def setup_oidc(sub_test_name: str) -> dict[str, str] | None:
|
|||||||
if sub_test_name == "azure":
|
if sub_test_name == "azure":
|
||||||
env["AZUREOIDC_VMNAME_PREFIX"] = "PYTHON_DRIVER"
|
env["AZUREOIDC_VMNAME_PREFIX"] = "PYTHON_DRIVER"
|
||||||
if "-remote" not in sub_test_name:
|
if "-remote" not in sub_test_name:
|
||||||
|
if sub_test_name == "azure":
|
||||||
|
# Found using "az vm image list --output table"
|
||||||
|
env["AZUREOIDC_IMAGE"] = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest"
|
||||||
|
else:
|
||||||
|
env["GCPKMS_IMAGEFAMILY"] = "debian-12"
|
||||||
run_command(f"bash {target_dir}/setup.sh", env=env)
|
run_command(f"bash {target_dir}/setup.sh", env=env)
|
||||||
if sub_test_name in K8S_NAMES:
|
if sub_test_name in K8S_NAMES:
|
||||||
run_command(f"bash {target_dir}/setup-pod.sh {sub_test_name}")
|
run_command(f"bash {target_dir}/setup-pod.sh {sub_test_name}")
|
||||||
|
|||||||
150
.evergreen/scripts/resync-all-specs.py
Normal file
150
.evergreen/scripts/resync-all-specs.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
def resync_specs(directory: pathlib.Path, errored: dict[str, str]) -> None:
|
||||||
|
"""Actually sync the specs"""
|
||||||
|
print("Beginning to sync specs")
|
||||||
|
for spec in os.scandir(directory):
|
||||||
|
if not spec.is_dir():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if spec.name in ["asynchronous"]:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["bash", "./.evergreen/resync-specs.sh", spec.name], # noqa: S603, S607
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except CalledProcessError as exc:
|
||||||
|
errored[spec.name] = exc.stderr
|
||||||
|
print("Done syncing specs")
|
||||||
|
|
||||||
|
|
||||||
|
def apply_patches(errored):
|
||||||
|
print("Beginning to apply patches")
|
||||||
|
subprocess.run(
|
||||||
|
["bash", "./.evergreen/remove-unimplemented-tests.sh"], # noqa: S603, S607
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
except CalledProcessError as exc:
|
||||||
|
errored["applying patches"] = exc.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def check_new_spec_directories(directory: pathlib.Path) -> list[str]:
|
||||||
|
"""Check to see if there are any directories in the spec repo that don't exist in pymongo/test"""
|
||||||
|
spec_dir = pathlib.Path(os.environ["MDB_SPECS"]) / "source"
|
||||||
|
spec_set = {
|
||||||
|
entry.name.replace("-", "_")
|
||||||
|
for entry in os.scandir(spec_dir)
|
||||||
|
if entry.is_dir()
|
||||||
|
and (pathlib.Path(entry.path) / "tests").is_dir()
|
||||||
|
and len(list(os.scandir(pathlib.Path(entry.path) / "tests"))) > 1
|
||||||
|
}
|
||||||
|
test_set = {entry.name.replace("-", "_") for entry in os.scandir(directory) if entry.is_dir()}
|
||||||
|
known_mappings = {
|
||||||
|
"ocsp_support": "ocsp",
|
||||||
|
"client_side_operations_timeout": "csot",
|
||||||
|
"mongodb_handshake": "handshake",
|
||||||
|
"load_balancers": "load_balancer",
|
||||||
|
"connection_monitoring_and_pooling": "connection_monitoring",
|
||||||
|
"command_logging_and_monitoring": "command_logging",
|
||||||
|
"initial_dns_seedlist_discovery": "srv_seedlist",
|
||||||
|
"server_discovery_and_monitoring": "sdam_monitoring",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v in known_mappings.items():
|
||||||
|
if k in spec_set:
|
||||||
|
spec_set.remove(k)
|
||||||
|
spec_set.add(v)
|
||||||
|
return list(spec_set - test_set)
|
||||||
|
|
||||||
|
|
||||||
|
def write_summary(errored: dict[str, str], new: list[str], filename: str | None) -> 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
|
||||||
|
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)
|
||||||
|
|
||||||
|
if len(succeeded) > 0:
|
||||||
|
pr_body += "The following specs were changed:\n -"
|
||||||
|
pr_body += "\n -".join(succeeded)
|
||||||
|
pr_body += "\n"
|
||||||
|
if len(errored) > 0:
|
||||||
|
pr_body += "\n\nThe following spec syncs encountered errors:"
|
||||||
|
for k, v in errored.items():
|
||||||
|
pr_body += f"\n -{k}\n```{v}\n```"
|
||||||
|
pr_body += "\n"
|
||||||
|
if len(new) > 0:
|
||||||
|
pr_body += "\n\nThe following directories are in the specification repository and not in our test directory:\n -"
|
||||||
|
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:
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
# replacements made for proper json
|
||||||
|
f.write(pr_body.replace("\n", "\\n").replace("\t", "\\t"))
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: Namespace):
|
||||||
|
directory = pathlib.Path("./test")
|
||||||
|
errored: dict[str, str] = {}
|
||||||
|
resync_specs(directory, errored)
|
||||||
|
apply_patches(errored)
|
||||||
|
new = check_new_spec_directories(directory)
|
||||||
|
write_summary(errored, new, args.filename)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
main(args)
|
||||||
43
.evergreen/scripts/resync-all-specs.sh
Executable file
43
.evergreen/scripts/resync-all-specs.sh
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run spec syncing script and create PR
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# SETUP
|
||||||
|
SRC_URL="https://github.com/mongodb/specifications.git"
|
||||||
|
# needs to be set for resync-specs.sh
|
||||||
|
SPEC_SRC="$(realpath "../specifications")"
|
||||||
|
SCRIPT="$(realpath "./.evergreen/resync-specs.sh")"
|
||||||
|
|
||||||
|
# Clone the spec repo if the directory does not exist
|
||||||
|
if [[ ! -d $SPEC_SRC ]]; then
|
||||||
|
git clone $SRC_URL $SPEC_SRC
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo "Error: Failed to clone repository."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set environment variable to the cloned spec repo for resync-specs.sh
|
||||||
|
export MDB_SPECS="$SPEC_SRC"
|
||||||
|
|
||||||
|
# Check that resync-specs.sh exists and is executable
|
||||||
|
if [[ ! -x $SCRIPT ]]; then
|
||||||
|
echo "Error: $SCRIPT not found or is not executable."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PR_DESC="spec_sync.txt"
|
||||||
|
|
||||||
|
# run python script that actually does all the resyncing
|
||||||
|
if ! [ -n "${CI:-}" ]
|
||||||
|
then
|
||||||
|
# we're running locally
|
||||||
|
python3 ./.evergreen/scripts/resync-all-specs.py
|
||||||
|
else
|
||||||
|
/opt/devtools/bin/python3.11 ./.evergreen/scripts/resync-all-specs.py --filename "$PR_DESC"
|
||||||
|
if [[ -f $PR_DESC ]]; then
|
||||||
|
# changes were made -> call scrypt to create PR for us
|
||||||
|
.evergreen/scripts/create-spec-pr.sh "$PR_DESC"
|
||||||
|
rm "$PR_DESC"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@ -12,7 +12,7 @@ def set_env(name: str, value: Any = "1") -> None:
|
|||||||
|
|
||||||
def start_server():
|
def start_server():
|
||||||
opts, extra_opts = get_test_options(
|
opts, extra_opts = get_test_options(
|
||||||
"Run a MongoDB server. All given flags will be passed to run-orchestration.sh in DRIVERS_TOOLS.",
|
"Run a MongoDB server. All given flags will be passed to run-mongodb.sh in DRIVERS_TOOLS.",
|
||||||
require_sub_test_name=False,
|
require_sub_test_name=False,
|
||||||
allow_extra_opts=True,
|
allow_extra_opts=True,
|
||||||
)
|
)
|
||||||
@ -51,7 +51,7 @@ def start_server():
|
|||||||
elif opts.quiet:
|
elif opts.quiet:
|
||||||
extra_opts.append("-q")
|
extra_opts.append("-q")
|
||||||
|
|
||||||
cmd = ["bash", f"{DRIVERS_TOOLS}/.evergreen/run-orchestration.sh", *extra_opts]
|
cmd = ["bash", f"{DRIVERS_TOOLS}/.evergreen/run-mongodb.sh", "start", *extra_opts]
|
||||||
run_command(cmd, cwd=DRIVERS_TOOLS)
|
run_command(cmd, cwd=DRIVERS_TOOLS)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,12 +4,20 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import which
|
from shutil import which
|
||||||
|
|
||||||
|
try:
|
||||||
|
import importlib_metadata
|
||||||
|
except ImportError:
|
||||||
|
from importlib import metadata as importlib_metadata
|
||||||
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from utils import DRIVERS_TOOLS, LOGGER, ROOT, run_command
|
from utils import DRIVERS_TOOLS, LOGGER, ROOT, run_command
|
||||||
|
|
||||||
@ -23,6 +31,22 @@ TEST_NAME = os.environ.get("TEST_NAME")
|
|||||||
SUB_TEST_NAME = os.environ.get("SUB_TEST_NAME")
|
SUB_TEST_NAME = os.environ.get("SUB_TEST_NAME")
|
||||||
|
|
||||||
|
|
||||||
|
def list_packages():
|
||||||
|
packages = set()
|
||||||
|
for distribution in importlib_metadata.distributions():
|
||||||
|
if distribution.name:
|
||||||
|
packages.add(distribution.name)
|
||||||
|
print("Package Version URL")
|
||||||
|
print("------------------- ----------- ----------------------------------------------------")
|
||||||
|
for name in sorted(packages):
|
||||||
|
distribution = importlib_metadata.distribution(name)
|
||||||
|
url = ""
|
||||||
|
if distribution.origin is not None:
|
||||||
|
url = distribution.origin.url
|
||||||
|
print(f"{name:20s}{distribution.version:12s}{url}")
|
||||||
|
print("------------------- ----------- ----------------------------------------------------\n")
|
||||||
|
|
||||||
|
|
||||||
def handle_perf(start_time: datetime):
|
def handle_perf(start_time: datetime):
|
||||||
end_time = datetime.now()
|
end_time = datetime.now()
|
||||||
elapsed_secs = (end_time - start_time).total_seconds()
|
elapsed_secs = (end_time - start_time).total_seconds()
|
||||||
@ -46,13 +70,7 @@ def handle_perf(start_time: datetime):
|
|||||||
|
|
||||||
|
|
||||||
def handle_green_framework() -> None:
|
def handle_green_framework() -> None:
|
||||||
if GREEN_FRAMEWORK == "eventlet":
|
if GREEN_FRAMEWORK == "gevent":
|
||||||
import eventlet
|
|
||||||
|
|
||||||
# https://github.com/eventlet/eventlet/issues/401
|
|
||||||
eventlet.sleep()
|
|
||||||
eventlet.monkey_patch()
|
|
||||||
elif GREEN_FRAMEWORK == "gevent":
|
|
||||||
from gevent import monkey
|
from gevent import monkey
|
||||||
|
|
||||||
monkey.patch_all()
|
monkey.patch_all()
|
||||||
@ -90,10 +108,11 @@ def handle_aws_lambda() -> None:
|
|||||||
env["TEST_LAMBDA_DIRECTORY"] = str(target_dir)
|
env["TEST_LAMBDA_DIRECTORY"] = str(target_dir)
|
||||||
env.setdefault("AWS_REGION", "us-east-1")
|
env.setdefault("AWS_REGION", "us-east-1")
|
||||||
dirs = ["pymongo", "gridfs", "bson"]
|
dirs = ["pymongo", "gridfs", "bson"]
|
||||||
# Store the original .so files.
|
# Remove the original .so files.
|
||||||
before_sos = []
|
|
||||||
for dname in dirs:
|
for dname in dirs:
|
||||||
before_sos.extend(f"{f.parent.name}/{f.name}" for f in (ROOT / dname).glob("*.so"))
|
so_paths = [f"{f.parent.name}/{f.name}" for f in (ROOT / dname).glob("*.so")]
|
||||||
|
for so_path in list(so_paths):
|
||||||
|
Path(so_path).unlink()
|
||||||
# Build the c extensions.
|
# Build the c extensions.
|
||||||
docker = which("docker") or which("podman")
|
docker = which("docker") or which("podman")
|
||||||
if not docker:
|
if not docker:
|
||||||
@ -106,21 +125,23 @@ def handle_aws_lambda() -> None:
|
|||||||
target = ROOT / "test/lambda/mongodb" / dname
|
target = ROOT / "test/lambda/mongodb" / dname
|
||||||
shutil.rmtree(target, ignore_errors=True)
|
shutil.rmtree(target, ignore_errors=True)
|
||||||
shutil.copytree(ROOT / dname, target)
|
shutil.copytree(ROOT / dname, target)
|
||||||
# Remove the original so files from the lambda directory.
|
|
||||||
for so_path in before_sos:
|
|
||||||
(ROOT / "test/lambda/mongodb" / so_path).unlink()
|
|
||||||
# Remove the new so files from the ROOT directory.
|
# Remove the new so files from the ROOT directory.
|
||||||
for dname in dirs:
|
for dname in dirs:
|
||||||
so_paths = [f"{f.parent.name}/{f.name}" for f in (ROOT / dname).glob("*.so")]
|
so_paths = [f"{f.parent.name}/{f.name}" for f in (ROOT / dname).glob("*.so")]
|
||||||
for so_path in list(so_paths):
|
for so_path in list(so_paths):
|
||||||
if so_path not in before_sos:
|
Path(so_path).unlink()
|
||||||
Path(so_path).unlink()
|
|
||||||
|
|
||||||
script_name = "run-deployed-lambda-aws-tests.sh"
|
script_name = "run-deployed-lambda-aws-tests.sh"
|
||||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/aws_lambda/{script_name}", env=env)
|
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/aws_lambda/{script_name}", env=env)
|
||||||
|
|
||||||
|
|
||||||
def run() -> None:
|
def run() -> None:
|
||||||
|
# Add diagnostic for python version.
|
||||||
|
print("Running with python", sys.version)
|
||||||
|
|
||||||
|
# List the installed packages.
|
||||||
|
list_packages()
|
||||||
|
|
||||||
# Handle green framework first so they can patch modules.
|
# Handle green framework first so they can patch modules.
|
||||||
if GREEN_FRAMEWORK:
|
if GREEN_FRAMEWORK:
|
||||||
handle_green_framework()
|
handle_green_framework()
|
||||||
@ -183,6 +204,16 @@ def run() -> None:
|
|||||||
if os.environ.get("DEBUG_LOG"):
|
if os.environ.get("DEBUG_LOG"):
|
||||||
TEST_ARGS.extend(f"-o log_cli_level={logging.DEBUG}".split())
|
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.
|
# Run local tests.
|
||||||
ret = pytest.main(TEST_ARGS + sys.argv[1:])
|
ret = pytest.main(TEST_ARGS + sys.argv[1:])
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Set up a development environment on an evergreen host.
|
# Set up development environment.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
HERE=$(dirname ${BASH_SOURCE:-$0})
|
HERE=$(dirname ${BASH_SOURCE:-$0})
|
||||||
HERE="$( cd -- "$HERE" > /dev/null 2>&1 && pwd )"
|
HERE="$( cd -- "$HERE" > /dev/null 2>&1 && pwd )"
|
||||||
ROOT=$(dirname "$(dirname $HERE)")
|
ROOT=$(dirname "$(dirname $HERE)")
|
||||||
pushd $ROOT > /dev/null
|
|
||||||
|
|
||||||
# Source the env files to pick up common variables.
|
# Source the env files to pick up common variables.
|
||||||
if [ -f $HERE/env.sh ]; then
|
if [ -f $HERE/env.sh ]; then
|
||||||
. $HERE/env.sh
|
. $HERE/env.sh
|
||||||
fi
|
fi
|
||||||
# PYTHON_BINARY or PYTHON_VERSION may be defined in test-env.sh.
|
|
||||||
|
# Get variables defined in test-env.sh.
|
||||||
if [ -f $HERE/test-env.sh ]; then
|
if [ -f $HERE/test-env.sh ]; then
|
||||||
. $HERE/test-env.sh
|
. $HERE/test-env.sh
|
||||||
fi
|
fi
|
||||||
@ -19,41 +19,40 @@ fi
|
|||||||
# Ensure dependencies are installed.
|
# Ensure dependencies are installed.
|
||||||
bash $HERE/install-dependencies.sh
|
bash $HERE/install-dependencies.sh
|
||||||
|
|
||||||
# Get the appropriate UV_PYTHON.
|
# Handle the value for UV_PYTHON.
|
||||||
. $ROOT/.evergreen/utils.sh
|
. $HERE/setup-uv-python.sh
|
||||||
|
|
||||||
if [ -z "${PYTHON_BINARY:-}" ]; then
|
# Only run the next part if not running on CI.
|
||||||
if [ -n "${PYTHON_VERSION:-}" ]; then
|
if [ -z "${CI:-}" ]; then
|
||||||
PYTHON_BINARY=$(get_python_binary $PYTHON_VERSION)
|
# Add the default install path to the path if needed.
|
||||||
else
|
if [ -z "${PYMONGO_BIN_DIR:-}" ]; then
|
||||||
PYTHON_BINARY=$(find_python3)
|
export PATH="$PATH:$HOME/.local/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up venv, making sure c extensions build unless disabled.
|
||||||
|
if [ -z "${NO_EXT:-}" ]; then
|
||||||
|
export PYMONGO_C_EXT_MUST_BUILD=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
cd $ROOT && uv sync
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up build utilities on Windows spawn hosts.
|
||||||
|
if [ -f $HOME/.visualStudioEnv.sh ]; then
|
||||||
|
set +u
|
||||||
|
SSH_TTY=1 source $HOME/.visualStudioEnv.sh
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only set up pre-commit if we are in a git checkout.
|
||||||
|
if [ -f $HERE/.git ]; then
|
||||||
|
if ! command -v pre-commit &>/dev/null; then
|
||||||
|
uv tool install pre-commit
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
export UV_PYTHON=${PYTHON_BINARY}
|
|
||||||
echo "Using python $UV_PYTHON"
|
|
||||||
|
|
||||||
# Add the default install path to the path if needed.
|
if [ ! -f .git/hooks/pre-commit ]; then
|
||||||
if [ -z "${PYMONGO_BIN_DIR:-}" ]; then
|
uvx pre-commit install
|
||||||
export PATH="$PATH:$HOME/.local/bin"
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up venv, making sure c extensions build unless disabled.
|
|
||||||
if [ -z "${NO_EXT:-}" ]; then
|
|
||||||
export PYMONGO_C_EXT_MUST_BUILD=1
|
|
||||||
fi
|
|
||||||
# Set up visual studio env on Windows spawn hosts.
|
|
||||||
if [ -f $HOME/.visualStudioEnv.sh ]; then
|
|
||||||
set +u
|
|
||||||
SSH_TTY=1 source $HOME/.visualStudioEnv.sh
|
|
||||||
set -u
|
|
||||||
fi
|
|
||||||
uv sync --frozen
|
|
||||||
|
|
||||||
echo "Setting up python environment... done."
|
|
||||||
|
|
||||||
# Ensure there is a pre-commit hook if there is a git checkout.
|
|
||||||
if [ -d .git ] && [ ! -f .git/hooks/pre-commit ]; then
|
|
||||||
uv run --frozen pre-commit install
|
|
||||||
fi
|
|
||||||
|
|
||||||
popd > /dev/null
|
|
||||||
|
|||||||
@ -8,9 +8,13 @@ echo "Setting up system..."
|
|||||||
bash .evergreen/scripts/configure-env.sh
|
bash .evergreen/scripts/configure-env.sh
|
||||||
source .evergreen/scripts/env.sh
|
source .evergreen/scripts/env.sh
|
||||||
bash $DRIVERS_TOOLS/.evergreen/setup.sh
|
bash $DRIVERS_TOOLS/.evergreen/setup.sh
|
||||||
bash .evergreen/scripts/install-dependencies.sh
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
# Run spawn host-specific tasks.
|
||||||
|
if [ -z "${CI:-}" ]; then
|
||||||
|
bash $HERE/setup-dev-env.sh
|
||||||
|
fi
|
||||||
|
|
||||||
# Enable core dumps if enabled on the machine
|
# Enable core dumps if enabled on the machine
|
||||||
# Copied from https://github.com/mongodb/mongo/blob/master/etc/evergreen.yml
|
# Copied from https://github.com/mongodb/mongo/blob/master/etc/evergreen.yml
|
||||||
if [ -f /proc/self/coredump_filter ]; then
|
if [ -f /proc/self/coredump_filter ]; then
|
||||||
@ -38,4 +42,14 @@ if [ "$(uname -s)" = "Darwin" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -w /etc/hosts ]; then
|
||||||
|
SUDO=""
|
||||||
|
else
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add 'server' and 'hostname_not_in_cert' as a hostnames
|
||||||
|
echo "127.0.0.1 server" | $SUDO tee -a /etc/hosts
|
||||||
|
echo "127.0.0.1 hostname_not_in_cert" | $SUDO tee -a /etc/hosts
|
||||||
|
|
||||||
echo "Setting up system... done."
|
echo "Setting up system... done."
|
||||||
|
|||||||
@ -12,6 +12,7 @@ set -eu
|
|||||||
# TEST_CRYPT_SHARED If non-empty, install crypt_shared lib.
|
# TEST_CRYPT_SHARED If non-empty, install crypt_shared lib.
|
||||||
# MONGODB_API_VERSION The mongodb api version to use in tests.
|
# MONGODB_API_VERSION The mongodb api version to use in tests.
|
||||||
# MONGODB_URI If non-empty, use as the MONGODB_URI 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})
|
SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0})
|
||||||
|
|
||||||
@ -21,5 +22,5 @@ if [ -f $SCRIPT_DIR/env.sh ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Setting up tests with args \"$*\"..."
|
echo "Setting up tests with args \"$*\"..."
|
||||||
uv run $SCRIPT_DIR/setup_tests.py "$@"
|
uv run ${USE_ACTIVE_VENV:+--active} "$SCRIPT_DIR/setup_tests.py" "$@"
|
||||||
echo "Setting up tests with args \"$*\"... done."
|
echo "Setting up tests with args \"$*\"... done."
|
||||||
|
|||||||
53
.evergreen/scripts/setup-uv-python.sh
Executable file
53
.evergreen/scripts/setup-uv-python.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Set up the UV_PYTHON variable.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
HERE=$(dirname ${BASH_SOURCE:-$0})
|
||||||
|
HERE="$( cd -- "$HERE" > /dev/null 2>&1 && pwd )"
|
||||||
|
|
||||||
|
# Use min supported version by default.
|
||||||
|
_python="3.10"
|
||||||
|
|
||||||
|
# Source the env files to pick up common variables.
|
||||||
|
if [ -f $HERE/env.sh ]; then
|
||||||
|
. $HERE/env.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get variables defined in test-env.sh.
|
||||||
|
if [ -f $HERE/test-env.sh ]; then
|
||||||
|
. $HERE/test-env.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${UV_PYTHON:-}" ]; then
|
||||||
|
set -x
|
||||||
|
# Translate a TOOLCHAIN_VERSION to UV_PYTHON.
|
||||||
|
if [ -n "${TOOLCHAIN_VERSION:-}" ]; then
|
||||||
|
_python=$TOOLCHAIN_VERSION
|
||||||
|
if [ "$(uname -s)" = "Darwin" ]; then
|
||||||
|
if [[ "$_python" == *"t"* ]]; then
|
||||||
|
binary_name="python3t"
|
||||||
|
framework_dir="PythonT"
|
||||||
|
else
|
||||||
|
binary_name="python3"
|
||||||
|
framework_dir="Python"
|
||||||
|
fi
|
||||||
|
_python=$(echo "$_python" | sed 's/t//g')
|
||||||
|
_python="/Library/Frameworks/$framework_dir.Framework/Versions/$_python/bin/$binary_name"
|
||||||
|
elif [ "Windows_NT" = "${OS:-}" ]; then
|
||||||
|
_python=$(echo $_python | cut -d. -f1,2 | sed 's/\.//g; s/t//g')
|
||||||
|
if [[ "$TOOLCHAIN_VERSION" == *"t"* ]]; then
|
||||||
|
_exe="python${TOOLCHAIN_VERSION}.exe"
|
||||||
|
else
|
||||||
|
_exe="python.exe"
|
||||||
|
fi
|
||||||
|
if [ -n "${IS_WIN32:-}" ]; then
|
||||||
|
_python="C:/python/32/Python${_python}/${_exe}"
|
||||||
|
else
|
||||||
|
_python="C:/python/Python${_python}/${_exe}"
|
||||||
|
fi
|
||||||
|
elif [ -d "/opt/python/$_python/bin" ]; then
|
||||||
|
_python="/opt/python/$_python/bin/python3"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
export UV_PYTHON="$_python"
|
||||||
|
fi
|
||||||
@ -1,12 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import tarfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
@ -31,8 +29,7 @@ PASS_THROUGH_ENV = [
|
|||||||
"NO_EXT",
|
"NO_EXT",
|
||||||
"MONGODB_API_VERSION",
|
"MONGODB_API_VERSION",
|
||||||
"DEBUG_LOG",
|
"DEBUG_LOG",
|
||||||
"PYTHON_BINARY",
|
"UV_PYTHON",
|
||||||
"PYTHON_VERSION",
|
|
||||||
"REQUIRE_FIPS",
|
"REQUIRE_FIPS",
|
||||||
"IS_WIN32",
|
"IS_WIN32",
|
||||||
]
|
]
|
||||||
@ -53,7 +50,7 @@ EXTRAS_MAP = {
|
|||||||
GROUP_MAP = dict(mockupdb="mockupdb", perf="perf")
|
GROUP_MAP = dict(mockupdb="mockupdb", perf="perf")
|
||||||
|
|
||||||
# The python version used for perf tests.
|
# The python version used for perf tests.
|
||||||
PERF_PYTHON_VERSION = "3.9.13"
|
PERF_PYTHON_VERSION = "3.10.11"
|
||||||
|
|
||||||
|
|
||||||
def is_set(var: str) -> bool:
|
def is_set(var: str) -> bool:
|
||||||
@ -90,6 +87,13 @@ def setup_libmongocrypt():
|
|||||||
distro = get_distro()
|
distro = get_distro()
|
||||||
if distro.name.startswith("Debian"):
|
if distro.name.startswith("Debian"):
|
||||||
target = f"debian{distro.version_id}"
|
target = f"debian{distro.version_id}"
|
||||||
|
elif distro.name.startswith("Ubuntu"):
|
||||||
|
if distro.version_id == "20.04":
|
||||||
|
target = "debian11"
|
||||||
|
elif distro.version_id == "22.04":
|
||||||
|
target = "debian12"
|
||||||
|
elif distro.version_id == "24.04":
|
||||||
|
target = "debian13"
|
||||||
elif distro.name.startswith("Red Hat"):
|
elif distro.name.startswith("Red Hat"):
|
||||||
if distro.version_id.startswith("7"):
|
if distro.version_id.startswith("7"):
|
||||||
target = "rhel-70-64-bit"
|
target = "rhel-70-64-bit"
|
||||||
@ -111,9 +115,10 @@ def setup_libmongocrypt():
|
|||||||
LOGGER.info(f"Fetching {url}...")
|
LOGGER.info(f"Fetching {url}...")
|
||||||
with request.urlopen(request.Request(url), timeout=15.0) as response: # noqa: S310
|
with request.urlopen(request.Request(url), timeout=15.0) as response: # noqa: S310
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
fileobj = io.BytesIO(response.read())
|
with Path("libmongocrypt.tar.gz").open("wb") as f:
|
||||||
with tarfile.open("libmongocrypt.tar.gz", fileobj=fileobj) as fid:
|
f.write(response.read())
|
||||||
fid.extractall(Path.cwd() / "libmongocrypt")
|
Path("libmongocrypt").mkdir()
|
||||||
|
run_command("tar -xzf libmongocrypt.tar.gz -C libmongocrypt")
|
||||||
LOGGER.info(f"Fetching {url}... done.")
|
LOGGER.info(f"Fetching {url}... done.")
|
||||||
|
|
||||||
run_command("ls -la libmongocrypt")
|
run_command("ls -la libmongocrypt")
|
||||||
@ -148,6 +153,10 @@ def handle_test_env() -> None:
|
|||||||
# Start compiling the args we'll pass to uv.
|
# Start compiling the args we'll pass to uv.
|
||||||
UV_ARGS = ["--extra test --no-group dev"]
|
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
|
test_title = test_name
|
||||||
if sub_test_name:
|
if sub_test_name:
|
||||||
test_title += f" {sub_test_name}"
|
test_title += f" {sub_test_name}"
|
||||||
@ -160,11 +169,6 @@ def handle_test_env() -> None:
|
|||||||
|
|
||||||
write_env("PIP_QUIET") # Quiet by default.
|
write_env("PIP_QUIET") # Quiet by default.
|
||||||
write_env("PIP_PREFER_BINARY") # Prefer binary dists by default.
|
write_env("PIP_PREFER_BINARY") # Prefer binary dists by default.
|
||||||
write_env("UV_FROZEN") # Do not modify lock files.
|
|
||||||
|
|
||||||
# Skip CSOT tests on non-linux platforms.
|
|
||||||
if PLATFORM != "linux":
|
|
||||||
write_env("SKIP_CSOT_TESTS")
|
|
||||||
|
|
||||||
# Set an environment variable for the test name and sub test name.
|
# Set an environment variable for the test name and sub test name.
|
||||||
write_env(f"TEST_{test_name.upper()}")
|
write_env(f"TEST_{test_name.upper()}")
|
||||||
@ -182,6 +186,9 @@ def handle_test_env() -> None:
|
|||||||
if group := GROUP_MAP.get(test_name, ""):
|
if group := GROUP_MAP.get(test_name, ""):
|
||||||
UV_ARGS.append(f"--group {group}")
|
UV_ARGS.append(f"--group {group}")
|
||||||
|
|
||||||
|
if opts.test_min_deps:
|
||||||
|
UV_ARGS.append("--resolution=lowest-direct")
|
||||||
|
|
||||||
if test_name == "auth_oidc":
|
if test_name == "auth_oidc":
|
||||||
from oidc_tester import setup_oidc
|
from oidc_tester import setup_oidc
|
||||||
|
|
||||||
@ -218,26 +225,8 @@ def handle_test_env() -> None:
|
|||||||
if key in os.environ:
|
if key in os.environ:
|
||||||
write_env(key, os.environ[key])
|
write_env(key, os.environ[key])
|
||||||
|
|
||||||
if test_name == "data_lake":
|
|
||||||
# Stop any running mongo-orchestration which might be using the port.
|
|
||||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/stop-orchestration.sh")
|
|
||||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/atlas_data_lake/setup.sh")
|
|
||||||
AUTH = "auth"
|
|
||||||
|
|
||||||
if AUTH != "noauth":
|
if AUTH != "noauth":
|
||||||
if test_name == "data_lake":
|
if test_name == "auth_oidc":
|
||||||
config = read_env(f"{DRIVERS_TOOLS}/.evergreen/atlas_data_lake/secrets-export.sh")
|
|
||||||
DB_USER = config["ADL_USERNAME"]
|
|
||||||
DB_PASSWORD = config["ADL_PASSWORD"]
|
|
||||||
elif test_name == "serverless":
|
|
||||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/serverless/setup.sh")
|
|
||||||
config = read_env(f"{DRIVERS_TOOLS}/.evergreen/serverless/secrets-export.sh")
|
|
||||||
DB_USER = config["SERVERLESS_ATLAS_USER"]
|
|
||||||
DB_PASSWORD = config["SERVERLESS_ATLAS_PASSWORD"]
|
|
||||||
write_env("MONGODB_URI", config["SERVERLESS_URI"])
|
|
||||||
write_env("SINGLE_MONGOS_LB_URI", config["SERVERLESS_URI"])
|
|
||||||
write_env("MULTI_MONGOS_LB_URI", config["SERVERLESS_URI"])
|
|
||||||
elif test_name == "auth_oidc":
|
|
||||||
DB_USER = config["OIDC_ADMIN_USER"]
|
DB_USER = config["OIDC_ADMIN_USER"]
|
||||||
DB_PASSWORD = config["OIDC_ADMIN_PWD"]
|
DB_PASSWORD = config["OIDC_ADMIN_PWD"]
|
||||||
elif test_name == "search_index":
|
elif test_name == "search_index":
|
||||||
@ -255,7 +244,7 @@ def handle_test_env() -> None:
|
|||||||
if is_set("MONGODB_URI"):
|
if is_set("MONGODB_URI"):
|
||||||
write_env("PYMONGO_MUST_CONNECT", "true")
|
write_env("PYMONGO_MUST_CONNECT", "true")
|
||||||
|
|
||||||
if is_set("DISABLE_TEST_COMMANDS") or opts.disable_test_commands:
|
if opts.disable_test_commands:
|
||||||
write_env("PYMONGO_DISABLE_TEST_COMMANDS", "1")
|
write_env("PYMONGO_DISABLE_TEST_COMMANDS", "1")
|
||||||
|
|
||||||
if test_name == "enterprise_auth":
|
if test_name == "enterprise_auth":
|
||||||
@ -339,7 +328,8 @@ def handle_test_env() -> None:
|
|||||||
version = os.environ.get("VERSION", "latest")
|
version = os.environ.get("VERSION", "latest")
|
||||||
cmd = [
|
cmd = [
|
||||||
"bash",
|
"bash",
|
||||||
f"{DRIVERS_TOOLS}/.evergreen/run-orchestration.sh",
|
f"{DRIVERS_TOOLS}/.evergreen/run-mongodb.sh",
|
||||||
|
"start",
|
||||||
"--ssl",
|
"--ssl",
|
||||||
"--version",
|
"--version",
|
||||||
version,
|
version,
|
||||||
@ -367,8 +357,10 @@ def handle_test_env() -> None:
|
|||||||
if not (ROOT / "libmongocrypt").exists():
|
if not (ROOT / "libmongocrypt").exists():
|
||||||
setup_libmongocrypt()
|
setup_libmongocrypt()
|
||||||
|
|
||||||
# TODO: Test with 'pip install pymongocrypt'
|
if not opts.test_min_deps:
|
||||||
UV_ARGS.append("--group pymongocrypt_source")
|
UV_ARGS.append(
|
||||||
|
"--with pymongocrypt@git+https://github.com/mongodb/libmongocrypt@master#subdirectory=bindings/python"
|
||||||
|
)
|
||||||
|
|
||||||
# Use the nocrypto build to avoid dependency issues with older windows/python versions.
|
# Use the nocrypto build to avoid dependency issues with older windows/python versions.
|
||||||
BASE = ROOT / "libmongocrypt/nocrypto"
|
BASE = ROOT / "libmongocrypt/nocrypto"
|
||||||
@ -397,7 +389,7 @@ def handle_test_env() -> None:
|
|||||||
if sub_test_name == "pyopenssl":
|
if sub_test_name == "pyopenssl":
|
||||||
UV_ARGS.append("--extra ocsp")
|
UV_ARGS.append("--extra ocsp")
|
||||||
|
|
||||||
if is_set("TEST_CRYPT_SHARED") or opts.crypt_shared:
|
if opts.crypt_shared:
|
||||||
config = read_env(f"{DRIVERS_TOOLS}/mo-expansion.sh")
|
config = read_env(f"{DRIVERS_TOOLS}/mo-expansion.sh")
|
||||||
CRYPT_SHARED_DIR = Path(config["CRYPT_SHARED_LIB_PATH"]).parent.as_posix()
|
CRYPT_SHARED_DIR = Path(config["CRYPT_SHARED_LIB_PATH"]).parent.as_posix()
|
||||||
LOGGER.info("Using crypt_shared_dir %s", CRYPT_SHARED_DIR)
|
LOGGER.info("Using crypt_shared_dir %s", CRYPT_SHARED_DIR)
|
||||||
@ -429,10 +421,24 @@ def handle_test_env() -> None:
|
|||||||
run_command(f"bash {auth_aws_dir}/setup-secrets.sh")
|
run_command(f"bash {auth_aws_dir}/setup-secrets.sh")
|
||||||
|
|
||||||
if test_name == "atlas_connect":
|
if test_name == "atlas_connect":
|
||||||
get_secrets("drivers/atlas_connect")
|
secrets = get_secrets("drivers/atlas_connect")
|
||||||
|
|
||||||
|
# Write file with Atlas X509 client certificate:
|
||||||
|
decoded = base64.b64decode(secrets["ATLAS_X509_DEV_CERT_BASE64"]).decode("utf8")
|
||||||
|
cert_file = ROOT / ".evergreen/atlas_x509_dev_client_certificate.pem"
|
||||||
|
with cert_file.open("w") as file:
|
||||||
|
file.write(decoded)
|
||||||
|
write_env(
|
||||||
|
"ATLAS_X509_DEV_WITH_CERT",
|
||||||
|
secrets["ATLAS_X509_DEV"] + "&tlsCertificateKeyFile=" + str(cert_file),
|
||||||
|
)
|
||||||
|
|
||||||
# We do not want the default client_context to be initialized.
|
# We do not want the default client_context to be initialized.
|
||||||
write_env("DISABLE_CONTEXT")
|
write_env("DISABLE_CONTEXT")
|
||||||
|
|
||||||
|
if test_name == "numpy":
|
||||||
|
UV_ARGS.append("--with numpy")
|
||||||
|
|
||||||
if test_name == "perf":
|
if test_name == "perf":
|
||||||
data_dir = ROOT / "specifications/source/benchmarking/data"
|
data_dir = ROOT / "specifications/source/benchmarking/data"
|
||||||
if not data_dir.exists():
|
if not data_dir.exists():
|
||||||
@ -456,16 +462,18 @@ def handle_test_env() -> None:
|
|||||||
|
|
||||||
# Add coverage if requested.
|
# Add coverage if requested.
|
||||||
# Only cover CPython. PyPy reports suspiciously low coverage.
|
# Only cover CPython. PyPy reports suspiciously low coverage.
|
||||||
if (is_set("COVERAGE") or opts.cov) and platform.python_implementation() == "CPython":
|
if opts.cov and platform.python_implementation() == "CPython":
|
||||||
# Keep in sync with combine-coverage.sh.
|
# Keep in sync with combine-coverage.sh.
|
||||||
# coverage >=5 is needed for relative_files=true.
|
# coverage >=5 is needed for relative_files=true.
|
||||||
UV_ARGS.append("--group coverage")
|
UV_ARGS.append("--group coverage")
|
||||||
TEST_ARGS = f"{TEST_ARGS} --cov"
|
|
||||||
write_env("COVERAGE")
|
write_env("COVERAGE")
|
||||||
|
|
||||||
if is_set("GREEN_FRAMEWORK") or opts.green_framework:
|
if opts.green_framework:
|
||||||
framework = opts.green_framework or os.environ["GREEN_FRAMEWORK"]
|
framework = opts.green_framework or os.environ["GREEN_FRAMEWORK"]
|
||||||
UV_ARGS.append(f"--group {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:
|
else:
|
||||||
TEST_ARGS = f"-v --durations=5 {TEST_ARGS}"
|
TEST_ARGS = f"-v --durations=5 {TEST_ARGS}"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Stop a server that was started using run-orchestration.sh in DRIVERS_TOOLS.
|
# Stop a server that was started using run-mongodb.sh in DRIVERS_TOOLS.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
HERE=$(dirname ${BASH_SOURCE:-$0})
|
HERE=$(dirname ${BASH_SOURCE:-$0})
|
||||||
@ -11,4 +11,4 @@ if [ -f $HERE/env.sh ]; then
|
|||||||
source $HERE/env.sh
|
source $HERE/env.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bash ${DRIVERS_TOOLS}/.evergreen/stop-orchestration.sh
|
bash ${DRIVERS_TOOLS}/.evergreen/run-mongodb.sh stop
|
||||||
|
|||||||
@ -36,10 +36,6 @@ elif TEST_NAME == "auth_oidc":
|
|||||||
elif TEST_NAME == "ocsp":
|
elif TEST_NAME == "ocsp":
|
||||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/ocsp/teardown.sh")
|
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/ocsp/teardown.sh")
|
||||||
|
|
||||||
# Tear down serverless if applicable.
|
|
||||||
elif TEST_NAME == "serverless":
|
|
||||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/serverless/teardown.sh")
|
|
||||||
|
|
||||||
# Tear down atlas cluster if applicable.
|
# Tear down atlas cluster if applicable.
|
||||||
if TEST_NAME in ["aws_lambda", "search_index"]:
|
if TEST_NAME in ["aws_lambda", "search_index"]:
|
||||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh")
|
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh")
|
||||||
@ -61,10 +57,6 @@ elif TEST_NAME == "mod_wsgi":
|
|||||||
|
|
||||||
teardown_mod_wsgi()
|
teardown_mod_wsgi()
|
||||||
|
|
||||||
# Tear down data_lake if applicable.
|
|
||||||
elif TEST_NAME == "data_lake":
|
|
||||||
run_command(f"{DRIVERS_TOOLS}/.evergreen/atlas_data_lake/teardown.sh")
|
|
||||||
|
|
||||||
# Tear down coverage if applicable.
|
# Tear down coverage if applicable.
|
||||||
if os.environ.get("COVERAGE"):
|
if os.environ.get("COVERAGE"):
|
||||||
shutil.rmtree(".pytest_cache", ignore_errors=True)
|
shutil.rmtree(".pytest_cache", ignore_errors=True)
|
||||||
|
|||||||
57
.evergreen/scripts/upload-codecov.sh
Executable file
57
.evergreen/scripts/upload-codecov.sh
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
#!/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
|
||||||
@ -33,7 +33,6 @@ TEST_SUITE_MAP = {
|
|||||||
"atlas_connect": "atlas_connect",
|
"atlas_connect": "atlas_connect",
|
||||||
"auth_aws": "auth_aws",
|
"auth_aws": "auth_aws",
|
||||||
"auth_oidc": "auth_oidc",
|
"auth_oidc": "auth_oidc",
|
||||||
"data_lake": "data_lake",
|
|
||||||
"default": "",
|
"default": "",
|
||||||
"default_async": "default_async",
|
"default_async": "default_async",
|
||||||
"default_sync": "default",
|
"default_sync": "default",
|
||||||
@ -45,7 +44,7 @@ TEST_SUITE_MAP = {
|
|||||||
"mockupdb": "mockupdb",
|
"mockupdb": "mockupdb",
|
||||||
"ocsp": "ocsp",
|
"ocsp": "ocsp",
|
||||||
"perf": "perf",
|
"perf": "perf",
|
||||||
"serverless": "",
|
"numpy": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tests that require a sub test suite.
|
# Tests that require a sub test suite.
|
||||||
@ -53,17 +52,18 @@ SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi", "perf"]
|
|||||||
|
|
||||||
EXTRA_TESTS = ["mod_wsgi", "aws_lambda", "doctest"]
|
EXTRA_TESTS = ["mod_wsgi", "aws_lambda", "doctest"]
|
||||||
|
|
||||||
# Tests that do not use run-orchestration directly.
|
# Tests that do not use run-mongodb directly.
|
||||||
NO_RUN_ORCHESTRATION = [
|
NO_RUN_ORCHESTRATION = [
|
||||||
"auth_oidc",
|
"auth_oidc",
|
||||||
"atlas_connect",
|
"atlas_connect",
|
||||||
"aws_lambda",
|
"aws_lambda",
|
||||||
"data_lake",
|
|
||||||
"mockupdb",
|
"mockupdb",
|
||||||
"serverless",
|
|
||||||
"ocsp",
|
"ocsp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Mapping of env variables to options
|
||||||
|
OPTION_TO_ENV_VAR = {"cov": "COVERAGE", "crypt_shared": "TEST_CRYPT_SHARED"}
|
||||||
|
|
||||||
|
|
||||||
def get_test_options(
|
def get_test_options(
|
||||||
description, require_sub_test_name=True, allow_extra_opts=False
|
description, require_sub_test_name=True, allow_extra_opts=False
|
||||||
@ -98,6 +98,9 @@ def get_test_options(
|
|||||||
)
|
)
|
||||||
parser.add_argument("--auth", action="store_true", help="Whether to add authentication.")
|
parser.add_argument("--auth", action="store_true", help="Whether to add authentication.")
|
||||||
parser.add_argument("--ssl", action="store_true", help="Whether to add TLS configuration.")
|
parser.add_argument("--ssl", action="store_true", help="Whether to add TLS configuration.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--test-min-deps", action="store_true", help="Test against minimum dependency versions"
|
||||||
|
)
|
||||||
|
|
||||||
# Add the test modifiers.
|
# Add the test modifiers.
|
||||||
if require_sub_test_name:
|
if require_sub_test_name:
|
||||||
@ -108,7 +111,7 @@ def get_test_options(
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--green-framework",
|
"--green-framework",
|
||||||
nargs=1,
|
nargs=1,
|
||||||
choices=["eventlet", "gevent"],
|
choices=["gevent"],
|
||||||
help="Optional green framework to test against.",
|
help="Optional green framework to test against.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -131,26 +134,53 @@ def get_test_options(
|
|||||||
opts, extra_opts = parser.parse_args(), []
|
opts, extra_opts = parser.parse_args(), []
|
||||||
else:
|
else:
|
||||||
opts, extra_opts = parser.parse_known_args()
|
opts, extra_opts = parser.parse_known_args()
|
||||||
if opts.verbose:
|
|
||||||
LOGGER.setLevel(logging.DEBUG)
|
# Convert list inputs to strings.
|
||||||
elif opts.quiet:
|
for name in vars(opts):
|
||||||
LOGGER.setLevel(logging.WARNING)
|
value = getattr(opts, name)
|
||||||
|
if isinstance(value, list):
|
||||||
|
setattr(opts, name, value[0])
|
||||||
|
|
||||||
# Handle validation and environment variable overrides.
|
# Handle validation and environment variable overrides.
|
||||||
test_name = opts.test_name
|
test_name = opts.test_name
|
||||||
sub_test_name = opts.sub_test_name if require_sub_test_name else ""
|
sub_test_name = opts.sub_test_name if require_sub_test_name else ""
|
||||||
if require_sub_test_name and test_name in SUB_TEST_REQUIRED and not sub_test_name:
|
if require_sub_test_name and test_name in SUB_TEST_REQUIRED and not sub_test_name:
|
||||||
raise ValueError(f"Test '{test_name}' requires a sub_test_name")
|
raise ValueError(f"Test '{test_name}' requires a sub_test_name")
|
||||||
if "auth" in test_name or os.environ.get("AUTH") == "auth":
|
handle_env_overrides(parser, opts)
|
||||||
|
if "auth" in test_name:
|
||||||
opts.auth = True
|
opts.auth = True
|
||||||
# 'auth_aws ecs' shouldn't have extra auth set.
|
# 'auth_aws ecs' shouldn't have extra auth set.
|
||||||
if test_name == "auth_aws" and sub_test_name == "ecs":
|
if test_name == "auth_aws" and sub_test_name == "ecs":
|
||||||
opts.auth = False
|
opts.auth = False
|
||||||
if os.environ.get("SSL") == "ssl":
|
if opts.verbose:
|
||||||
opts.ssl = True
|
LOGGER.setLevel(logging.DEBUG)
|
||||||
|
elif opts.quiet:
|
||||||
|
LOGGER.setLevel(logging.WARNING)
|
||||||
return opts, extra_opts
|
return opts, extra_opts
|
||||||
|
|
||||||
|
|
||||||
|
def handle_env_overrides(parser: argparse.ArgumentParser, opts: argparse.Namespace) -> None:
|
||||||
|
# Get the options, and then allow environment variable overrides.
|
||||||
|
for key in vars(opts):
|
||||||
|
if key in OPTION_TO_ENV_VAR:
|
||||||
|
env_var = OPTION_TO_ENV_VAR[key]
|
||||||
|
else:
|
||||||
|
env_var = key.upper()
|
||||||
|
if env_var in os.environ:
|
||||||
|
if parser.get_default(key) != getattr(opts, key):
|
||||||
|
LOGGER.info("Overriding env var '%s' with cli option", env_var)
|
||||||
|
elif env_var == "AUTH":
|
||||||
|
opts.auth = os.environ.get("AUTH") == "auth"
|
||||||
|
elif env_var == "SSL":
|
||||||
|
ssl_opt = os.environ.get("SSL", "")
|
||||||
|
opts.ssl = ssl_opt and ssl_opt.lower() != "nossl"
|
||||||
|
elif isinstance(getattr(opts, key), bool):
|
||||||
|
if os.environ[env_var]:
|
||||||
|
setattr(opts, key, True)
|
||||||
|
else:
|
||||||
|
setattr(opts, key, os.environ[env_var])
|
||||||
|
|
||||||
|
|
||||||
def read_env(path: Path | str) -> dict[str, str]:
|
def read_env(path: Path | str) -> dict[str, str]:
|
||||||
config = dict()
|
config = dict()
|
||||||
with Path(path).open() as fid:
|
with Path(path).open() as fid:
|
||||||
|
|||||||
@ -15,5 +15,4 @@ echo "Copying files to $target..."
|
|||||||
rsync -az -e ssh --exclude '.git' --filter=':- .gitignore' -r . $target:$remote_dir
|
rsync -az -e ssh --exclude '.git' --filter=':- .gitignore' -r . $target:$remote_dir
|
||||||
echo "Copying files to $target... done"
|
echo "Copying files to $target... done"
|
||||||
|
|
||||||
ssh $target $remote_dir/.evergreen/scripts/setup-system.sh
|
ssh $target "$remote_dir/.evergreen/scripts/setup-system.sh"
|
||||||
ssh $target "cd $remote_dir && PYTHON_BINARY=${PYTHON_BINARY:-} .evergreen/scripts/setup-dev-env.sh"
|
|
||||||
|
|||||||
24
.evergreen/spec-patch/PYTHON-4918.patch
Normal file
24
.evergreen/spec-patch/PYTHON-4918.patch
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
diff --git a/test/connection_monitoring/pool-create-min-size-error.json b/test/connection_monitoring/pool-create-min-size-error.json
|
||||||
|
index 1c744b85..509b2a23 100644
|
||||||
|
--- a/test/connection_monitoring/pool-create-min-size-error.json
|
||||||
|
+++ b/test/connection_monitoring/pool-create-min-size-error.json
|
||||||
|
@@ -49,15 +49,15 @@
|
||||||
|
"type": "ConnectionCreated",
|
||||||
|
"address": 42
|
||||||
|
},
|
||||||
|
+ {
|
||||||
|
+ "type": "ConnectionPoolCleared",
|
||||||
|
+ "address": 42
|
||||||
|
+ },
|
||||||
|
{
|
||||||
|
"type": "ConnectionClosed",
|
||||||
|
"address": 42,
|
||||||
|
"connectionId": 42,
|
||||||
|
"reason": "error"
|
||||||
|
- },
|
||||||
|
- {
|
||||||
|
- "type": "ConnectionPoolCleared",
|
||||||
|
- "address": 42
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
440
.evergreen/spec-patch/PYTHON-5052.patch
Normal file
440
.evergreen/spec-patch/PYTHON-5052.patch
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
diff --git a/test/unified-test-format/invalid/entity-client-observeTracingMessages-additionalProperties.json b/test/unified-test-format/invalid/entity-client-observeTracingMessages-additionalProperties.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..aa8046d2
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/entity-client-observeTracingMessages-additionalProperties.json
|
||||||
|
@@ -0,0 +1,20 @@
|
||||||
|
+{
|
||||||
|
+ "description": "entity-client-observeTracingMessages-additionalProperties",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0",
|
||||||
|
+ "observeTracingMessages": {
|
||||||
|
+ "foo": "bar"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "observeTracingMessages must not have additional properties'",
|
||||||
|
+ "operations": []
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/entity-client-observeTracingMessages-additionalPropertyType.json b/test/unified-test-format/invalid/entity-client-observeTracingMessages-additionalPropertyType.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..0b3a65f5
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/entity-client-observeTracingMessages-additionalPropertyType.json
|
||||||
|
@@ -0,0 +1,20 @@
|
||||||
|
+{
|
||||||
|
+ "description": "entity-client-observeTracingMessages-additionalPropertyType",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0",
|
||||||
|
+ "observeTracingMessages": {
|
||||||
|
+ "enableCommandPayload": 0
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "observeTracingMessages enableCommandPayload must be boolean",
|
||||||
|
+ "operations": []
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/entity-client-observeTracingMessages-type.json b/test/unified-test-format/invalid/entity-client-observeTracingMessages-type.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..de3ef39a
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/entity-client-observeTracingMessages-type.json
|
||||||
|
@@ -0,0 +1,18 @@
|
||||||
|
+{
|
||||||
|
+ "description": "entity-client-observeTracingMessages-type",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0",
|
||||||
|
+ "observeTracingMessages": "foo"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "observeTracingMessages must be an object",
|
||||||
|
+ "operations": []
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-additionalProperties.json b/test/unified-test-format/invalid/expectedTracingSpans-additionalProperties.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..5947a286
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-additionalProperties.json
|
||||||
|
@@ -0,0 +1,30 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-additionalProperties",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "additional property foo not allowed in expectTracingMessages",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "ignoreExtraSpans": false,
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "command",
|
||||||
|
+ "tags": {
|
||||||
|
+ "db.system": "mongodb"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "foo": 0
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-clientType.json b/test/unified-test-format/invalid/expectedTracingSpans-clientType.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..2fe7faea
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-clientType.json
|
||||||
|
@@ -0,0 +1,28 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-clientType",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "client type must be string",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": 0,
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "command",
|
||||||
|
+ "tags": {
|
||||||
|
+ "db.system": "mongodb"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-emptyNestedSpan.json b/test/unified-test-format/invalid/expectedTracingSpans-emptyNestedSpan.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..8a98d5ba
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-emptyNestedSpan.json
|
||||||
|
@@ -0,0 +1,29 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-emptyNestedSpan",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "nested spans must not have fewer than 1 items'",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "command",
|
||||||
|
+ "tags": {
|
||||||
|
+ "db.system": "mongodb"
|
||||||
|
+ },
|
||||||
|
+ "nested": []
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-invalidNestedSpan.json b/test/unified-test-format/invalid/expectedTracingSpans-invalidNestedSpan.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..79a86744
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-invalidNestedSpan.json
|
||||||
|
@@ -0,0 +1,31 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-invalidNestedSpan",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "nested span must have required property name",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "command",
|
||||||
|
+ "tags": {
|
||||||
|
+ "db.system": "mongodb"
|
||||||
|
+ },
|
||||||
|
+ "nested": [
|
||||||
|
+ {}
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-missingPropertyClient.json b/test/unified-test-format/invalid/expectedTracingSpans-missingPropertyClient.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..2fb1cd5b
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-missingPropertyClient.json
|
||||||
|
@@ -0,0 +1,27 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-missingPropertyClient",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "missing required property client",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "command",
|
||||||
|
+ "tags": {
|
||||||
|
+ "db.system": "mongodb"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-missingPropertySpans.json b/test/unified-test-format/invalid/expectedTracingSpans-missingPropertySpans.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..acd10307
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-missingPropertySpans.json
|
||||||
|
@@ -0,0 +1,20 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-missingPropertySpans",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "missing required property spans",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedAdditionalProperties.json b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedAdditionalProperties.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..17299f86
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedAdditionalProperties.json
|
||||||
|
@@ -0,0 +1,28 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-spanMalformedAdditionalProperties",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "Span must not have additional properties",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "foo",
|
||||||
|
+ "tags": {},
|
||||||
|
+ "nested": [],
|
||||||
|
+ "foo": "bar"
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedMissingName.json b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedMissingName.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..0257cd9b
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedMissingName.json
|
||||||
|
@@ -0,0 +1,27 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-spanMalformedMissingName",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "missing required span name",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "tags": {
|
||||||
|
+ "db.system": "mongodb"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedMissingTags.json b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedMissingTags.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..a09ca31c
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedMissingTags.json
|
||||||
|
@@ -0,0 +1,25 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-spanMalformedMissingTags",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "missing required span tags",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "foo"
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedNestedMustBeArray.json b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedNestedMustBeArray.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..ccff0410
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedNestedMustBeArray.json
|
||||||
|
@@ -0,0 +1,27 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-spanMalformedNestedMustBeArray",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "nested spans must be an array",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "foo",
|
||||||
|
+ "tags": {},
|
||||||
|
+ "nested": {}
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
|
diff --git a/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedTagsMustBeObject.json b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedTagsMustBeObject.json
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000..72af1c29
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/unified-test-format/invalid/expectedTracingSpans-spanMalformedTagsMustBeObject.json
|
||||||
|
@@ -0,0 +1,26 @@
|
||||||
|
+{
|
||||||
|
+ "description": "expectedTracingSpans-spanMalformedNestedMustBeObject",
|
||||||
|
+ "schemaVersion": "1.26",
|
||||||
|
+ "createEntities": [
|
||||||
|
+ {
|
||||||
|
+ "client": {
|
||||||
|
+ "id": "client0"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
+ "tests": [
|
||||||
|
+ {
|
||||||
|
+ "description": "span tags must be an object",
|
||||||
|
+ "operations": [],
|
||||||
|
+ "expectTracingMessages": {
|
||||||
|
+ "client": "client0",
|
||||||
|
+ "spans": [
|
||||||
|
+ {
|
||||||
|
+ "name": "foo",
|
||||||
|
+ "tags": []
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
26
.evergreen/spec-patch/PYTHON-5445.patch
Normal file
26
.evergreen/spec-patch/PYTHON-5445.patch
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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",
|
||||||
50
.evergreen/spec-patch/PYTHON-5493.patch
Normal file
50
.evergreen/spec-patch/PYTHON-5493.patch
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
diff --git a/test/connection_logging/connection-logging.json b/test/connection_logging/connection-logging.json
|
||||||
|
index 5799e834..72103b3c 100644
|
||||||
|
--- a/test/connection_logging/connection-logging.json
|
||||||
|
+++ b/test/connection_logging/connection-logging.json
|
||||||
|
@@ -446,6 +446,22 @@
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
+ {
|
||||||
|
+ "level": "debug",
|
||||||
|
+ "component": "connection",
|
||||||
|
+ "data": {
|
||||||
|
+ "message": "Connection pool cleared",
|
||||||
|
+ "serverHost": {
|
||||||
|
+ "$$type": "string"
|
||||||
|
+ },
|
||||||
|
+ "serverPort": {
|
||||||
|
+ "$$type": [
|
||||||
|
+ "int",
|
||||||
|
+ "long"
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ },
|
||||||
|
{
|
||||||
|
"level": "debug",
|
||||||
|
"component": "connection",
|
||||||
|
@@ -498,22 +514,6 @@
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- },
|
||||||
|
- {
|
||||||
|
- "level": "debug",
|
||||||
|
- "component": "connection",
|
||||||
|
- "data": {
|
||||||
|
- "message": "Connection pool cleared",
|
||||||
|
- "serverHost": {
|
||||||
|
- "$$type": "string"
|
||||||
|
- },
|
||||||
|
- "serverPort": {
|
||||||
|
- "$$type": [
|
||||||
|
- "int",
|
||||||
|
- "long"
|
||||||
|
- ]
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
815
.evergreen/spec-patch/PYTHON-5559.patch
Normal file
815
.evergreen/spec-patch/PYTHON-5559.patch
Normal file
@ -0,0 +1,815 @@
|
|||||||
|
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"
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
460
.evergreen/spec-patch/PYTHON-5759.patch
Normal file
460
.evergreen/spec-patch/PYTHON-5759.patch
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
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": []
|
||||||
|
+ }
|
||||||
|
+ ]
|
||||||
|
+}
|
||||||
@ -1,140 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Utility functions used by pymongo evergreen scripts.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
find_python3() {
|
|
||||||
PYTHON=""
|
|
||||||
# Find a suitable toolchain version, if available.
|
|
||||||
if [ "$(uname -s)" = "Darwin" ]; then
|
|
||||||
PYTHON="/Library/Frameworks/Python.Framework/Versions/3.9/bin/python3"
|
|
||||||
elif [ "Windows_NT" = "${OS:-}" ]; then # Magic variable in cygwin
|
|
||||||
PYTHON="C:/python/Python39/python.exe"
|
|
||||||
else
|
|
||||||
# Prefer our own toolchain, fall back to mongodb toolchain if it has Python 3.9+.
|
|
||||||
if [ -f "/opt/python/3.9/bin/python3" ]; then
|
|
||||||
PYTHON="/opt/python/Current/bin/python3"
|
|
||||||
elif is_python_39 "$(command -v /opt/mongodbtoolchain/v5/bin/python3)"; then
|
|
||||||
PYTHON="/opt/mongodbtoolchain/v5/bin/python3"
|
|
||||||
elif is_python_39 "$(command -v /opt/mongodbtoolchain/v4/bin/python3)"; then
|
|
||||||
PYTHON="/opt/mongodbtoolchain/v4/bin/python3"
|
|
||||||
elif is_python_39 "$(command -v /opt/mongodbtoolchain/v3/bin/python3)"; then
|
|
||||||
PYTHON="/opt/mongodbtoolchain/v3/bin/python3"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# Add a fallback system python3 if it is available and Python 3.9+.
|
|
||||||
if [ -z "$PYTHON" ]; then
|
|
||||||
if is_python_39 "$(command -v python3)"; then
|
|
||||||
PYTHON="$(command -v python3)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ -z "$PYTHON" ]; then
|
|
||||||
echo "Cannot test without python3.9+ installed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "$PYTHON"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage:
|
|
||||||
# createvirtualenv /path/to/python /output/path/for/venv
|
|
||||||
# * param1: Python binary to use for the virtualenv
|
|
||||||
# * param2: Path to the virtualenv to create
|
|
||||||
createvirtualenv () {
|
|
||||||
PYTHON=$1
|
|
||||||
VENVPATH=$2
|
|
||||||
|
|
||||||
# Prefer venv
|
|
||||||
VENV="$PYTHON -m venv"
|
|
||||||
if [ "$(uname -s)" = "Darwin" ]; then
|
|
||||||
VIRTUALENV="$PYTHON -m virtualenv"
|
|
||||||
else
|
|
||||||
VIRTUALENV=$(command -v virtualenv 2>/dev/null || echo "$PYTHON -m virtualenv")
|
|
||||||
VIRTUALENV="$VIRTUALENV -p $PYTHON"
|
|
||||||
fi
|
|
||||||
if ! $VENV $VENVPATH 2>/dev/null; then
|
|
||||||
# Workaround for bug in older versions of virtualenv.
|
|
||||||
$VIRTUALENV $VENVPATH 2>/dev/null || $VIRTUALENV $VENVPATH
|
|
||||||
fi
|
|
||||||
if [ "Windows_NT" = "${OS:-}" ]; then
|
|
||||||
# Workaround https://bugs.python.org/issue32451:
|
|
||||||
# mongovenv/Scripts/activate: line 3: $'\r': command not found
|
|
||||||
dos2unix $VENVPATH/Scripts/activate || true
|
|
||||||
. $VENVPATH/Scripts/activate
|
|
||||||
else
|
|
||||||
. $VENVPATH/bin/activate
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PIP_QUIET=1
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage:
|
|
||||||
# testinstall /path/to/python /path/to/.whl ["no-virtualenv"]
|
|
||||||
# * param1: Python binary to test
|
|
||||||
# * param2: Path to the wheel to install
|
|
||||||
# * param3 (optional): If set to a non-empty string, don't create a virtualenv. Used in manylinux containers.
|
|
||||||
testinstall () {
|
|
||||||
PYTHON=$1
|
|
||||||
RELEASE=$2
|
|
||||||
NO_VIRTUALENV=$3
|
|
||||||
PYTHON_IMPL=$(python -c "import platform; print(platform.python_implementation())")
|
|
||||||
|
|
||||||
if [ -z "$NO_VIRTUALENV" ]; then
|
|
||||||
createvirtualenv $PYTHON venvtestinstall
|
|
||||||
PYTHON=python
|
|
||||||
fi
|
|
||||||
|
|
||||||
$PYTHON -m pip install --upgrade $RELEASE
|
|
||||||
cd tools
|
|
||||||
|
|
||||||
if [ "$PYTHON_IMPL" = "CPython" ]; then
|
|
||||||
$PYTHON fail_if_no_c.py
|
|
||||||
fi
|
|
||||||
|
|
||||||
$PYTHON -m pip uninstall -y pymongo
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
if [ -z "$NO_VIRTUALENV" ]; then
|
|
||||||
deactivate
|
|
||||||
rm -rf venvtestinstall
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function that returns success if the provided Python binary is version 3.9 or later
|
|
||||||
# Usage:
|
|
||||||
# is_python_39 /path/to/python
|
|
||||||
# * param1: Python binary
|
|
||||||
is_python_39() {
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
return 1
|
|
||||||
elif $1 -c "import sys; exit(sys.version_info[:2] < (3, 9))"; then
|
|
||||||
# runs when sys.version_info[:2] >= (3, 9)
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Function that gets a python binary given a python version string.
|
|
||||||
# Versions can be of the form 3.xx or pypy3.xx.
|
|
||||||
get_python_binary() {
|
|
||||||
version=$1
|
|
||||||
if [ "$(uname -s)" = "Darwin" ]; then
|
|
||||||
PYTHON="/Library/Frameworks/Python.Framework/Versions/$version/bin/python3"
|
|
||||||
elif [ "Windows_NT" = "${OS:-}" ]; then
|
|
||||||
version=$(echo $version | cut -d. -f1,2 | sed 's/\.//g')
|
|
||||||
if [ -n "${IS_WIN32:-}" ]; then
|
|
||||||
PYTHON="C:/python/32/Python$version/python.exe"
|
|
||||||
else
|
|
||||||
PYTHON="C:/python/Python$version/python.exe"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
PYTHON="/opt/python/$version/bin/python3"
|
|
||||||
fi
|
|
||||||
if is_python_39 "$(command -v $PYTHON)"; then
|
|
||||||
echo "$PYTHON"
|
|
||||||
else
|
|
||||||
echo "Could not find suitable python binary for '$version'" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* @mongodb/dbx-python
|
||||||
44
.github/copilot-instructions.md
vendored
Normal file
44
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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,6 +5,8 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
groups:
|
groups:
|
||||||
actions:
|
actions:
|
||||||
patterns:
|
patterns:
|
||||||
|
|||||||
33
.github/pull_request_template.md
vendored
Normal file
33
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!-- Thanks for contributing! -->
|
||||||
|
<!-- Please ensure that the title of the PR is in the following form:
|
||||||
|
[JIRA TICKET]: 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]
|
||||||
|
|
||||||
|
## 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
|
||||||
|
<!-- 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. -->
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
### Checklist for 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?)
|
||||||
|
- [ ] Is all relevant documentation (README or docstring) updated?
|
||||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@ -38,15 +38,15 @@ jobs:
|
|||||||
build-mode: none
|
build-mode: none
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
|
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@ -63,6 +63,6 @@ jobs:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
|
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
8
.github/workflows/create-release-branch.yml
vendored
8
.github/workflows/create-release-branch.yml
vendored
@ -33,17 +33,19 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.pre-publish.outputs.version }}
|
version: ${{ steps.pre-publish.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v2
|
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
|
||||||
with:
|
with:
|
||||||
app_id: ${{ vars.APP_ID }}
|
app_id: ${{ vars.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
- uses: mongodb-labs/drivers-github-tools/setup@v2
|
- uses: mongodb-labs/drivers-github-tools/setup@v3
|
||||||
with:
|
with:
|
||||||
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
|
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
|
||||||
aws_region_name: ${{ vars.AWS_REGION_NAME }}
|
aws_region_name: ${{ vars.AWS_REGION_NAME }}
|
||||||
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
|
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
|
||||||
artifactory_username: ${{ vars.ARTIFACTORY_USERNAME }}
|
artifactory_username: ${{ vars.ARTIFACTORY_USERNAME }}
|
||||||
- uses: mongodb-labs/drivers-github-tools/create-branch@v2
|
- name: Get hatch
|
||||||
|
run: pip install hatch
|
||||||
|
- uses: mongodb-labs/drivers-github-tools/create-branch@v3
|
||||||
id: create-branch
|
id: create-branch
|
||||||
with:
|
with:
|
||||||
branch_name: ${{ inputs.branch_name }}
|
branch_name: ${{ inputs.branch_name }}
|
||||||
|
|||||||
44
.github/workflows/dist.yml
vendored
44
.github/workflows/dist.yml
vendored
@ -39,28 +39,29 @@ jobs:
|
|||||||
- [ubuntu-latest, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"]
|
- [ubuntu-latest, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"]
|
||||||
- [ubuntu-latest, "manylinux_s390x", "cp3*-manylinux_s390x"]
|
- [ubuntu-latest, "manylinux_s390x", "cp3*-manylinux_s390x"]
|
||||||
- [ubuntu-latest, "manylinux_i686", "cp3*-manylinux_i686"]
|
- [ubuntu-latest, "manylinux_i686", "cp3*-manylinux_i686"]
|
||||||
- [windows-2019, "win_amd6", "cp3*-win_amd64"]
|
- [windows-2022, "win_amd6", "cp3*-win_amd64"]
|
||||||
- [windows-2019, "win32", "cp3*-win32"]
|
- [windows-2022, "win32", "cp3*-win32"]
|
||||||
|
- [windows-11-arm, "win_arm64", "cp3*-win_arm64"]
|
||||||
- [macos-14, "macos", "cp*-macosx_*"]
|
- [macos-14, "macos", "cp*-macosx_*"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout pymongo
|
- name: Checkout pymongo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
python-version: 3.9
|
python-version: 3.11
|
||||||
cache-dependency-path: 'pyproject.toml'
|
cache-dependency-path: 'pyproject.toml'
|
||||||
allow-prereleases: true
|
allow-prereleases: true
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
with:
|
with:
|
||||||
# setup-qemu-action by default uses `tonistiigi/binfmt:latest` image,
|
# setup-qemu-action by default uses `tonistiigi/binfmt:latest` image,
|
||||||
# which is out of date. This causes seg faults during build.
|
# which is out of date. This causes seg faults during build.
|
||||||
@ -69,24 +70,16 @@ jobs:
|
|||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Install cibuildwheel
|
- name: Install cibuildwheel
|
||||||
# Note: the default manylinux is manylinux2014
|
# Note: the default manylinux is manylinux_2_28
|
||||||
run: |
|
run: |
|
||||||
python -m pip install -U pip
|
python -m pip install -U pip
|
||||||
python -m pip install "cibuildwheel>=2.20,<3"
|
python -m pip install "cibuildwheel>=3.2.0,<4"
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
env:
|
env:
|
||||||
CIBW_BUILD: ${{ matrix.buildplat[2] }}
|
CIBW_BUILD: ${{ matrix.buildplat[2] }}
|
||||||
run: python -m cibuildwheel --output-dir wheelhouse
|
run: python -m cibuildwheel --output-dir wheelhouse
|
||||||
|
|
||||||
- name: Build manylinux1 wheels
|
|
||||||
if: ${{ matrix.buildplat[1] == 'manylinux_x86_64' || matrix.buildplat[1] == 'manylinux_i686' }}
|
|
||||||
env:
|
|
||||||
CIBW_MANYLINUX_X86_64_IMAGE: manylinux1
|
|
||||||
CIBW_MANYLINUX_I686_IMAGE: manylinux1
|
|
||||||
CIBW_BUILD: "cp39-${{ matrix.buildplat[1] }} cp39-${{ matrix.buildplat[1] }}"
|
|
||||||
run: python -m cibuildwheel --output-dir wheelhouse
|
|
||||||
|
|
||||||
- name: Assert all versions in wheelhouse
|
- name: Assert all versions in wheelhouse
|
||||||
if: ${{ ! startsWith(matrix.buildplat[1], 'macos') }}
|
if: ${{ ! startsWith(matrix.buildplat[1], 'macos') }}
|
||||||
run: |
|
run: |
|
||||||
@ -95,10 +88,11 @@ jobs:
|
|||||||
ls wheelhouse/*cp311*.whl
|
ls wheelhouse/*cp311*.whl
|
||||||
ls wheelhouse/*cp312*.whl
|
ls wheelhouse/*cp312*.whl
|
||||||
ls wheelhouse/*cp313*.whl
|
ls wheelhouse/*cp313*.whl
|
||||||
|
ls wheelhouse/*cp314*.whl
|
||||||
# Free-threading builds:
|
# Free-threading builds:
|
||||||
ls wheelhouse/*cp313t*.whl
|
ls wheelhouse/*cp314t*.whl
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: wheel-${{ matrix.buildplat[1] }}
|
name: wheel-${{ matrix.buildplat[1] }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
@ -106,18 +100,18 @@ jobs:
|
|||||||
|
|
||||||
make_sdist:
|
make_sdist:
|
||||||
name: Make SDist
|
name: Make SDist
|
||||||
runs-on: macos-13
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ inputs.ref }}
|
ref: ${{ inputs.ref }}
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
# Build sdist on lowest supported Python
|
# Build sdist on lowest supported Python
|
||||||
python-version: '3.9'
|
python-version: "3.9"
|
||||||
|
|
||||||
- name: Build SDist
|
- name: Build SDist
|
||||||
run: |
|
run: |
|
||||||
@ -131,7 +125,7 @@ jobs:
|
|||||||
cd ..
|
cd ..
|
||||||
python -c "from pymongo import has_c; assert has_c()"
|
python -c "from pymongo import has_c; assert has_c()"
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: "sdist"
|
name: "sdist"
|
||||||
path: ./dist/*.tar.gz
|
path: ./dist/*.tar.gz
|
||||||
@ -142,13 +136,13 @@ jobs:
|
|||||||
name: Download Wheels
|
name: Download Wheels
|
||||||
steps:
|
steps:
|
||||||
- name: Download all workflow run artifacts
|
- name: Download all workflow run artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
- name: Flatten directory
|
- name: Flatten directory
|
||||||
working-directory: .
|
working-directory: .
|
||||||
run: |
|
run: |
|
||||||
find . -mindepth 2 -type f -exec mv {} . \;
|
find . -mindepth 2 -type f -exec mv {} . \;
|
||||||
find . -type d -empty -delete
|
find . -type d -empty -delete
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: all-dist-${{ github.run_id }}
|
name: all-dist-${{ github.run_id }}
|
||||||
path: "./*"
|
path: "./*"
|
||||||
|
|||||||
23
.github/workflows/pull_request_template.md
vendored
23
.github/workflows/pull_request_template.md
vendored
@ -1,23 +0,0 @@
|
|||||||
# [JIRA Ticket ID](Link to Ticket)
|
|
||||||
<!-- Please provide explicit URL link to the corresponding JIRA ticket. -->
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
<!-- Please provide a high level overview of what changes have been made. -->
|
|
||||||
|
|
||||||
# Changes in this PR
|
|
||||||
<!-- Highlight any high level architecture changes if the summary doesn't already cover the scope. -->
|
|
||||||
|
|
||||||
# Test Plan
|
|
||||||
<!-- Talk through any unit tests added, and if this is a bug fix, please add repro steps in the event the fix needs to be verified. -->
|
|
||||||
|
|
||||||
# Screenshots (Optional)
|
|
||||||
<!-- Add a before and after picture to indicate changes. -->
|
|
||||||
|
|
||||||
# Callouts or Follow-up items (Optional)
|
|
||||||
<!-- Any additional info not already specified in the PR including but not limited to:
|
|
||||||
|
|
||||||
1. Potential stakeholders
|
|
||||||
2. Slack threads etc.
|
|
||||||
3. Implementation details that need additional oversight
|
|
||||||
4. Callouts on future tactics
|
|
||||||
-->
|
|
||||||
20
.github/workflows/release-python.yml
vendored
20
.github/workflows/release-python.yml
vendored
@ -38,17 +38,16 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.pre-publish.outputs.version }}
|
version: ${{ steps.pre-publish.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v2
|
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
|
||||||
with:
|
with:
|
||||||
app_id: ${{ vars.APP_ID }}
|
app_id: ${{ vars.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
- uses: mongodb-labs/drivers-github-tools/setup@v2
|
- uses: mongodb-labs/drivers-github-tools/setup@v3
|
||||||
with:
|
with:
|
||||||
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
|
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
|
||||||
aws_region_name: ${{ vars.AWS_REGION_NAME }}
|
aws_region_name: ${{ vars.AWS_REGION_NAME }}
|
||||||
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
|
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
|
||||||
artifactory_username: ${{ vars.ARTIFACTORY_USERNAME }}
|
- uses: mongodb-labs/drivers-github-tools/python/pre-publish@v3
|
||||||
- uses: mongodb-labs/drivers-github-tools/python/pre-publish@v2
|
|
||||||
id: pre-publish
|
id: pre-publish
|
||||||
with:
|
with:
|
||||||
dry_run: ${{ env.DRY_RUN }}
|
dry_run: ${{ env.DRY_RUN }}
|
||||||
@ -76,19 +75,19 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: all-dist-${{ github.run_id }}
|
name: all-dist-${{ github.run_id }}
|
||||||
path: dist/
|
path: dist/
|
||||||
- name: Publish package distributions to TestPyPI
|
- name: Publish package distributions to TestPyPI
|
||||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1
|
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
|
||||||
with:
|
with:
|
||||||
repository-url: https://test.pypi.org/legacy/
|
repository-url: https://test.pypi.org/legacy/
|
||||||
skip-existing: true
|
skip-existing: true
|
||||||
attestations: ${{ env.DRY_RUN }}
|
attestations: ${{ env.DRY_RUN }}
|
||||||
- name: Publish package distributions to PyPI
|
- name: Publish package distributions to PyPI
|
||||||
if: startsWith(env.DRY_RUN, 'false')
|
if: startsWith(env.DRY_RUN, 'false')
|
||||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1
|
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
|
||||||
|
|
||||||
post-publish:
|
post-publish:
|
||||||
needs: [publish]
|
needs: [publish]
|
||||||
@ -100,17 +99,16 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v2
|
- uses: mongodb-labs/drivers-github-tools/secure-checkout@v3
|
||||||
with:
|
with:
|
||||||
app_id: ${{ vars.APP_ID }}
|
app_id: ${{ vars.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
- uses: mongodb-labs/drivers-github-tools/setup@v2
|
- uses: mongodb-labs/drivers-github-tools/setup@v3
|
||||||
with:
|
with:
|
||||||
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
|
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
|
||||||
aws_region_name: ${{ vars.AWS_REGION_NAME }}
|
aws_region_name: ${{ vars.AWS_REGION_NAME }}
|
||||||
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
|
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}
|
||||||
artifactory_username: ${{ vars.ARTIFACTORY_USERNAME }}
|
- uses: mongodb-labs/drivers-github-tools/python/post-publish@v3
|
||||||
- uses: mongodb-labs/drivers-github-tools/python/post-publish@v2
|
|
||||||
with:
|
with:
|
||||||
following_version: ${{ env.FOLLOWING_VERSION }}
|
following_version: ${{ env.FOLLOWING_VERSION }}
|
||||||
product_name: ${{ env.PRODUCT_NAME }}
|
product_name: ${{ env.PRODUCT_NAME }}
|
||||||
|
|||||||
104
.github/workflows/sbom.yml
vendored
Normal file
104
.github/workflows/sbom.yml
vendored
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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
|
||||||
181
.github/workflows/test-python.yml
vendored
181
.github/workflows/test-python.yml
vendored
@ -14,21 +14,24 @@ defaults:
|
|||||||
run:
|
run:
|
||||||
shell: bash -eux {0}
|
shell: bash -eux {0}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
static:
|
static:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install just
|
|
||||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v5
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.9"
|
python-version: "3.10"
|
||||||
|
- name: Install just
|
||||||
|
run: uv tool install rust-just
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: |
|
run: |
|
||||||
just install
|
just install
|
||||||
@ -50,52 +53,81 @@ jobs:
|
|||||||
cppcheck pymongo
|
cppcheck pymongo
|
||||||
|
|
||||||
build:
|
build:
|
||||||
# supercharge/mongodb-github-action requires containers so we don't test other platforms
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
# Tests currently only pass on ubuntu on GitHub Actions.
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
python-version: ["3.9", "pypy-3.10", "3.13", "3.13t"]
|
python-version: ["3.10", "pypy-3.11", "3.13t"]
|
||||||
|
mongodb-version: ["8.0"]
|
||||||
|
|
||||||
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
|
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install just
|
|
||||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v5
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- id: setup-mongodb
|
||||||
run: just install
|
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||||
- name: Start MongoDB
|
|
||||||
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0
|
|
||||||
with:
|
with:
|
||||||
mongodb-version: 6.0
|
version: "${{ matrix.mongodb-version }}"
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: just test
|
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:
|
doctest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: DocTest
|
name: DocTest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install just
|
|
||||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v5
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.9"
|
python-version: "3.10"
|
||||||
- name: Start MongoDB
|
- name: Install just
|
||||||
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0
|
run: uv tool install rust-just
|
||||||
|
- id: setup-mongodb
|
||||||
|
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||||
with:
|
with:
|
||||||
mongodb-version: '8.0.0-rc4'
|
version: "8.0"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: just install
|
run: just install
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
@ -107,16 +139,16 @@ jobs:
|
|||||||
name: Docs Checks
|
name: Docs Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v5
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.9"
|
python-version: "3.10"
|
||||||
- name: Install just
|
- name: Install just
|
||||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
|
run: uv tool install rust-just
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: just install
|
run: just install
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
@ -126,16 +158,16 @@ jobs:
|
|||||||
name: Link Check
|
name: Link Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v5
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.9"
|
python-version: "3.10"
|
||||||
- name: Install just
|
- name: Install just
|
||||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
|
run: uv tool install rust-just
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: just install
|
run: just install
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
@ -146,18 +178,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python: ["3.9", "3.11"]
|
python: ["3.10", "3.11"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v5
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "${{matrix.python}}"
|
python-version: "${{matrix.python}}"
|
||||||
- name: Install just
|
- name: Install just
|
||||||
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
|
run: uv tool install rust-just
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
just install
|
just install
|
||||||
@ -165,25 +197,55 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
just typing
|
just typing
|
||||||
|
|
||||||
|
integration_tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Integration Tests
|
||||||
|
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"
|
||||||
|
- name: Install just
|
||||||
|
run: uv tool install rust-just
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
just install
|
||||||
|
- id: setup-mongodb
|
||||||
|
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
just integration-tests
|
||||||
|
- id: setup-mongodb-ssl
|
||||||
|
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||||
|
with:
|
||||||
|
ssl: true
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
just integration-tests
|
||||||
|
|
||||||
make_sdist:
|
make_sdist:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Make an sdist"
|
name: "Make an sdist"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
cache-dependency-path: 'pyproject.toml'
|
cache-dependency-path: 'pyproject.toml'
|
||||||
# Build sdist on lowest supported Python
|
# Build sdist on lowest supported Python
|
||||||
python-version: '3.9'
|
python-version: "3.9"
|
||||||
- name: Build SDist
|
- name: Build SDist
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pip install build
|
pip install build
|
||||||
python -m build --sdist
|
python -m build --sdist
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: "sdist"
|
name: "sdist"
|
||||||
path: dist/*.tar.gz
|
path: dist/*.tar.gz
|
||||||
@ -195,7 +257,9 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: Download sdist
|
- name: Download sdist
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
path: sdist/
|
||||||
- name: Unpack SDist
|
- name: Unpack SDist
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@ -204,14 +268,14 @@ jobs:
|
|||||||
mkdir test
|
mkdir test
|
||||||
tar --strip-components=1 -zxf *.tar.gz -C ./test
|
tar --strip-components=1 -zxf *.tar.gz -C ./test
|
||||||
ls test
|
ls test
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
cache-dependency-path: 'sdist/test/pyproject.toml'
|
cache-dependency-path: 'sdist/test/pyproject.toml'
|
||||||
# Test sdist on lowest supported Python
|
# Test sdist on lowest supported Python
|
||||||
python-version: '3.9'
|
python-version: "3.9"
|
||||||
- name: Start MongoDB
|
- id: setup-mongodb
|
||||||
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0
|
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||||
- name: Run connect test from sdist
|
- name: Run connect test from sdist
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@ -220,3 +284,28 @@ jobs:
|
|||||||
which python
|
which python
|
||||||
pip install -e ".[test]"
|
pip install -e ".[test]"
|
||||||
PYMONGO_MUST_CONNECT=1 pytest -v -k client_context
|
PYMONGO_MUST_CONNECT=1 pytest -v -k client_context
|
||||||
|
|
||||||
|
test_minimum:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Test minimum dependencies and Python
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
|
with:
|
||||||
|
python-version: "3.9"
|
||||||
|
- id: setup-mongodb
|
||||||
|
uses: mongodb-labs/drivers-evergreen-tools@master
|
||||||
|
with:
|
||||||
|
version: "8.0"
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
uv venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
uv pip install -e ".[test]" --resolution=lowest-direct --force-reinstall
|
||||||
|
pytest -v test/test_srv_polling.py test/test_dns.py test/asynchronous/test_srv_polling.py test/asynchronous/test_dns.py
|
||||||
|
|||||||
15
.github/workflows/zizmor.yml
vendored
15
.github/workflows/zizmor.yml
vendored
@ -14,19 +14,8 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup Rust
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9 # v1
|
|
||||||
- name: Get zizmor
|
|
||||||
run: cargo install zizmor
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
run: zizmor --format sarif . > results.sarif
|
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Upload SARIF file
|
|
||||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
|
|
||||||
with:
|
|
||||||
sarif_file: results.sarif
|
|
||||||
category: zizmor
|
|
||||||
|
|||||||
7
.github/zizmor.yml
vendored
Normal file
7
.github/zizmor.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
rules:
|
||||||
|
unpinned-uses:
|
||||||
|
config:
|
||||||
|
policies:
|
||||||
|
actions/*: ref-pin
|
||||||
|
mongodb-labs/drivers-github-tools/*: ref-pin
|
||||||
|
mongodb-labs/drivers-evergreen-tools: ref-pin
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -30,6 +30,7 @@ expansion.yml
|
|||||||
.evergreen/scripts/test-env.sh
|
.evergreen/scripts/test-env.sh
|
||||||
specifications/
|
specifications/
|
||||||
results.json
|
results.json
|
||||||
|
.evergreen/atlas_x509_dev_client_certificate.pem
|
||||||
|
|
||||||
# Lambda temp files
|
# Lambda temp files
|
||||||
test/lambda/.aws-sam
|
test/lambda/.aws-sam
|
||||||
@ -40,4 +41,6 @@ test/lambda/*.json
|
|||||||
|
|
||||||
# test results and logs
|
# test results and logs
|
||||||
xunit-results/
|
xunit-results/
|
||||||
|
coverage.xml
|
||||||
server.log
|
server.log
|
||||||
|
.coverage
|
||||||
|
|||||||
@ -122,3 +122,14 @@ repos:
|
|||||||
language: python
|
language: python
|
||||||
require_serial: true
|
require_serial: true
|
||||||
additional_dependencies: ["shrub.py>=3.10.0", "pyyaml>=6.0.2"]
|
additional_dependencies: ["shrub.py>=3.10.0", "pyyaml>=6.0.2"]
|
||||||
|
|
||||||
|
- id: uv-lock
|
||||||
|
name: uv-lock
|
||||||
|
entry: uv lock
|
||||||
|
language: python
|
||||||
|
require_serial: true
|
||||||
|
files: ^(uv\.lock|pyproject\.toml|requirements.txt|requirements/.*\.txt)$
|
||||||
|
pass_filenames: false
|
||||||
|
fail_fast: true
|
||||||
|
additional_dependencies:
|
||||||
|
- "uv>=0.8.4"
|
||||||
|
|||||||
250
CONTRIBUTING.md
250
CONTRIBUTING.md
@ -16,7 +16,7 @@ be of interest or that has already been addressed.
|
|||||||
|
|
||||||
## Supported Interpreters
|
## Supported Interpreters
|
||||||
|
|
||||||
PyMongo supports CPython 3.9+ and PyPy3.10+. Language features not
|
PyMongo supports CPython 3.9+ and PyPy3.9+. Language features not
|
||||||
supported by all interpreters can not be used.
|
supported by all interpreters can not be used.
|
||||||
|
|
||||||
## Style Guide
|
## Style Guide
|
||||||
@ -85,49 +85,53 @@ likelihood for getting review sooner shoots up.
|
|||||||
- `versionadded:: 3.11`
|
- `versionadded:: 3.11`
|
||||||
- `versionchanged:: 3.5`
|
- `versionchanged:: 3.5`
|
||||||
|
|
||||||
**Pull Request Template Breakdown**
|
### AI-Generated Contributions Policy
|
||||||
|
|
||||||
- **Github PR Title**
|
#### Our Stance
|
||||||
|
|
||||||
- The PR Title format should always be
|
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.**
|
||||||
`[JIRA-ID] : Jira Title or Blurb Summary`.
|
|
||||||
|
|
||||||
- **JIRA LINK**
|
##### Why This Policy Exists
|
||||||
|
|
||||||
- Convenient link to the associated JIRA ticket.
|
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:
|
||||||
|
|
||||||
- **Summary**
|
- **Deep domain expertise**: MongoDB's wire protocol, BSON specification, connection pooling, authentication mechanisms, and concurrency patterns require an understanding that AI alone cannot substantiate.
|
||||||
|
|
||||||
- Small blurb on why this is needed. The JIRA task should have
|
- **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.
|
||||||
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.
|
|
||||||
|
|
||||||
- **Changes in this PR**
|
- **Security responsibility**: Authentication, credential handling, and TLS implementation cannot be left to probabilistic code generation.
|
||||||
|
|
||||||
- The explicit code changes that this PR is introducing. This
|
##### What This Means for Contributors
|
||||||
should be more specific than just the task name. (Unless the
|
|
||||||
task name is very clear).
|
|
||||||
|
|
||||||
- **Test Plan**
|
**Required:**
|
||||||
|
|
||||||
- Everything needs a test description. Describe what you did
|
- Full understanding of every line of code you submit
|
||||||
to validate your changes actually worked; if you did
|
- Ability to explain and defend your implementation choices
|
||||||
nothing, then document you did not test it. Aim to make
|
- Willingness to iterate and maintain your contributions
|
||||||
these steps reproducible by other engineers, specifically
|
|
||||||
with your primary reviewer in mind.
|
|
||||||
|
|
||||||
- **Screenshots**
|
**Encouraged:**
|
||||||
|
|
||||||
- Any images that provide more context to the PR. Usually,
|
- Using AI assistants as learning tools to understand concepts
|
||||||
these just coincide with the test plan.
|
- 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.
|
||||||
|
|
||||||
- **Callouts or follow-up items**
|
**Not allowed:**
|
||||||
|
|
||||||
- This is a good place for identifying "to-dos" that you've
|
- Submitting PRs generated solely by AI tools
|
||||||
placed in the code (Must have an accompanying JIRA Ticket).
|
- Copy-pasting AI-generated code without full understanding
|
||||||
- Potential bugs that you are unsure how to test in the code.
|
|
||||||
- Opinions you want to receive about your code.
|
##### 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.*
|
||||||
|
|
||||||
## Running Linters
|
## Running Linters
|
||||||
|
|
||||||
@ -190,19 +194,22 @@ just docs-serve
|
|||||||
Browse to the link provided, and then as you make changes to docstrings or narrative docs,
|
Browse to the link provided, and then as you make changes to docstrings or narrative docs,
|
||||||
the pages will re-render and the browser will automatically refresh.
|
the pages will re-render and the browser will automatically refresh.
|
||||||
|
|
||||||
|
|
||||||
## Running Tests Locally
|
## Running Tests Locally
|
||||||
|
|
||||||
- Ensure you have started the appropriate Mongo Server(s).
|
|
||||||
- Run `just install` to set a local virtual environment, or you can manually
|
- Run `just install` to set a local virtual environment, or you can manually
|
||||||
create a virtual environment and run `pytest` directly. If you want to use a specific
|
create a virtual environment and run `pytest` directly. If you want to use a specific
|
||||||
version of Python, remove the `.venv` folder and set `PYTHON_BINARY` before running `just install`.
|
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`
|
||||||
|
for a full list of options.
|
||||||
- Run `just test` or `pytest` to run all of the tests.
|
- Run `just test` or `pytest` to run all of the tests.
|
||||||
- Append `test/<mod_name>.py::<class_name>::<test_name>` to run
|
- Append `test/<mod_name>.py::<class_name>::<test_name>` to run
|
||||||
specific tests. You can omit the `<test_name>` to test a full class
|
specific tests. You can omit the `<test_name>` to test a full class
|
||||||
and the `<class_name>` to test a full module. For example:
|
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`.
|
`just test test/test_change_stream.py::TestUnifiedChangeStreamsErrors::test_change_stream_errors_on_ElectionInProgress`.
|
||||||
- Use the `-k` argument to select tests by pattern.
|
- 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
|
## Running tests that require secrets, services, or other configuration
|
||||||
@ -213,18 +220,36 @@ the pages will re-render and the browser will automatically refresh.
|
|||||||
`git clone git@github.com:mongodb-labs/drivers-evergreen-tools.git`.
|
`git clone git@github.com:mongodb-labs/drivers-evergreen-tools.git`.
|
||||||
- Run `export DRIVERS_TOOLS=$PWD/drivers-evergreen-tools`. This can be put into a `.bashrc` file
|
- Run `export DRIVERS_TOOLS=$PWD/drivers-evergreen-tools`. This can be put into a `.bashrc` file
|
||||||
for convenience.
|
for convenience.
|
||||||
- Set up access to [Drivers test secrets](https://github.com/mongodb-labs/drivers-evergreen-tools/tree/master/.evergreen/secrets_handling#secrets-handling).
|
- Some tests require access to [Drivers test secrets](https://github.com/mongodb-labs/drivers-evergreen-tools/tree/master/.evergreen/secrets_handling#secrets-handling).
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
- Run `just run-server` with optional args to set up the server. All given options will be passed to
|
- Run `just run-server` with optional args to set up the server.
|
||||||
`run-orchestration.sh` in `$DRIVERS_TOOLS`. See `$DRIVERS_TOOLS/evergreen/run-orchestration.sh -h`
|
|
||||||
for a full list of options.
|
|
||||||
- Run `just setup-tests` with optional args to set up the test environment, secrets, etc.
|
- Run `just setup-tests` with optional args to set up the test environment, secrets, etc.
|
||||||
See `just setup-tests -h` for a full list of available options.
|
See `just setup-tests -h` for a full list of available options.
|
||||||
- Run `just run-tests` to run the tests in an appropriate Python environment.
|
- Run `just run-tests` to run the tests in an appropriate Python environment.
|
||||||
- When done, run `just teardown-tests` to clean up and `just stop-server` to stop the server.
|
- When done, run `just teardown-tests` to clean up and `just stop-server` to stop the server.
|
||||||
|
|
||||||
|
### SSL tests
|
||||||
|
|
||||||
|
- Run `just run-server --ssl` to start the server with TLS enabled.
|
||||||
|
- Run `just setup-tests --ssl`.
|
||||||
|
- Run `just run-tests`.
|
||||||
|
|
||||||
|
Note: for general testing purposes with an TLS-enabled server, you can use the following (this should ONLY be used
|
||||||
|
for local testing):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
client = MongoClient(
|
||||||
|
"mongodb://localhost:27017?tls=true&tlsAllowInvalidCertificates=true"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use the actual certificate file then set `tlsCertificateKeyFile` to the local path
|
||||||
|
to `<repo_roo>/test/certificates/client.pem` and `tlsCAFile` to the local path to `<repo_roo>/test/certificates/ca.pem`.
|
||||||
|
|
||||||
### Encryption tests
|
### Encryption tests
|
||||||
|
|
||||||
- Run `just run-server` to start the server.
|
- Run `just run-server` to start the server.
|
||||||
@ -315,7 +340,7 @@ Locally you can run:
|
|||||||
|
|
||||||
- Run `just run-server`.
|
- Run `just run-server`.
|
||||||
- Run `just setup-tests`.
|
- Run `just setup-tests`.
|
||||||
- Run `UV_PYTHON=3.13t just run-tests`.
|
- Run `UV_PYTHON=3.14t just run-tests`.
|
||||||
|
|
||||||
### AWS Lambda tests
|
### AWS Lambda tests
|
||||||
|
|
||||||
@ -335,13 +360,6 @@ Note: these tests can only be run from an Evergreen Linux host that has the Pyth
|
|||||||
The `mode` can be `standalone` or `embedded`. For the `replica_set` version of the tests, use
|
The `mode` can be `standalone` or `embedded`. For the `replica_set` version of the tests, use
|
||||||
`TOPOLOGY=replica_set just run-server`.
|
`TOPOLOGY=replica_set just run-server`.
|
||||||
|
|
||||||
### Atlas Data Lake tests.
|
|
||||||
|
|
||||||
You must have `docker` or `podman` installed locally.
|
|
||||||
|
|
||||||
- Run `just setup-tests data_lake`.
|
|
||||||
- Run `just run-tests`.
|
|
||||||
|
|
||||||
### OCSP tests
|
### OCSP tests
|
||||||
|
|
||||||
- Export the orchestration file, e.g. `export ORCHESTRATION_FILE=rsa-basic-tls-ocsp-disableStapling.json`.
|
- Export the orchestration file, e.g. `export ORCHESTRATION_FILE=rsa-basic-tls-ocsp-disableStapling.json`.
|
||||||
@ -369,11 +387,21 @@ If you are running one of the `no-responder` tests, omit the `run-server` step.
|
|||||||
- Finally, you can use `just setup-tests --debug-log`.
|
- Finally, you can use `just setup-tests --debug-log`.
|
||||||
- For evergreen patch builds, you can use `evergreen patch --param DEBUG_LOG=1` to enable debug logs for failed tests in the patch.
|
- For evergreen patch builds, you can use `evergreen patch --param DEBUG_LOG=1` to enable debug logs for failed tests in the patch.
|
||||||
|
|
||||||
|
## Testing minimum dependencies
|
||||||
|
|
||||||
|
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
|
## 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
|
- 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.
|
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-orchestration.sh`, add it to the `NO_RUN_ORCHESTRATION` list in
|
- If the test uses Atlas or otherwise doesn't use `run-mongodb.sh`, add it to the `NO_RUN_ORCHESTRATION` list in
|
||||||
`.evergreen/scripts/utils.py`.
|
`.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
|
- 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`.
|
like `AUTH`, add that logic to `.evergreen/scripts/run_server.py`.
|
||||||
@ -381,10 +409,44 @@ If you are running one of the `no-responder` tests, omit the `run-server` step.
|
|||||||
- If there are any special test considerations, including not running `pytest` at all, handle it in `.evergreen/scripts/run_tests.py`.
|
- If there are any special test considerations, including not running `pytest` at all, handle it in `.evergreen/scripts/run_tests.py`.
|
||||||
- If there are any services or atlas clusters to teardown, handle them in `.evergreen/scripts/teardown_tests.py`.
|
- If there are any services or atlas clusters to teardown, handle them in `.evergreen/scripts/teardown_tests.py`.
|
||||||
- Add functions to generate the test variant(s) and task(s) to the `.evergreen/scripts/generate_config.py`.
|
- Add functions to generate the test variant(s) and task(s) to the `.evergreen/scripts/generate_config.py`.
|
||||||
|
- There are some considerations about the Python version used in the test:
|
||||||
|
- If a specific version of Python is needed in a task that is running on variants with a toolchain, use
|
||||||
|
``TOOLCHAIN_VERSION`` (e.g. `TOOLCHAIN_VERSION=3.10`). The actual path lookup needs to be done on the host, since
|
||||||
|
tasks are host-agnostic.
|
||||||
|
- If a specific Python binary is needed (for example on the FIPS host), set `UV_PYTHON=/path/to/python`.
|
||||||
|
- If a specific Python version is needed and the toolchain will not be available, use `UV_PYTHON` (e.g. `UV_PYTHON=3.11`).
|
||||||
|
- The default if neither ``TOOLCHAIN_VERSION`` or ``UV_PYTHON`` is set is to use UV to install the minimum
|
||||||
|
supported version of Python and use that. This ensures a consistent behavior across host types that do not
|
||||||
|
have the Python toolchain (e.g. Azure VMs), by having a known version of Python with the build headers (`Python.h`)
|
||||||
|
needed to build the C extensions.
|
||||||
- Regenerate the test variants and tasks using `pre-commit run --all-files generate-config`.
|
- Regenerate the test variants and tasks using `pre-commit run --all-files generate-config`.
|
||||||
- Make sure to add instructions for running the test suite to `CONTRIBUTING.md`.
|
- Make sure to add instructions for running the test suite to `CONTRIBUTING.md`.
|
||||||
|
|
||||||
## Re-sync Spec Tests
|
## Handling flaky tests
|
||||||
|
|
||||||
|
We have a custom `flaky` decorator in [test/asynchronous/utils.py](test/asynchronous/utils.py) that can be used for
|
||||||
|
tests that are `flaky`. By default the decorator only applies when not running on CPython on Linux, since other
|
||||||
|
runtimes tend to have more variation. When using the `flaky` decorator, open a corresponding ticket and
|
||||||
|
a use the ticket number as the "reason" parameter to the decorator, e.g. `@flaky(reason="PYTHON-1234")`.
|
||||||
|
When running tests locally (not in CI), the `flaky` decorator will be disabled unless `ENABLE_FLAKY` is set.
|
||||||
|
To disable the `flaky` decorator in CI, you can use `evergreen patch --param DISABLE_FLAKY=1`.
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
The `integration_tests` directory has a set of scripts that verify the usage of PyMongo with downstream packages or frameworks. See the [README](./integration_tests/README.md) for more information.
|
||||||
|
|
||||||
|
To run the tests, use `just integration_tests`.
|
||||||
|
|
||||||
|
The tests should be able to run with and without SSL enabled.
|
||||||
|
|
||||||
|
## Specification Tests
|
||||||
|
|
||||||
|
The MongoDB [specifications repository](https://github.com/mongodb/specifications)
|
||||||
|
holds in progress and completed specifications for features of MongoDB, drivers,
|
||||||
|
and associated products. PyMongo supports the [Unified Test Format](https://jira.mongodb.org/browse/DRIVERS-709)
|
||||||
|
for running specification tests to confirm PyMongo behaves as expected.
|
||||||
|
|
||||||
|
### Resynchronizing the Specification Tests
|
||||||
|
|
||||||
If you would like to re-sync the copy of the specification tests in the
|
If you would like to re-sync the copy of the specification tests in the
|
||||||
PyMongo repository with that which is inside the [specifications
|
PyMongo repository with that which is inside the [specifications
|
||||||
@ -405,17 +467,58 @@ update in PyMongo. This is primarily helpful if you are implementing a
|
|||||||
new feature in PyMongo that has spec tests already implemented, or if
|
new feature in PyMongo that has spec tests already implemented, or if
|
||||||
you are attempting to validate new spec tests in PyMongo.
|
you are attempting to validate new spec tests in PyMongo.
|
||||||
|
|
||||||
|
### Automated Specification Test Resyncing
|
||||||
|
The (`/.evergreen/scripts/resync-all-specs.sh`) script
|
||||||
|
automatically runs once a week to resync all the specs with the [specifications repo](https://github.com/mongodb/specifications).
|
||||||
|
A PR will be generated by mongodb-drivers-pr-bot containing any changes picked up by this resync.
|
||||||
|
The PR description will display the name(s) of the updated specs along
|
||||||
|
with any errors that occurred.
|
||||||
|
|
||||||
|
Spec test changes associated with a behavioral change or bugfix that has yet to be implemented in PyMongo
|
||||||
|
must be added to a patch file in `/.evergreen/spec-patch`. Each patch
|
||||||
|
file must be named after the associated PYTHON ticket and contain the
|
||||||
|
test differences between PyMongo's current tests and the specification.
|
||||||
|
All changes listed in these patch files will be *undone* by the script and won't
|
||||||
|
be applied to PyMongo's tests.
|
||||||
|
|
||||||
|
When a new test file or folder is added to the spec repo before the associated code changes are implemented, that test's path must be added to `.evergreen/remove-unimplemented-tests.sh` along with a comment indicating the associated PYTHON ticket for those changes.
|
||||||
|
|
||||||
|
Any PR that implements a PYTHON ticket documented in a patch file or within `.evergreen/remove-unimplemented-tests.sh` must also remove the associated patch file or entry in `remove-unimplemented-tests.sh`.
|
||||||
|
|
||||||
|
#### Adding to a patch file
|
||||||
|
To add to or create a patch file, run `git diff` to show the desired changes to undo and copy the
|
||||||
|
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).
|
||||||
|
```bash
|
||||||
|
./.evergreen/scripts/resync-all-specs.sh
|
||||||
|
python3 ./.evergreen/scripts/resync-all-specs.py
|
||||||
|
```
|
||||||
|
|
||||||
## Making a Release
|
## Making a Release
|
||||||
|
|
||||||
Follow the [Python Driver Release Process Wiki](https://wiki.corp.mongodb.com/display/DRIVERS/Python+Driver+Release+Process).
|
Follow the [Python Driver Release Process Wiki](https://wiki.corp.mongodb.com/display/DRIVERS/Python+Driver+Release+Process).
|
||||||
|
|
||||||
## Asyncio considerations
|
## Project Structure and Asyncio Considerations
|
||||||
|
|
||||||
PyMongo adds asyncio capability by modifying the source files in `*/asynchronous` to `*/synchronous` using
|
This section describes the layout of the `pymongo/` package.
|
||||||
[unasync](https://github.com/python-trio/unasync/) and some custom transforms.
|
|
||||||
|
|
||||||
Where possible, edit the code in `*/asynchronous/*.py` and not the synchronous files.
|
Within `pymongo/`, the code is further divided into the `pymongo/asynchronous` and `pymongo/synchronous` subdirectories.
|
||||||
You can run `pre-commit run --all-files synchro` before running tests if you are testing synchronous code.
|
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.
|
||||||
|
|
||||||
To prevent the `synchro` hook from accidentally overwriting code, it first checks to see whether a sync version
|
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.
|
of a file is changing and not its async counterpart, and will fail.
|
||||||
@ -427,8 +530,51 @@ run `pre-commit run --all-files --hook-stage manual ruff` and fix all reported e
|
|||||||
hook again.
|
hook again.
|
||||||
|
|
||||||
## Converting a test to async
|
## Converting a test to async
|
||||||
|
|
||||||
The `tools/convert_test_to_async.py` script takes in an existing synchronous test file and outputs a
|
The `tools/convert_test_to_async.py` script takes in an existing synchronous test file and outputs a
|
||||||
partially-converted asynchronous version of the same name to the `test/asynchronous` directory.
|
partially-converted asynchronous version of the same name to the `test/asynchronous` directory.
|
||||||
Use this generated file as a starting point for the completed conversion.
|
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]`
|
The script is used like so: `python tools/convert_test_to_async.py [test_file.py]`
|
||||||
|
|
||||||
|
## CPU profiling
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
2. Inside your test script, perform any required setup and then loop over the code you want to profile for improved sampling.
|
||||||
|
3. Run `py-spy record -o <output.svg> -r <sample_rate=100> -- python <path/to/script>` to generate a `.svg` file containing the flame graph.
|
||||||
|
(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
|
||||||
|
|
||||||
|
Dependabot will raise PRs at most once per week, grouped by GitHub Actions updates and Python requirement
|
||||||
|
file updates. We have a pre-commit hook that will update the `uv.lock` file when requirements change.
|
||||||
|
To update the lock file on a failing PR, you can use a method like `gh pr checkout <pr number>`, then run
|
||||||
|
`just lint uv-lock` to update the lock file, and then push the changes. If a typing dependency has changed,
|
||||||
|
also run `just typing` and handle any new findings.
|
||||||
|
|||||||
24
README.md
24
README.md
@ -4,6 +4,7 @@
|
|||||||
[](https://pypi.org/project/pymongo)
|
[](https://pypi.org/project/pymongo)
|
||||||
[](https://pepy.tech/project/pymongo)
|
[](https://pepy.tech/project/pymongo)
|
||||||
[](http://pymongo.readthedocs.io/en/stable/api?badge=stable)
|
[](http://pymongo.readthedocs.io/en/stable/api?badge=stable)
|
||||||
|
[](https://codecov.io/gh/mongodb/mongo-python-driver)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@ -14,7 +15,14 @@ a native Python driver for MongoDB, offering both synchronous and asynchronous A
|
|||||||
[gridfs](https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.md/)
|
[gridfs](https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.md/)
|
||||||
implementation on top of `pymongo`.
|
implementation on top of `pymongo`.
|
||||||
|
|
||||||
PyMongo supports MongoDB 4.0, 4.2, 4.4, 5.0, 6.0, 7.0, and 8.0.
|
PyMongo supports MongoDB 4.0, 4.2, 4.4, 5.0, 6.0, 7.0, and 8.0. PyMongo follows [semantic versioning](https://semver.org/spec/v2.0.0.html) for its releases.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Documentation is available at
|
||||||
|
[mongodb.com](https://www.mongodb.com/docs/languages/python/pymongo-driver/current/).
|
||||||
|
|
||||||
|
[API documentation](https://pymongo.readthedocs.io/en/stable/api/) and the [full changelog](https://pymongo.readthedocs.io/en/stable/changelog.html) for each release is available at [readthedocs.io](https://pymongo.readthedocs.io/en/stable/index.html).
|
||||||
|
|
||||||
## Support / Feedback
|
## Support / Feedback
|
||||||
|
|
||||||
@ -90,7 +98,7 @@ package that is incompatible with PyMongo.
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
PyMongo supports CPython 3.9+ and PyPy3.10+.
|
PyMongo supports CPython 3.9+ and PyPy3.9+.
|
||||||
|
|
||||||
Required dependencies:
|
Required dependencies:
|
||||||
|
|
||||||
@ -132,7 +140,8 @@ python -m pip install "pymongo[snappy]"
|
|||||||
```
|
```
|
||||||
|
|
||||||
Wire protocol compression with zstandard requires
|
Wire protocol compression with zstandard requires
|
||||||
[zstandard](https://pypi.org/project/zstandard):
|
[backports.zstd](https://pypi.org/project/backports.zstd)
|
||||||
|
when used with Python versions before 3.14:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pip install "pymongo[zstd]"
|
python -m pip install "pymongo[zstd]"
|
||||||
@ -191,13 +200,6 @@ ObjectId('4aba160ee23f6b543e000002')
|
|||||||
[8, 11]
|
[8, 11]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Documentation is available at
|
|
||||||
[pymongo.readthedocs.io](https://pymongo.readthedocs.io/en/stable/).
|
|
||||||
|
|
||||||
See the [contributing guide](./CONTRIBUTING.md#documentation) for how to build the documentation.
|
|
||||||
|
|
||||||
## Learning Resources
|
## Learning Resources
|
||||||
|
|
||||||
- MongoDB Learn - [Python
|
- MongoDB Learn - [Python
|
||||||
@ -214,4 +216,4 @@ pip install -e ".[test]"
|
|||||||
pytest
|
pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
For more advanced testing scenarios, see the [contributing guide](./CONTRIBUTING.md#running-tests-locally).
|
For more advanced testing scenarios, see the [contributing guide](https://github.com/mongodb/mongo-python-driver/blob/master/CONTRIBUTING.md#running-tests-locally).
|
||||||
|
|||||||
@ -130,7 +130,11 @@ if "--no_ext" in sys.argv or os.environ.get("NO_EXT"):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
ext_modules = []
|
ext_modules = []
|
||||||
elif sys.platform.startswith("java") or sys.platform == "cli" or "PyPy" in sys.version:
|
elif (
|
||||||
|
sys.platform.startswith("java")
|
||||||
|
or sys.platform == "cli"
|
||||||
|
or sys.implementation.name in ("pypy", "graalpy")
|
||||||
|
):
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
"""
|
"""
|
||||||
*****************************************************\n
|
*****************************************************\n
|
||||||
|
|||||||
@ -58,10 +58,10 @@ bytes [#bytes]_ binary both
|
|||||||
the microsecond field is truncated.
|
the microsecond field is truncated.
|
||||||
.. [#dt2] all datetime.datetime instances are encoded as UTC. By default, they
|
.. [#dt2] all datetime.datetime instances are encoded as UTC. By default, they
|
||||||
are decoded as *naive* but timezone aware datetimes are also supported.
|
are decoded as *naive* but timezone aware datetimes are also supported.
|
||||||
See :doc:`/examples/datetimes` for examples.
|
See `Dates and Times <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#dates-and-times>`_ for examples.
|
||||||
.. [#dt3] To enable decoding a bson UTC datetime to a :class:`~bson.datetime_ms.DatetimeMS`
|
.. [#dt3] To enable decoding a bson UTC datetime to a :class:`~bson.datetime_ms.DatetimeMS`
|
||||||
instance see :ref:`handling-out-of-range-datetimes`.
|
instance see `handling out of range datetimes <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes>`_.
|
||||||
.. [#uuid] For :py:class:`uuid.UUID` encoding and decoding behavior see :doc:`/examples/uuid`.
|
.. [#uuid] For :py:class:`uuid.UUID` encoding and decoding behavior see `<https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#universally-unique-ids--uuids->`_.
|
||||||
.. [#re] :class:`~bson.regex.Regex` instances and regular expression
|
.. [#re] :class:`~bson.regex.Regex` instances and regular expression
|
||||||
objects from ``re.compile()`` are both saved as BSON regular expressions.
|
objects from ``re.compile()`` are both saved as BSON regular expressions.
|
||||||
BSON regular expressions are decoded as :class:`~bson.regex.Regex`
|
BSON regular expressions are decoded as :class:`~bson.regex.Regex`
|
||||||
@ -1009,7 +1009,7 @@ def _dict_to_bson(
|
|||||||
try:
|
try:
|
||||||
elements.append(_element_to_bson(key, value, check_keys, opts))
|
elements.append(_element_to_bson(key, value, check_keys, opts))
|
||||||
except InvalidDocument as err:
|
except InvalidDocument as err:
|
||||||
raise InvalidDocument(f"Invalid document {doc} | {err}") from err
|
raise InvalidDocument(f"Invalid document: {err}", doc) from err
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise TypeError(f"encoder expected a mapping type but got: {doc!r}") from None
|
raise TypeError(f"encoder expected a mapping type but got: {doc!r}") from None
|
||||||
|
|
||||||
@ -1109,7 +1109,9 @@ def _decode_all(data: _ReadableBuffer, opts: CodecOptions[_DocumentType]) -> lis
|
|||||||
while position < end:
|
while position < end:
|
||||||
obj_size = _UNPACK_INT_FROM(data, position)[0]
|
obj_size = _UNPACK_INT_FROM(data, position)[0]
|
||||||
if data_len - position < obj_size:
|
if data_len - position < obj_size:
|
||||||
raise InvalidBSON("invalid object size")
|
raise InvalidBSON(
|
||||||
|
f"invalid object size: expected {obj_size}, got {data_len - position}"
|
||||||
|
)
|
||||||
obj_end = position + obj_size - 1
|
obj_end = position + obj_size - 1
|
||||||
if data[obj_end] != 0:
|
if data[obj_end] != 0:
|
||||||
raise InvalidBSON("bad eoo")
|
raise InvalidBSON("bad eoo")
|
||||||
@ -1327,7 +1329,7 @@ def decode_iter(
|
|||||||
elements = data[position : position + obj_size]
|
elements = data[position : position + obj_size]
|
||||||
position += obj_size
|
position += obj_size
|
||||||
|
|
||||||
yield _bson_to_dict(elements, opts) # type:ignore[misc]
|
yield _bson_to_dict(elements, opts)
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@ -1373,7 +1375,7 @@ def decode_file_iter(
|
|||||||
raise InvalidBSON("cut off in middle of objsize")
|
raise InvalidBSON("cut off in middle of objsize")
|
||||||
obj_size = _UNPACK_INT_FROM(size_data, 0)[0] - 4
|
obj_size = _UNPACK_INT_FROM(size_data, 0)[0] - 4
|
||||||
elements = size_data + file_obj.read(max(0, obj_size))
|
elements = size_data + file_obj.read(max(0, obj_size))
|
||||||
yield _bson_to_dict(elements, opts) # type:ignore[arg-type, misc]
|
yield _bson_to_dict(elements, opts) # type:ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
def is_valid(bson: bytes) -> bool:
|
def is_valid(bson: bytes) -> bool:
|
||||||
|
|||||||
@ -109,6 +109,7 @@ struct module_state {
|
|||||||
#define DATETIME_CLAMP 2
|
#define DATETIME_CLAMP 2
|
||||||
#define DATETIME_MS 3
|
#define DATETIME_MS 3
|
||||||
#define DATETIME_AUTO 4
|
#define DATETIME_AUTO 4
|
||||||
|
#define PYTHON_3_12 0x030C0000
|
||||||
|
|
||||||
/* Converts integer to its string representation in decimal notation. */
|
/* Converts integer to its string representation in decimal notation. */
|
||||||
extern int cbson_long_long_to_str(long long num, char* str, size_t size) {
|
extern int cbson_long_long_to_str(long long num, char* str, size_t size) {
|
||||||
@ -249,6 +250,67 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
|||||||
*/
|
*/
|
||||||
static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw);
|
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 */
|
/* Date stuff */
|
||||||
static PyObject* datetime_from_millis(long long millis) {
|
static PyObject* datetime_from_millis(long long millis) {
|
||||||
/* To encode a datetime instance like datetime(9999, 12, 31, 23, 59, 59, 999999)
|
/* To encode a datetime instance like datetime(9999, 12, 31, 23, 59, 59, 999999)
|
||||||
@ -294,34 +356,57 @@ static PyObject* datetime_from_millis(long long millis) {
|
|||||||
timeinfo.tm_sec,
|
timeinfo.tm_sec,
|
||||||
microseconds);
|
microseconds);
|
||||||
if(!datetime) {
|
if(!datetime) {
|
||||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||||
|
PyObject *exc = PyErr_GetRaisedException();
|
||||||
|
|
||||||
/*
|
/* Only add additional error message on ValueError exceptions. */
|
||||||
* Calling _error clears the error state, so fetch it first.
|
if (exc && PyErr_GivenExceptionMatches(exc, PyExc_ValueError)) {
|
||||||
*/
|
PyObject* err_msg = PyObject_Str(exc);
|
||||||
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) {
|
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");
|
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) {
|
if (appendage) {
|
||||||
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
|
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
|
||||||
if (msg) {
|
if (msg) {
|
||||||
Py_DECREF(evalue);
|
PyObject* new_exc = PyObject_CallOneArg(PyExc_ValueError, msg);
|
||||||
evalue = msg;
|
if (new_exc) {
|
||||||
|
exc = _transfer_traceback(exc, new_exc);
|
||||||
|
}
|
||||||
|
Py_DECREF(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Py_XDECREF(appendage);
|
Py_XDECREF(appendage);
|
||||||
}
|
}
|
||||||
Py_XDECREF(err_msg);
|
Py_XDECREF(err_msg);
|
||||||
}
|
}
|
||||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
/* Steals reference to exc. */
|
||||||
}
|
PyErr_SetRaisedException(exc);
|
||||||
/* Steals references to args. */
|
#else
|
||||||
PyErr_Restore(etype, evalue, etrace);
|
/* 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
|
||||||
}
|
}
|
||||||
return datetime;
|
return datetime;
|
||||||
}
|
}
|
||||||
@ -356,7 +441,8 @@ static PyObject* datetime_ms_from_millis(PyObject* self, long long millis){
|
|||||||
if (!(ll_millis = PyLong_FromLongLong(millis))){
|
if (!(ll_millis = PyLong_FromLongLong(millis))){
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
dt = PyObject_CallFunctionObjArgs(state->DatetimeMS, ll_millis, NULL);
|
PyObject* args[1] = {ll_millis};
|
||||||
|
dt = PyObject_Vectorcall(state->DatetimeMS, args, 1, NULL);
|
||||||
Py_DECREF(ll_millis);
|
Py_DECREF(ll_millis);
|
||||||
return dt;
|
return dt;
|
||||||
}
|
}
|
||||||
@ -401,7 +487,9 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
|||||||
int64_t min_millis_offset = 0;
|
int64_t min_millis_offset = 0;
|
||||||
int64_t max_millis_offset = 0;
|
int64_t max_millis_offset = 0;
|
||||||
if (options->tz_aware && options->tzinfo && options->tzinfo != Py_None) {
|
if (options->tz_aware && options->tzinfo && options->tzinfo != Py_None) {
|
||||||
PyObject* utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->min_datetime, NULL);
|
PyObject* utcoffset_args[2] = {options->tzinfo, state->min_datetime};
|
||||||
|
PyObject* utcoffset = PyObject_VectorcallMethod(
|
||||||
|
state->_utcoffset_str, utcoffset_args, 2, NULL);
|
||||||
if (utcoffset == NULL) {
|
if (utcoffset == NULL) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -420,7 +508,9 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
|||||||
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
|
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
|
||||||
}
|
}
|
||||||
Py_DECREF(utcoffset);
|
Py_DECREF(utcoffset);
|
||||||
utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->max_datetime, NULL);
|
utcoffset_args[1] = state->max_datetime;
|
||||||
|
utcoffset = PyObject_VectorcallMethod(
|
||||||
|
state->_utcoffset_str, utcoffset_args, 2, NULL);
|
||||||
if (utcoffset == NULL) {
|
if (utcoffset == NULL) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -481,7 +571,9 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
|||||||
|
|
||||||
/* convert to local time */
|
/* convert to local time */
|
||||||
if (options->tzinfo != Py_None) {
|
if (options->tzinfo != Py_None) {
|
||||||
PyObject* temp = PyObject_CallMethodObjArgs(value, state->_astimezone_str, options->tzinfo, NULL);
|
PyObject* astimezone_args[2] = {value, options->tzinfo};
|
||||||
|
PyObject* temp = PyObject_VectorcallMethod(
|
||||||
|
state->_astimezone_str, astimezone_args, 2, NULL);
|
||||||
Py_DECREF(value);
|
Py_DECREF(value);
|
||||||
value = temp;
|
value = temp;
|
||||||
}
|
}
|
||||||
@ -688,7 +780,8 @@ static int _load_python_objects(PyObject* module) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
compiled = PyObject_CallFunction(re_compile, "O", empty_string);
|
PyObject* compile_args[1] = {empty_string};
|
||||||
|
compiled = PyObject_Vectorcall(re_compile, compile_args, 1, NULL);
|
||||||
Py_DECREF(re_compile);
|
Py_DECREF(re_compile);
|
||||||
if (compiled == NULL) {
|
if (compiled == NULL) {
|
||||||
state->REType = NULL;
|
state->REType = NULL;
|
||||||
@ -711,13 +804,19 @@ static long _type_marker(PyObject* object, PyObject* _type_marker_str) {
|
|||||||
PyObject* type_marker = NULL;
|
PyObject* type_marker = NULL;
|
||||||
long type = 0;
|
long type = 0;
|
||||||
|
|
||||||
if (PyObject_HasAttr(object, _type_marker_str)) {
|
#if PY_VERSION_HEX >= 0x030D0000
|
||||||
type_marker = PyObject_GetAttr(object, _type_marker_str);
|
// 3.13
|
||||||
if (type_marker == NULL) {
|
if (PyObject_GetOptionalAttr(object, _type_marker_str, &type_marker) == -1) {
|
||||||
return -1;
|
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
|
* Python objects with broken __getattr__ implementations could return
|
||||||
* arbitrary types for a call to PyObject_GetAttrString. For example
|
* arbitrary types for a call to PyObject_GetAttrString. For example
|
||||||
@ -814,6 +913,7 @@ int convert_codec_options(PyObject* self, PyObject* options_obj, codec_options_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
options->is_raw_bson = (101 == type_marker);
|
options->is_raw_bson = (101 == type_marker);
|
||||||
|
options->is_dict_class = (options->document_class == (PyObject*)&PyDict_Type);
|
||||||
options->options_obj = options_obj;
|
options->options_obj = options_obj;
|
||||||
|
|
||||||
Py_INCREF(options->options_obj);
|
Py_INCREF(options->options_obj);
|
||||||
@ -1013,10 +1113,20 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
|||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Use _type_marker attribute instead of PyObject_IsInstance for better perf.
|
* 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.
|
||||||
*/
|
*/
|
||||||
type = _type_marker(value, state->_type_marker_str);
|
if (PyUnicode_CheckExact(value) || PyLong_CheckExact(value) || PyFloat_CheckExact(value) ||
|
||||||
if (type < 0) {
|
PyBool_Check(value) || PyDict_CheckExact(value) || PyList_CheckExact(value) ||
|
||||||
return 0;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -1227,7 +1337,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
|||||||
case 100:
|
case 100:
|
||||||
{
|
{
|
||||||
/* DBRef */
|
/* DBRef */
|
||||||
PyObject* as_doc = PyObject_CallMethodObjArgs(value, state->_as_doc_str, NULL);
|
PyObject* as_doc_args[1] = {value};
|
||||||
|
PyObject* as_doc = PyObject_VectorcallMethod(
|
||||||
|
state->_as_doc_str, as_doc_args, 1, NULL);
|
||||||
if (!as_doc) {
|
if (!as_doc) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -1383,7 +1495,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
|||||||
return write_unicode(buffer, value);
|
return write_unicode(buffer, value);
|
||||||
} else if (PyDateTime_Check(value)) {
|
} else if (PyDateTime_Check(value)) {
|
||||||
long long millis;
|
long long millis;
|
||||||
PyObject* utcoffset = PyObject_CallMethodObjArgs(value, state->_utcoffset_str , NULL);
|
PyObject* utcoffset_args[1] = {value};
|
||||||
|
PyObject* utcoffset = PyObject_VectorcallMethod(
|
||||||
|
state->_utcoffset_str, utcoffset_args, 1, NULL);
|
||||||
if (utcoffset == NULL)
|
if (utcoffset == NULL)
|
||||||
return 0;
|
return 0;
|
||||||
if (utcoffset != Py_None) {
|
if (utcoffset != Py_None) {
|
||||||
@ -1422,7 +1536,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
|||||||
if (!(uuid_rep_obj = PyLong_FromLong(options->uuid_rep))) {
|
if (!(uuid_rep_obj = PyLong_FromLong(options->uuid_rep))) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
binary_value = PyObject_CallMethodObjArgs(state->Binary, state->_from_uuid_str, value, uuid_rep_obj, NULL);
|
PyObject* from_uuid_args[3] = {state->Binary, value, uuid_rep_obj};
|
||||||
|
binary_value = PyObject_VectorcallMethod(
|
||||||
|
state->_from_uuid_str, from_uuid_args, 3, NULL);
|
||||||
Py_DECREF(uuid_rep_obj);
|
Py_DECREF(uuid_rep_obj);
|
||||||
|
|
||||||
if (binary_value == NULL) {
|
if (binary_value == NULL) {
|
||||||
@ -1452,7 +1568,8 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
|||||||
if (converter != NULL) {
|
if (converter != NULL) {
|
||||||
/* Transform types that have a registered converter.
|
/* Transform types that have a registered converter.
|
||||||
* A new reference is created upon transformation. */
|
* A new reference is created upon transformation. */
|
||||||
new_value = PyObject_CallFunctionObjArgs(converter, value, NULL);
|
PyObject* converter_args[1] = {value};
|
||||||
|
new_value = PyObject_Vectorcall(converter, converter_args, 1, NULL);
|
||||||
if (new_value == NULL) {
|
if (new_value == NULL) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -1466,8 +1583,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
|||||||
/* Try the fallback encoder if one is provided and we have not already
|
/* Try the fallback encoder if one is provided and we have not already
|
||||||
* attempted to use the fallback encoder. */
|
* attempted to use the fallback encoder. */
|
||||||
if (!in_fallback_call && options->type_registry.has_fallback_encoder) {
|
if (!in_fallback_call && options->type_registry.has_fallback_encoder) {
|
||||||
new_value = PyObject_CallFunctionObjArgs(
|
PyObject* fallback_args[1] = {value};
|
||||||
options->type_registry.fallback_encoder, value, NULL);
|
new_value = PyObject_Vectorcall(
|
||||||
|
options->type_registry.fallback_encoder, fallback_args, 1, NULL);
|
||||||
if (new_value == NULL) {
|
if (new_value == NULL) {
|
||||||
// propagate any exception raised by the callback
|
// propagate any exception raised by the callback
|
||||||
return 0;
|
return 0;
|
||||||
@ -1645,11 +1763,51 @@ fail:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Update Invalid Document error message to include doc.
|
/* Update Invalid Document error to include doc as a property.
|
||||||
*/
|
*/
|
||||||
void handle_invalid_doc_error(PyObject* dict) {
|
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 *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||||
PyObject *msg = NULL, *dict_str = NULL, *new_msg = NULL;
|
PyObject *msg = NULL, *new_msg = NULL, *new_evalue = NULL;
|
||||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||||
PyObject *InvalidDocument = _error("InvalidDocument");
|
PyObject *InvalidDocument = _error("InvalidDocument");
|
||||||
if (InvalidDocument == NULL) {
|
if (InvalidDocument == NULL) {
|
||||||
@ -1657,30 +1815,29 @@ void handle_invalid_doc_error(PyObject* dict) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (evalue && PyErr_GivenExceptionMatches(etype, InvalidDocument)) {
|
if (evalue && PyErr_GivenExceptionMatches(etype, InvalidDocument)) {
|
||||||
PyObject *msg = PyObject_Str(evalue);
|
msg = PyObject_Str(evalue);
|
||||||
if (msg) {
|
if (msg) {
|
||||||
// Prepend doc to the existing message
|
|
||||||
PyObject *dict_str = PyObject_Str(dict);
|
|
||||||
if (dict_str == NULL) {
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
const char * dict_str_utf8 = PyUnicode_AsUTF8(dict_str);
|
|
||||||
if (dict_str_utf8 == NULL) {
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
const char * msg_utf8 = PyUnicode_AsUTF8(msg);
|
const char * msg_utf8 = PyUnicode_AsUTF8(msg);
|
||||||
if (msg_utf8 == NULL) {
|
if (msg_utf8 == NULL) {
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
PyObject *new_msg = PyUnicode_FromFormat("Invalid document %s | %s", dict_str_utf8, msg_utf8);
|
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};
|
||||||
|
new_evalue = PyObject_Vectorcall(InvalidDocument, exc_args, 2, NULL);
|
||||||
Py_DECREF(evalue);
|
Py_DECREF(evalue);
|
||||||
Py_DECREF(etype);
|
Py_DECREF(etype);
|
||||||
etype = InvalidDocument;
|
etype = InvalidDocument;
|
||||||
InvalidDocument = NULL;
|
InvalidDocument = NULL;
|
||||||
if (new_msg) {
|
if (new_evalue) {
|
||||||
evalue = new_msg;
|
evalue = new_evalue;
|
||||||
|
new_evalue = NULL;
|
||||||
} else {
|
} else {
|
||||||
evalue = msg;
|
evalue = msg;
|
||||||
|
msg = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||||
@ -1689,8 +1846,9 @@ cleanup:
|
|||||||
PyErr_Restore(etype, evalue, etrace);
|
PyErr_Restore(etype, evalue, etrace);
|
||||||
Py_XDECREF(msg);
|
Py_XDECREF(msg);
|
||||||
Py_XDECREF(InvalidDocument);
|
Py_XDECREF(InvalidDocument);
|
||||||
Py_XDECREF(dict_str);
|
Py_XDECREF(new_evalue);
|
||||||
Py_XDECREF(new_msg);
|
Py_XDECREF(new_msg);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1946,7 +2104,8 @@ static PyObject *_dbref_hook(PyObject* self, PyObject* value) {
|
|||||||
PyMapping_DelItem(value, state->_dollar_db_str);
|
PyMapping_DelItem(value, state->_dollar_db_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = PyObject_CallFunctionObjArgs(state->DBRef, ref, id, database, value, NULL);
|
PyObject* dbref_args[4] = {ref, id, database, value};
|
||||||
|
ret = PyObject_Vectorcall(state->DBRef, dbref_args, 4, NULL);
|
||||||
Py_DECREF(value);
|
Py_DECREF(value);
|
||||||
} else {
|
} else {
|
||||||
ret = value;
|
ret = value;
|
||||||
@ -2122,7 +2281,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
}
|
}
|
||||||
memcpy(&length, buffer + *position, 4);
|
memcpy(&length, buffer + *position, 4);
|
||||||
length = BSON_UINT32_FROM_LE(length);
|
length = BSON_UINT32_FROM_LE(length);
|
||||||
if (max < length) {
|
if (max - 5 < length) { // Account for 5-byte header. max >= 5 guaranteed above
|
||||||
goto invalid;
|
goto invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2162,7 +2321,13 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
goto uuiderror;
|
goto uuiderror;
|
||||||
}
|
}
|
||||||
|
|
||||||
binary_value = PyObject_CallFunction(state->Binary, "(Oi)", data, subtype);
|
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);
|
||||||
if (binary_value == NULL) {
|
if (binary_value == NULL) {
|
||||||
goto uuiderror;
|
goto uuiderror;
|
||||||
}
|
}
|
||||||
@ -2177,7 +2342,9 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
if (!uuid_rep_obj) {
|
if (!uuid_rep_obj) {
|
||||||
goto uuiderror;
|
goto uuiderror;
|
||||||
}
|
}
|
||||||
value = PyObject_CallMethodObjArgs(binary_value, state->_as_uuid_str, uuid_rep_obj, NULL);
|
PyObject* as_uuid_args[2] = {binary_value, uuid_rep_obj};
|
||||||
|
value = PyObject_VectorcallMethod(
|
||||||
|
state->_as_uuid_str, as_uuid_args, 2, NULL);
|
||||||
Py_DECREF(uuid_rep_obj);
|
Py_DECREF(uuid_rep_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2196,7 +2363,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
Py_DECREF(data);
|
Py_DECREF(data);
|
||||||
goto invalid;
|
goto invalid;
|
||||||
}
|
}
|
||||||
value = PyObject_CallFunctionObjArgs(state->Binary, data, st, NULL);
|
PyObject* binary_args[2] = {data, st};
|
||||||
|
value = PyObject_Vectorcall(state->Binary, binary_args, 2, NULL);
|
||||||
Py_DECREF(st);
|
Py_DECREF(st);
|
||||||
Py_DECREF(data);
|
Py_DECREF(data);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -2217,7 +2385,13 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
if (max < 12) {
|
if (max < 12) {
|
||||||
goto invalid;
|
goto invalid;
|
||||||
}
|
}
|
||||||
value = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12);
|
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);
|
||||||
*position += 12;
|
*position += 12;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2296,7 +2470,14 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
}
|
}
|
||||||
*position += (unsigned)flags_length + 1;
|
*position += (unsigned)flags_length + 1;
|
||||||
|
|
||||||
value = PyObject_CallFunction(state->Regex, "Oi", pattern, flags);
|
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);
|
||||||
Py_DECREF(pattern);
|
Py_DECREF(pattern);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2329,13 +2510,21 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
}
|
}
|
||||||
*position += coll_length;
|
*position += coll_length;
|
||||||
|
|
||||||
id = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12);
|
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);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
Py_DECREF(collection);
|
Py_DECREF(collection);
|
||||||
goto invalid;
|
goto invalid;
|
||||||
}
|
}
|
||||||
*position += 12;
|
*position += 12;
|
||||||
value = PyObject_CallFunctionObjArgs(state->DBRef, collection, id, NULL);
|
PyObject* dbref_args[2] = {collection, id};
|
||||||
|
value = PyObject_Vectorcall(state->DBRef, dbref_args, 2, NULL);
|
||||||
Py_DECREF(collection);
|
Py_DECREF(collection);
|
||||||
Py_DECREF(id);
|
Py_DECREF(id);
|
||||||
break;
|
break;
|
||||||
@ -2365,7 +2554,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
goto invalid;
|
goto invalid;
|
||||||
}
|
}
|
||||||
*position += value_length;
|
*position += value_length;
|
||||||
value = PyObject_CallFunctionObjArgs(state->Code, code, NULL, NULL);
|
PyObject* code_args[1] = {code};
|
||||||
|
value = PyObject_Vectorcall(state->Code, code_args, 1, NULL);
|
||||||
Py_DECREF(code);
|
Py_DECREF(code);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2431,7 +2621,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
}
|
}
|
||||||
*position += scope_size;
|
*position += scope_size;
|
||||||
|
|
||||||
value = PyObject_CallFunctionObjArgs(state->Code, code, scope, NULL);
|
PyObject* code_scope_args[2] = {code, scope};
|
||||||
|
value = PyObject_Vectorcall(state->Code, code_scope_args, 2, NULL);
|
||||||
Py_DECREF(code);
|
Py_DECREF(code);
|
||||||
Py_DECREF(scope);
|
Py_DECREF(scope);
|
||||||
break;
|
break;
|
||||||
@ -2461,7 +2652,19 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
memcpy(&time, buffer + *position + 4, 4);
|
memcpy(&time, buffer + *position + 4, 4);
|
||||||
inc = BSON_UINT32_FROM_LE(inc);
|
inc = BSON_UINT32_FROM_LE(inc);
|
||||||
time = BSON_UINT32_FROM_LE(time);
|
time = BSON_UINT32_FROM_LE(time);
|
||||||
value = PyObject_CallFunction(state->Timestamp, "II", time, inc);
|
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);
|
||||||
*position += 8;
|
*position += 8;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2473,7 +2676,13 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
}
|
}
|
||||||
memcpy(&ll, buffer + *position, 8);
|
memcpy(&ll, buffer + *position, 8);
|
||||||
ll = (int64_t)BSON_UINT64_FROM_LE(ll);
|
ll = (int64_t)BSON_UINT64_FROM_LE(ll);
|
||||||
value = PyObject_CallFunction(state->BSONInt64, "L", 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);
|
||||||
*position += 8;
|
*position += 8;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2486,19 +2695,21 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
if (!_bytes_obj) {
|
if (!_bytes_obj) {
|
||||||
goto invalid;
|
goto invalid;
|
||||||
}
|
}
|
||||||
value = PyObject_CallMethodObjArgs(state->Decimal128, state->_from_bid_str, _bytes_obj, NULL);
|
PyObject* dec128_args[2] = {state->Decimal128, _bytes_obj};
|
||||||
|
value = PyObject_VectorcallMethod(
|
||||||
|
state->_from_bid_str, dec128_args, 2, NULL);
|
||||||
Py_DECREF(_bytes_obj);
|
Py_DECREF(_bytes_obj);
|
||||||
*position += 16;
|
*position += 16;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 255:
|
case 255:
|
||||||
{
|
{
|
||||||
value = PyObject_CallFunctionObjArgs(state->MinKey, NULL);
|
value = PyObject_Vectorcall(state->MinKey, NULL, 0, NULL);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 127:
|
case 127:
|
||||||
{
|
{
|
||||||
value = PyObject_CallFunctionObjArgs(state->MaxKey, NULL);
|
value = PyObject_Vectorcall(state->MaxKey, NULL, 0, NULL);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -2550,7 +2761,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
}
|
}
|
||||||
converter = PyDict_GetItem(options->type_registry.decoder_map, value_type);
|
converter = PyDict_GetItem(options->type_registry.decoder_map, value_type);
|
||||||
if (converter != NULL) {
|
if (converter != NULL) {
|
||||||
PyObject* new_value = PyObject_CallFunctionObjArgs(converter, value, NULL);
|
PyObject* converter_args[1] = {value};
|
||||||
|
PyObject* new_value = PyObject_Vectorcall(converter, converter_args, 1, NULL);
|
||||||
Py_DECREF(value_type);
|
Py_DECREF(value_type);
|
||||||
Py_DECREF(value);
|
Py_DECREF(value);
|
||||||
return new_value;
|
return new_value;
|
||||||
@ -2568,42 +2780,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
|||||||
* Wrap any non-InvalidBSON errors in InvalidBSON.
|
* Wrap any non-InvalidBSON errors in InvalidBSON.
|
||||||
*/
|
*/
|
||||||
if (PyErr_Occurred()) {
|
if (PyErr_Occurred()) {
|
||||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
_rewrap_as_invalid_bson();
|
||||||
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 {
|
} else {
|
||||||
PyObject *InvalidBSON = _error("InvalidBSON");
|
PyObject *InvalidBSON = _error("InvalidBSON");
|
||||||
if (InvalidBSON) {
|
if (InvalidBSON) {
|
||||||
@ -2641,25 +2818,7 @@ static int _element_to_dict(PyObject* self, const char* string,
|
|||||||
if (!*name) {
|
if (!*name) {
|
||||||
/* If NULL is returned then wrap the UnicodeDecodeError
|
/* If NULL is returned then wrap the UnicodeDecodeError
|
||||||
in an InvalidBSON error */
|
in an InvalidBSON error */
|
||||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
_rewrap_as_invalid_bson();
|
||||||
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;
|
return -1;
|
||||||
}
|
}
|
||||||
position += (unsigned)name_length + 1;
|
position += (unsigned)name_length + 1;
|
||||||
@ -2718,11 +2877,20 @@ static PyObject* _elements_to_dict(PyObject* self, const char* string,
|
|||||||
unsigned max,
|
unsigned max,
|
||||||
const codec_options_t* options) {
|
const codec_options_t* options) {
|
||||||
unsigned position = 0;
|
unsigned position = 0;
|
||||||
PyObject* dict = PyObject_CallObject(options->document_class, NULL);
|
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);
|
||||||
|
}
|
||||||
if (!dict) {
|
if (!dict) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
int raw_array = 0;
|
|
||||||
while (position < max) {
|
while (position < max) {
|
||||||
PyObject* name = NULL;
|
PyObject* name = NULL;
|
||||||
PyObject* value = NULL;
|
PyObject* value = NULL;
|
||||||
@ -2737,7 +2905,24 @@ static PyObject* _elements_to_dict(PyObject* self, const char* string,
|
|||||||
position = (unsigned)new_position;
|
position = (unsigned)new_position;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject_SetItem(dict, name, value);
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Py_DECREF(name);
|
Py_DECREF(name);
|
||||||
Py_DECREF(value);
|
Py_DECREF(value);
|
||||||
}
|
}
|
||||||
@ -2749,9 +2934,14 @@ static PyObject* elements_to_dict(PyObject* self, const char* string,
|
|||||||
const codec_options_t* options) {
|
const codec_options_t* options) {
|
||||||
PyObject* result;
|
PyObject* result;
|
||||||
if (options->is_raw_bson) {
|
if (options->is_raw_bson) {
|
||||||
return PyObject_CallFunction(
|
PyObject* bson_bytes = PyBytes_FromStringAndSize(string, max);
|
||||||
options->document_class, "y#O",
|
if (!bson_bytes) {
|
||||||
string, max, options->options_obj);
|
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;
|
||||||
}
|
}
|
||||||
if (Py_EnterRecursiveCall(" while decoding a BSON document"))
|
if (Py_EnterRecursiveCall(" while decoding a BSON document"))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|||||||
@ -72,6 +72,7 @@ typedef struct codec_options_t {
|
|||||||
unsigned char datetime_conversion;
|
unsigned char datetime_conversion;
|
||||||
PyObject* options_obj;
|
PyObject* options_obj;
|
||||||
unsigned char is_raw_bson;
|
unsigned char is_raw_bson;
|
||||||
|
unsigned char is_dict_class;
|
||||||
} codec_options_t;
|
} codec_options_t;
|
||||||
|
|
||||||
/* C API functions */
|
/* C API functions */
|
||||||
|
|||||||
230
bson/binary.py
230
bson/binary.py
@ -14,6 +14,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
import warnings
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any, Optional, Sequence, Tuple, Type, Union, overload
|
from typing import TYPE_CHECKING, Any, Optional, Sequence, Tuple, Type, Union, overload
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@ -64,6 +65,9 @@ if TYPE_CHECKING:
|
|||||||
from array import array as _array
|
from array import array as _array
|
||||||
from mmap import mmap as _mmap
|
from mmap import mmap as _mmap
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
|
||||||
class UuidRepresentation:
|
class UuidRepresentation:
|
||||||
UNSPECIFIED = 0
|
UNSPECIFIED = 0
|
||||||
@ -78,7 +82,7 @@ class UuidRepresentation:
|
|||||||
:class:`~bson.binary.Binary` instance will be returned instead of a
|
:class:`~bson.binary.Binary` instance will be returned instead of a
|
||||||
:class:`uuid.UUID` instance.
|
:class:`uuid.UUID` instance.
|
||||||
|
|
||||||
See :ref:`unspecified-representation-details` for details.
|
See `unspecified representation details <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#unspecified>`_ for details.
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. versionadded:: 3.11
|
||||||
"""
|
"""
|
||||||
@ -90,7 +94,7 @@ class UuidRepresentation:
|
|||||||
and decoded from BSON binary, using RFC-4122 byte order with
|
and decoded from BSON binary, using RFC-4122 byte order with
|
||||||
binary subtype :data:`UUID_SUBTYPE`.
|
binary subtype :data:`UUID_SUBTYPE`.
|
||||||
|
|
||||||
See :ref:`standard-representation-details` for details.
|
See `standard representation details <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#standard>`_ for details.
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. versionadded:: 3.11
|
||||||
"""
|
"""
|
||||||
@ -102,7 +106,7 @@ class UuidRepresentation:
|
|||||||
and decoded from BSON binary, using RFC-4122 byte order with
|
and decoded from BSON binary, using RFC-4122 byte order with
|
||||||
binary subtype :data:`OLD_UUID_SUBTYPE`.
|
binary subtype :data:`OLD_UUID_SUBTYPE`.
|
||||||
|
|
||||||
See :ref:`python-legacy-representation-details` for details.
|
See `python legacy representation details <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#python_legacy>`_ for details.
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. versionadded:: 3.11
|
||||||
"""
|
"""
|
||||||
@ -114,7 +118,7 @@ class UuidRepresentation:
|
|||||||
and decoded from BSON binary subtype :data:`OLD_UUID_SUBTYPE`,
|
and decoded from BSON binary subtype :data:`OLD_UUID_SUBTYPE`,
|
||||||
using the Java driver's legacy byte order.
|
using the Java driver's legacy byte order.
|
||||||
|
|
||||||
See :ref:`java-legacy-representation-details` for details.
|
See `Java Legacy UUID <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#java_legacy>`_ for details.
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. versionadded:: 3.11
|
||||||
"""
|
"""
|
||||||
@ -126,7 +130,7 @@ class UuidRepresentation:
|
|||||||
and decoded from BSON binary subtype :data:`OLD_UUID_SUBTYPE`,
|
and decoded from BSON binary subtype :data:`OLD_UUID_SUBTYPE`,
|
||||||
using the C# driver's legacy byte order.
|
using the C# driver's legacy byte order.
|
||||||
|
|
||||||
See :ref:`csharp-legacy-representation-details` for details.
|
See `C# Legacy UUID <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#csharp_legacy>`_ for details.
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. versionadded:: 3.11
|
||||||
"""
|
"""
|
||||||
@ -233,13 +237,20 @@ class BinaryVector:
|
|||||||
|
|
||||||
__slots__ = ("data", "dtype", "padding")
|
__slots__ = ("data", "dtype", "padding")
|
||||||
|
|
||||||
def __init__(self, data: Sequence[float | int], dtype: BinaryVectorDtype, padding: int = 0):
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: Union[Sequence[float | int], npt.NDArray[np.number]],
|
||||||
|
dtype: BinaryVectorDtype,
|
||||||
|
padding: int = 0,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param data: Sequence of numbers representing the mathematical vector.
|
:param data: Sequence of numbers representing the mathematical vector.
|
||||||
:param dtype: The data type stored in binary
|
:param dtype: The data type stored in binary
|
||||||
:param padding: The number of bits in the final byte that are to be ignored
|
: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
|
when a vector element's size is less than a byte
|
||||||
and the length of the vector is not a multiple of 8.
|
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.data = data
|
||||||
self.dtype = dtype
|
self.dtype = dtype
|
||||||
@ -255,6 +266,9 @@ class BinaryVector:
|
|||||||
self.dtype == other.dtype and self.padding == other.padding and self.data == other.data
|
self.dtype == other.dtype and self.padding == other.padding and self.data == other.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
|
|
||||||
class Binary(bytes):
|
class Binary(bytes):
|
||||||
"""Representation of BSON binary data.
|
"""Representation of BSON binary data.
|
||||||
@ -294,7 +308,7 @@ class Binary(bytes):
|
|||||||
|
|
||||||
def __new__(
|
def __new__(
|
||||||
cls: Type[Binary],
|
cls: Type[Binary],
|
||||||
data: Union[memoryview, bytes, _mmap, _array[Any]],
|
data: Union[memoryview, bytes, bytearray, _mmap, _array[Any]],
|
||||||
subtype: int = BINARY_SUBTYPE,
|
subtype: int = BINARY_SUBTYPE,
|
||||||
) -> Binary:
|
) -> Binary:
|
||||||
if not isinstance(subtype, int):
|
if not isinstance(subtype, int):
|
||||||
@ -324,7 +338,7 @@ class Binary(bytes):
|
|||||||
:param uuid_representation: A member of
|
:param uuid_representation: A member of
|
||||||
:class:`~bson.binary.UuidRepresentation`. Default:
|
:class:`~bson.binary.UuidRepresentation`. Default:
|
||||||
:const:`~bson.binary.UuidRepresentation.STANDARD`.
|
:const:`~bson.binary.UuidRepresentation.STANDARD`.
|
||||||
See :ref:`handling-uuid-data-example` for details.
|
See `UUID representations <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#universally-unique-ids--uuids->`_ for details.
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. versionadded:: 3.11
|
||||||
"""
|
"""
|
||||||
@ -373,7 +387,7 @@ class Binary(bytes):
|
|||||||
:param uuid_representation: A member of
|
:param uuid_representation: A member of
|
||||||
:class:`~bson.binary.UuidRepresentation`. Default:
|
:class:`~bson.binary.UuidRepresentation`. Default:
|
||||||
:const:`~bson.binary.UuidRepresentation.STANDARD`.
|
:const:`~bson.binary.UuidRepresentation.STANDARD`.
|
||||||
See :ref:`handling-uuid-data-example` for details.
|
See `UUID representations <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#universally-unique-ids--uuids->`_ for details.
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. versionadded:: 3.11
|
||||||
"""
|
"""
|
||||||
@ -421,9 +435,19 @@ class Binary(bytes):
|
|||||||
...
|
...
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@overload
|
||||||
def from_vector(
|
def from_vector(
|
||||||
cls: Type[Binary],
|
cls: Type[Binary],
|
||||||
vector: Union[BinaryVector, list[int], list[float]],
|
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]],
|
||||||
dtype: Optional[BinaryVectorDtype] = None,
|
dtype: Optional[BinaryVectorDtype] = None,
|
||||||
padding: Optional[int] = None,
|
padding: Optional[int] = None,
|
||||||
) -> Binary:
|
) -> Binary:
|
||||||
@ -439,6 +463,9 @@ class Binary(bytes):
|
|||||||
:param padding: For fractional bytes, number of bits to ignore at end of vector.
|
:param padding: For fractional bytes, number of bits to ignore at end of vector.
|
||||||
:return: Binary packed data identified by dtype and padding.
|
:return: Binary packed data identified by dtype and padding.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.14
|
||||||
|
When padding is non-zero, ignored bits should be zero. Raise exception on encoding, warn on decoding.
|
||||||
|
|
||||||
.. versionadded:: 4.10
|
.. versionadded:: 4.10
|
||||||
"""
|
"""
|
||||||
if isinstance(vector, BinaryVector):
|
if isinstance(vector, BinaryVector):
|
||||||
@ -452,30 +479,72 @@ class Binary(bytes):
|
|||||||
vector = vector.data # type: ignore
|
vector = vector.data # type: ignore
|
||||||
|
|
||||||
padding = 0 if padding is None else padding
|
padding = 0 if padding is None else padding
|
||||||
if dtype == BinaryVectorDtype.INT8: # pack ints in [-128, 127] as signed int8
|
if not isinstance(dtype, BinaryVectorDtype):
|
||||||
format_str = "b"
|
raise TypeError(
|
||||||
if padding:
|
"dtype must be a bson.BinaryVectorDtype of BinaryVectorDType.INT8, PACKED_BIT, FLOAT32"
|
||||||
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)
|
metadata = struct.pack("<sB", dtype.value, padding)
|
||||||
data = struct.pack(f"<{len(vector)}{format_str}", *vector) # type: ignore
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
return cls(metadata + data, subtype=VECTOR_SUBTYPE)
|
||||||
|
|
||||||
def as_vector(self) -> BinaryVector:
|
def as_vector(self, return_numpy: bool = False) -> BinaryVector:
|
||||||
"""From the Binary, create a list of numbers, along with dtype and padding.
|
"""From the Binary, create a list or 1-d numpy array 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
|
:return: BinaryVector
|
||||||
|
|
||||||
.. versionadded:: 4.10
|
.. versionadded:: 4.10
|
||||||
@ -484,39 +553,84 @@ class Binary(bytes):
|
|||||||
if self.subtype != VECTOR_SUBTYPE:
|
if self.subtype != VECTOR_SUBTYPE:
|
||||||
raise ValueError(f"Cannot decode subtype {self.subtype} as a vector")
|
raise ValueError(f"Cannot decode subtype {self.subtype} as a vector")
|
||||||
|
|
||||||
position = 0
|
dtype, padding = struct.unpack_from("<sB", self)
|
||||||
dtype, padding = struct.unpack_from("<sB", self, position)
|
|
||||||
position += 2
|
|
||||||
dtype = BinaryVectorDtype(dtype)
|
dtype = BinaryVectorDtype(dtype)
|
||||||
n_values = len(self) - position
|
offset = 2
|
||||||
|
n_bytes = len(self) - offset
|
||||||
|
|
||||||
if dtype == BinaryVectorDtype.INT8:
|
if padding and dtype != BinaryVectorDtype.PACKED_BIT:
|
||||||
dtype_format = "b"
|
raise ValueError(
|
||||||
format_string = f"<{n_values}{dtype_format}"
|
f"Corrupt data. Padding ({padding}) must be 0 for all but PACKED_BIT dtypes. ({dtype=})"
|
||||||
vector = list(struct.unpack_from(format_string, self, position))
|
)
|
||||||
return BinaryVector(vector, dtype, padding)
|
|
||||||
|
|
||||||
elif dtype == BinaryVectorDtype.FLOAT32:
|
if not return_numpy:
|
||||||
n_bytes = len(self) - position
|
if dtype == BinaryVectorDtype.INT8:
|
||||||
n_values = n_bytes // 4
|
dtype_format = "b"
|
||||||
if n_bytes % 4:
|
format_string = f"<{n_bytes}{dtype_format}"
|
||||||
raise ValueError(
|
vector = list(struct.unpack_from(format_string, self, offset))
|
||||||
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
|
return BinaryVector(vector, dtype, padding)
|
||||||
)
|
|
||||||
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:
|
elif dtype == BinaryVectorDtype.FLOAT32:
|
||||||
# data packed as uint8
|
n_values = n_bytes // 4
|
||||||
dtype_format = "B"
|
if n_bytes % 4:
|
||||||
format_string = f"<{n_values}{dtype_format}"
|
raise ValueError(
|
||||||
unpacked_uint8s = list(struct.unpack_from(format_string, self, position))
|
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
|
||||||
return BinaryVector(unpacked_uint8s, dtype, padding)
|
)
|
||||||
|
dtype_format = "f"
|
||||||
|
format_string = f"<{n_values}{dtype_format}"
|
||||||
|
vector = list(struct.unpack_from(format_string, self, offset))
|
||||||
|
return BinaryVector(vector, dtype, padding)
|
||||||
|
|
||||||
else:
|
elif dtype == BinaryVectorDtype.PACKED_BIT:
|
||||||
raise NotImplementedError("Binary Vector dtype %s not yet supported" % dtype.name)
|
# 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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subtype(self) -> int:
|
def subtype(self) -> int:
|
||||||
|
|||||||
@ -57,7 +57,7 @@ class TypeEncoder(abc.ABC):
|
|||||||
Codec classes must implement the ``python_type`` attribute, and the
|
Codec classes must implement the ``python_type`` attribute, and the
|
||||||
``transform_python`` method to support encoding.
|
``transform_python`` method to support encoding.
|
||||||
|
|
||||||
See :ref:`custom-type-type-codec` documentation for an example.
|
See `encode data with type codecs <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/custom-types/type-codecs/#encode-data-with-type-codecs>`_ documentation for an example.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractproperty
|
@abc.abstractproperty
|
||||||
@ -76,7 +76,7 @@ class TypeDecoder(abc.ABC):
|
|||||||
Codec classes must implement the ``bson_type`` attribute, and the
|
Codec classes must implement the ``bson_type`` attribute, and the
|
||||||
``transform_bson`` method to support decoding.
|
``transform_bson`` method to support decoding.
|
||||||
|
|
||||||
See :ref:`custom-type-type-codec` documentation for an example.
|
See `encode data with type codecs <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/custom-types/type-codecs/#encode-data-with-type-codecs>`_ documentation for an example.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractproperty
|
@abc.abstractproperty
|
||||||
@ -98,7 +98,7 @@ class TypeCodec(TypeEncoder, TypeDecoder):
|
|||||||
``bson_type`` attribute, and the ``transform_bson`` method to support
|
``bson_type`` attribute, and the ``transform_bson`` method to support
|
||||||
decoding.
|
decoding.
|
||||||
|
|
||||||
See :ref:`custom-type-type-codec` documentation for an example.
|
See `encode data with type codecs <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/custom-types/type-codecs/#encode-data-with-type-codecs>`_ documentation for an example.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ class TypeRegistry:
|
|||||||
>>> type_registry = TypeRegistry([Codec1, Codec2, Codec3, ...],
|
>>> type_registry = TypeRegistry([Codec1, Codec2, Codec3, ...],
|
||||||
... fallback_encoder)
|
... fallback_encoder)
|
||||||
|
|
||||||
See :ref:`custom-type-type-registry` documentation for an example.
|
See `add codec to the type registry <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/custom-types/type-codecs/#add-codec-to-the-type-registry>`_ documentation for an example.
|
||||||
|
|
||||||
:param type_codecs: iterable of type codec instances. If
|
:param type_codecs: iterable of type codec instances. If
|
||||||
``type_codecs`` contains multiple codecs that transform a single
|
``type_codecs`` contains multiple codecs that transform a single
|
||||||
@ -128,7 +128,7 @@ class TypeRegistry:
|
|||||||
type.
|
type.
|
||||||
:param fallback_encoder: callable that accepts a single,
|
:param fallback_encoder: callable that accepts a single,
|
||||||
unencodable python value and transforms it into a type that
|
unencodable python value and transforms it into a type that
|
||||||
:mod:`bson` can encode. See :ref:`fallback-encoder-callable`
|
:mod:`bson` can encode. See `define a fallback encoder <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/custom-types/type-codecs/#define-a-fallback-encoder>`_
|
||||||
documentation for an example.
|
documentation for an example.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -160,6 +160,16 @@ class TypeRegistry:
|
|||||||
f"Expected an instance of {TypeEncoder.__name__}, {TypeDecoder.__name__}, or {TypeCodec.__name__}, got {codec!r} instead"
|
f"Expected an instance of {TypeEncoder.__name__}, {TypeDecoder.__name__}, or {TypeCodec.__name__}, got {codec!r} instead"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def codecs(self) -> list[TypeEncoder | TypeDecoder | TypeCodec]:
|
||||||
|
"""The list of type codecs in this registry."""
|
||||||
|
return self.__type_codecs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fallback_encoder(self) -> Optional[_Fallback]:
|
||||||
|
"""The fallback encoder in this registry."""
|
||||||
|
return self._fallback_encoder
|
||||||
|
|
||||||
def _validate_type_encoder(self, codec: _Codec) -> None:
|
def _validate_type_encoder(self, codec: _Codec) -> None:
|
||||||
from bson import _BUILT_IN_TYPES
|
from bson import _BUILT_IN_TYPES
|
||||||
|
|
||||||
@ -263,9 +273,6 @@ if TYPE_CHECKING:
|
|||||||
def _arguments_repr(self) -> str:
|
def _arguments_repr(self) -> str:
|
||||||
...
|
...
|
||||||
|
|
||||||
def _options_dict(self) -> dict[Any, Any]:
|
|
||||||
...
|
|
||||||
|
|
||||||
# NamedTuple API
|
# NamedTuple API
|
||||||
@classmethod
|
@classmethod
|
||||||
def _make(cls, obj: Iterable[Any]) -> CodecOptions[_DocumentType]:
|
def _make(cls, obj: Iterable[Any]) -> CodecOptions[_DocumentType]:
|
||||||
@ -317,10 +324,10 @@ else:
|
|||||||
>>> doc._id
|
>>> doc._id
|
||||||
ObjectId('5b3016359110ea14e8c58b93')
|
ObjectId('5b3016359110ea14e8c58b93')
|
||||||
|
|
||||||
See :doc:`/examples/datetimes` for examples using the `tz_aware` and
|
See `Dates and Times <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#dates-and-times>`_ for examples using the `tz_aware` and
|
||||||
`tzinfo` options.
|
`tzinfo` options.
|
||||||
|
|
||||||
See :doc:`/examples/uuid` for examples using the `uuid_representation`
|
See `UUID <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#universally-unique-ids--uuids->`_ for examples using the `uuid_representation`
|
||||||
option.
|
option.
|
||||||
|
|
||||||
:param document_class: BSON documents returned in queries will be decoded
|
:param document_class: BSON documents returned in queries will be decoded
|
||||||
@ -334,7 +341,7 @@ else:
|
|||||||
:data:`~bson.binary.UuidRepresentation.UNSPECIFIED`. New
|
:data:`~bson.binary.UuidRepresentation.UNSPECIFIED`. New
|
||||||
applications should consider setting this to
|
applications should consider setting this to
|
||||||
:data:`~bson.binary.UuidRepresentation.STANDARD` for cross language
|
:data:`~bson.binary.UuidRepresentation.STANDARD` for cross language
|
||||||
compatibility. See :ref:`handling-uuid-data-example` for details.
|
compatibility. See `UUID representations <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#universally-unique-ids--uuids->`_ for details.
|
||||||
:param unicode_decode_error_handler: The error handler to apply when
|
:param unicode_decode_error_handler: The error handler to apply when
|
||||||
a Unicode-related error occurs during BSON decoding that would
|
a Unicode-related error occurs during BSON decoding that would
|
||||||
otherwise raise :exc:`UnicodeDecodeError`. Valid options include
|
otherwise raise :exc:`UnicodeDecodeError`. Valid options include
|
||||||
@ -456,19 +463,6 @@ 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:
|
def __repr__(self) -> str:
|
||||||
return f"{self.__class__.__name__}({self._arguments_repr()})"
|
return f"{self.__class__.__name__}({self._arguments_repr()})"
|
||||||
|
|
||||||
@ -484,7 +478,7 @@ else:
|
|||||||
|
|
||||||
.. versionadded:: 3.5
|
.. versionadded:: 3.5
|
||||||
"""
|
"""
|
||||||
opts = self._options_dict()
|
opts = self._asdict()
|
||||||
opts.update(kwargs)
|
opts.update(kwargs)
|
||||||
return CodecOptions(**opts)
|
return CodecOptions(**opts)
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ class DatetimeMS:
|
|||||||
|
|
||||||
To decode UTC datetimes as a ``DatetimeMS``, `datetime_conversion` in
|
To decode UTC datetimes as a ``DatetimeMS``, `datetime_conversion` in
|
||||||
:class:`~bson.codec_options.CodecOptions` must be set to 'datetime_ms' or
|
:class:`~bson.codec_options.CodecOptions` must be set to 'datetime_ms' or
|
||||||
'datetime_auto'. See :ref:`handling-out-of-range-datetimes` for
|
'datetime_auto'. See `handling out of range datetimes <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes>`_ for
|
||||||
details.
|
details.
|
||||||
|
|
||||||
:param value: An instance of :class:`datetime.datetime` to be
|
:param value: An instance of :class:`datetime.datetime` to be
|
||||||
|
|||||||
@ -20,8 +20,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import decimal
|
import decimal
|
||||||
import struct
|
import struct
|
||||||
|
from decimal import Decimal
|
||||||
from typing import Any, Sequence, Tuple, Type, Union
|
from typing import Any, Sequence, Tuple, Type, Union
|
||||||
|
|
||||||
|
from bson.codec_options import TypeDecoder, TypeEncoder
|
||||||
|
|
||||||
_PACK_64 = struct.Struct("<Q").pack
|
_PACK_64 = struct.Struct("<Q").pack
|
||||||
_UNPACK_64 = struct.Struct("<Q").unpack
|
_UNPACK_64 = struct.Struct("<Q").unpack
|
||||||
|
|
||||||
@ -58,6 +61,42 @@ _DEC128_CTX = decimal.Context(**_CTX_OPTIONS.copy()) # type: ignore
|
|||||||
_VALUE_OPTIONS = Union[decimal.Decimal, float, str, Tuple[int, Sequence[int], int]]
|
_VALUE_OPTIONS = Union[decimal.Decimal, float, str, Tuple[int, Sequence[int], int]]
|
||||||
|
|
||||||
|
|
||||||
|
class DecimalEncoder(TypeEncoder):
|
||||||
|
"""Converts Python :class:`decimal.Decimal` to BSON :class:`Decimal128`.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
opts = CodecOptions(type_registry=TypeRegistry([DecimalEncoder()]))
|
||||||
|
bson.encode({"d": decimal.Decimal('1.0')}, codec_options=opts)
|
||||||
|
|
||||||
|
.. versionadded:: 4.15
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def python_type(self) -> Type[Decimal]:
|
||||||
|
return Decimal
|
||||||
|
|
||||||
|
def transform_python(self, value: Any) -> Decimal128:
|
||||||
|
return Decimal128(value)
|
||||||
|
|
||||||
|
|
||||||
|
class DecimalDecoder(TypeDecoder):
|
||||||
|
"""Converts BSON :class:`Decimal128` to Python :class:`decimal.Decimal`.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
opts = CodecOptions(type_registry=TypeRegistry([DecimalDecoder()]))
|
||||||
|
bson.decode(data, codec_options=opts)
|
||||||
|
|
||||||
|
.. versionadded:: 4.15
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bson_type(self) -> Type[Decimal128]:
|
||||||
|
return Decimal128
|
||||||
|
|
||||||
|
def transform_bson(self, value: Any) -> decimal.Decimal:
|
||||||
|
return value.to_decimal()
|
||||||
|
|
||||||
|
|
||||||
def create_decimal128_context() -> decimal.Context:
|
def create_decimal128_context() -> decimal.Context:
|
||||||
"""Returns an instance of :class:`decimal.Context` appropriate
|
"""Returns an instance of :class:`decimal.Context` appropriate
|
||||||
for working with IEEE-754 128-bit decimal floating point values.
|
for working with IEEE-754 128-bit decimal floating point values.
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
"""Exceptions raised by the BSON package."""
|
"""Exceptions raised by the BSON package."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
class BSONError(Exception):
|
class BSONError(Exception):
|
||||||
"""Base class for all BSON exceptions."""
|
"""Base class for all BSON exceptions."""
|
||||||
@ -31,6 +33,17 @@ class InvalidStringData(BSONError):
|
|||||||
class InvalidDocument(BSONError):
|
class InvalidDocument(BSONError):
|
||||||
"""Raised when trying to create a BSON object from an invalid document."""
|
"""Raised when trying to create a BSON object from an invalid document."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, document: Optional[Any] = None) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self._document = document
|
||||||
|
|
||||||
|
@property
|
||||||
|
def document(self) -> Any:
|
||||||
|
"""The invalid document that caused the error.
|
||||||
|
|
||||||
|
..versionadded:: 4.16"""
|
||||||
|
return self._document
|
||||||
|
|
||||||
|
|
||||||
class InvalidId(BSONError):
|
class InvalidId(BSONError):
|
||||||
"""Raised when trying to create an ObjectId from invalid data."""
|
"""Raised when trying to create an ObjectId from invalid data."""
|
||||||
|
|||||||
@ -281,7 +281,7 @@ class JSONOptions(_BASE_CLASS):
|
|||||||
return DatetimeMS objects when the underlying datetime is
|
return DatetimeMS objects when the underlying datetime is
|
||||||
out-of-range and 'datetime_clamp' to clamp to the minimum and
|
out-of-range and 'datetime_clamp' to clamp to the minimum and
|
||||||
maximum possible datetimes. Defaults to 'datetime'. See
|
maximum possible datetimes. Defaults to 'datetime'. See
|
||||||
:ref:`handling-out-of-range-datetimes` for details.
|
`handling out of range datetimes <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes>`_ for details.
|
||||||
:param args: arguments to :class:`~bson.codec_options.CodecOptions`
|
:param args: arguments to :class:`~bson.codec_options.CodecOptions`
|
||||||
:param kwargs: arguments to :class:`~bson.codec_options.CodecOptions`
|
:param kwargs: arguments to :class:`~bson.codec_options.CodecOptions`
|
||||||
|
|
||||||
@ -382,19 +382,6 @@ 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:
|
def with_options(self, **kwargs: Any) -> JSONOptions:
|
||||||
"""
|
"""
|
||||||
Make a copy of this JSONOptions, overriding some options::
|
Make a copy of this JSONOptions, overriding some options::
|
||||||
@ -408,7 +395,7 @@ class JSONOptions(_BASE_CLASS):
|
|||||||
|
|
||||||
.. versionadded:: 3.12
|
.. versionadded:: 3.12
|
||||||
"""
|
"""
|
||||||
opts = self._options_dict()
|
opts = self._asdict()
|
||||||
for opt in ("strict_number_long", "datetime_representation", "strict_uuid", "json_mode"):
|
for opt in ("strict_number_long", "datetime_representation", "strict_uuid", "json_mode"):
|
||||||
opts[opt] = kwargs.get(opt, getattr(self, opt))
|
opts[opt] = kwargs.get(opt, getattr(self, opt))
|
||||||
opts.update(kwargs)
|
opts.update(kwargs)
|
||||||
@ -844,7 +831,7 @@ def _encode_binary(data: bytes, subtype: int, json_options: JSONOptions) -> Any:
|
|||||||
return {"$binary": {"base64": base64.b64encode(data).decode(), "subType": "%02x" % subtype}}
|
return {"$binary": {"base64": base64.b64encode(data).decode(), "subType": "%02x" % subtype}}
|
||||||
|
|
||||||
|
|
||||||
def _encode_datetimems(obj: Any, json_options: JSONOptions) -> dict:
|
def _encode_datetimems(obj: Any, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
if (
|
if (
|
||||||
json_options.datetime_representation == DatetimeRepresentation.ISO8601
|
json_options.datetime_representation == DatetimeRepresentation.ISO8601
|
||||||
and 0 <= int(obj) <= _MAX_UTC_MS
|
and 0 <= int(obj) <= _MAX_UTC_MS
|
||||||
@ -855,7 +842,7 @@ def _encode_datetimems(obj: Any, json_options: JSONOptions) -> dict:
|
|||||||
return {"$date": {"$numberLong": str(int(obj))}}
|
return {"$date": {"$numberLong": str(int(obj))}}
|
||||||
|
|
||||||
|
|
||||||
def _encode_code(obj: Code, json_options: JSONOptions) -> dict:
|
def _encode_code(obj: Code, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
if obj.scope is None:
|
if obj.scope is None:
|
||||||
return {"$code": str(obj)}
|
return {"$code": str(obj)}
|
||||||
else:
|
else:
|
||||||
@ -873,7 +860,7 @@ def _encode_noop(obj: Any, dummy0: Any) -> Any:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def _encode_regex(obj: Any, json_options: JSONOptions) -> dict:
|
def _encode_regex(obj: Any, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
flags = ""
|
flags = ""
|
||||||
if obj.flags & re.IGNORECASE:
|
if obj.flags & re.IGNORECASE:
|
||||||
flags += "i"
|
flags += "i"
|
||||||
@ -918,7 +905,7 @@ def _encode_float(obj: float, json_options: JSONOptions) -> Any:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def _encode_datetime(obj: datetime.datetime, json_options: JSONOptions) -> dict:
|
def _encode_datetime(obj: datetime.datetime, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
if json_options.datetime_representation == DatetimeRepresentation.ISO8601:
|
if json_options.datetime_representation == DatetimeRepresentation.ISO8601:
|
||||||
if not obj.tzinfo:
|
if not obj.tzinfo:
|
||||||
obj = obj.replace(tzinfo=utc)
|
obj = obj.replace(tzinfo=utc)
|
||||||
@ -941,15 +928,15 @@ def _encode_datetime(obj: datetime.datetime, json_options: JSONOptions) -> dict:
|
|||||||
return {"$date": {"$numberLong": str(millis)}}
|
return {"$date": {"$numberLong": str(millis)}}
|
||||||
|
|
||||||
|
|
||||||
def _encode_bytes(obj: bytes, json_options: JSONOptions) -> dict:
|
def _encode_bytes(obj: bytes, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
return _encode_binary(obj, 0, json_options)
|
return _encode_binary(obj, 0, json_options)
|
||||||
|
|
||||||
|
|
||||||
def _encode_binary_obj(obj: Binary, json_options: JSONOptions) -> dict:
|
def _encode_binary_obj(obj: Binary, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
return _encode_binary(obj, obj.subtype, json_options)
|
return _encode_binary(obj, obj.subtype, json_options)
|
||||||
|
|
||||||
|
|
||||||
def _encode_uuid(obj: uuid.UUID, json_options: JSONOptions) -> dict:
|
def _encode_uuid(obj: uuid.UUID, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
if json_options.strict_uuid:
|
if json_options.strict_uuid:
|
||||||
binval = Binary.from_uuid(obj, uuid_representation=json_options.uuid_representation)
|
binval = Binary.from_uuid(obj, uuid_representation=json_options.uuid_representation)
|
||||||
return _encode_binary(binval, binval.subtype, json_options)
|
return _encode_binary(binval, binval.subtype, json_options)
|
||||||
@ -957,27 +944,27 @@ def _encode_uuid(obj: uuid.UUID, json_options: JSONOptions) -> dict:
|
|||||||
return {"$uuid": obj.hex}
|
return {"$uuid": obj.hex}
|
||||||
|
|
||||||
|
|
||||||
def _encode_objectid(obj: ObjectId, dummy0: Any) -> dict:
|
def _encode_objectid(obj: ObjectId, dummy0: Any) -> dict: # type: ignore[type-arg]
|
||||||
return {"$oid": str(obj)}
|
return {"$oid": str(obj)}
|
||||||
|
|
||||||
|
|
||||||
def _encode_timestamp(obj: Timestamp, dummy0: Any) -> dict:
|
def _encode_timestamp(obj: Timestamp, dummy0: Any) -> dict: # type: ignore[type-arg]
|
||||||
return {"$timestamp": {"t": obj.time, "i": obj.inc}}
|
return {"$timestamp": {"t": obj.time, "i": obj.inc}}
|
||||||
|
|
||||||
|
|
||||||
def _encode_decimal128(obj: Timestamp, dummy0: Any) -> dict:
|
def _encode_decimal128(obj: Timestamp, dummy0: Any) -> dict: # type: ignore[type-arg]
|
||||||
return {"$numberDecimal": str(obj)}
|
return {"$numberDecimal": str(obj)}
|
||||||
|
|
||||||
|
|
||||||
def _encode_dbref(obj: DBRef, json_options: JSONOptions) -> dict:
|
def _encode_dbref(obj: DBRef, json_options: JSONOptions) -> dict: # type: ignore[type-arg]
|
||||||
return _json_convert(obj.as_doc(), json_options=json_options)
|
return _json_convert(obj.as_doc(), json_options=json_options)
|
||||||
|
|
||||||
|
|
||||||
def _encode_minkey(dummy0: Any, dummy1: Any) -> dict:
|
def _encode_minkey(dummy0: Any, dummy1: Any) -> dict: # type: ignore[type-arg]
|
||||||
return {"$minKey": 1}
|
return {"$minKey": 1}
|
||||||
|
|
||||||
|
|
||||||
def _encode_maxkey(dummy0: Any, dummy1: Any) -> dict:
|
def _encode_maxkey(dummy0: Any, dummy1: Any) -> dict: # type: ignore[type-arg]
|
||||||
return {"$maxKey": 1}
|
return {"$maxKey": 1}
|
||||||
|
|
||||||
|
|
||||||
@ -985,7 +972,7 @@ def _encode_maxkey(dummy0: Any, dummy1: Any) -> dict:
|
|||||||
# Each encoder function's signature is:
|
# Each encoder function's signature is:
|
||||||
# - obj: a Python data type, e.g. a Python int for _encode_int
|
# - obj: a Python data type, e.g. a Python int for _encode_int
|
||||||
# - json_options: a JSONOptions
|
# - json_options: a JSONOptions
|
||||||
_ENCODERS: dict[Type, Callable[[Any, JSONOptions], Any]] = {
|
_ENCODERS: dict[Type, Callable[[Any, JSONOptions], Any]] = { # type: ignore[type-arg]
|
||||||
bool: _encode_noop,
|
bool: _encode_noop,
|
||||||
bytes: _encode_bytes,
|
bytes: _encode_bytes,
|
||||||
datetime.datetime: _encode_datetime,
|
datetime.datetime: _encode_datetime,
|
||||||
@ -1056,7 +1043,7 @@ def _get_datetime_size(obj: datetime.datetime) -> int:
|
|||||||
return 5 + len(str(obj.time()))
|
return 5 + len(str(obj.time()))
|
||||||
|
|
||||||
|
|
||||||
def _get_regex_size(obj: Regex) -> int:
|
def _get_regex_size(obj: Regex) -> int: # type: ignore[type-arg]
|
||||||
return 18 + len(obj.pattern)
|
return 18 + len(obj.pattern)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
"""Tools for working with MongoDB ObjectIds."""
|
"""Tools for working with MongoDB ObjectIds."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import binascii
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
@ -98,11 +97,27 @@ class ObjectId:
|
|||||||
objectid.rst>`_.
|
objectid.rst>`_.
|
||||||
"""
|
"""
|
||||||
if oid is None:
|
if oid is None:
|
||||||
self.__generate()
|
# 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]
|
||||||
elif isinstance(oid, bytes) and len(oid) == 12:
|
elif isinstance(oid, bytes) and len(oid) == 12:
|
||||||
self.__id = oid
|
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:
|
else:
|
||||||
self.__validate(oid)
|
raise TypeError(f"id must be an instance of (bytes, str, ObjectId), not {type(oid)}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_datetime(cls: Type[ObjectId], generation_time: datetime.datetime) -> ObjectId:
|
def from_datetime(cls: Type[ObjectId], generation_time: datetime.datetime) -> ObjectId:
|
||||||
@ -163,37 +178,6 @@ class ObjectId:
|
|||||||
cls.__random = _random_bytes()
|
cls.__random = _random_bytes()
|
||||||
return cls.__random
|
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
|
@property
|
||||||
def binary(self) -> bytes:
|
def binary(self) -> bytes:
|
||||||
"""12-byte binary representation of this ObjectId."""
|
"""12-byte binary representation of this ObjectId."""
|
||||||
@ -234,7 +218,7 @@ class ObjectId:
|
|||||||
self.__id = oid
|
self.__id = oid
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return binascii.hexlify(self.__id).decode()
|
return self.__id.hex()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"ObjectId('{self!s}')"
|
return f"ObjectId('{self!s}')"
|
||||||
|
|||||||
@ -60,7 +60,9 @@ from bson.codec_options import DEFAULT_CODEC_OPTIONS as DEFAULT
|
|||||||
|
|
||||||
|
|
||||||
def _inflate_bson(
|
def _inflate_bson(
|
||||||
bson_bytes: bytes, codec_options: CodecOptions[RawBSONDocument], raw_array: bool = False
|
bson_bytes: bytes | memoryview,
|
||||||
|
codec_options: CodecOptions[RawBSONDocument],
|
||||||
|
raw_array: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Inflates the top level fields of a BSON document.
|
"""Inflates the top level fields of a BSON document.
|
||||||
|
|
||||||
@ -85,7 +87,9 @@ class RawBSONDocument(Mapping[str, Any]):
|
|||||||
__codec_options: CodecOptions[RawBSONDocument]
|
__codec_options: CodecOptions[RawBSONDocument]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, bson_bytes: bytes, codec_options: Optional[CodecOptions[RawBSONDocument]] = None
|
self,
|
||||||
|
bson_bytes: bytes | memoryview,
|
||||||
|
codec_options: Optional[CodecOptions[RawBSONDocument]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a new :class:`RawBSONDocument`
|
"""Create a new :class:`RawBSONDocument`
|
||||||
|
|
||||||
@ -135,7 +139,7 @@ class RawBSONDocument(Mapping[str, Any]):
|
|||||||
_get_object_size(bson_bytes, 0, len(bson_bytes))
|
_get_object_size(bson_bytes, 0, len(bson_bytes))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def raw(self) -> bytes:
|
def raw(self) -> bytes | memoryview:
|
||||||
"""The raw BSON bytes composing this document."""
|
"""The raw BSON bytes composing this document."""
|
||||||
return self.__raw
|
return self.__raw
|
||||||
|
|
||||||
@ -153,7 +157,7 @@ class RawBSONDocument(Mapping[str, Any]):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _inflate_bson(
|
def _inflate_bson(
|
||||||
bson_bytes: bytes, codec_options: CodecOptions[RawBSONDocument]
|
bson_bytes: bytes | memoryview, codec_options: CodecOptions[RawBSONDocument]
|
||||||
) -> Mapping[str, Any]:
|
) -> Mapping[str, Any]:
|
||||||
return _inflate_bson(bson_bytes, codec_options)
|
return _inflate_bson(bson_bytes, codec_options)
|
||||||
|
|
||||||
@ -180,7 +184,7 @@ class _RawArrayBSONDocument(RawBSONDocument):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _inflate_bson(
|
def _inflate_bson(
|
||||||
bson_bytes: bytes, codec_options: CodecOptions[RawBSONDocument]
|
bson_bytes: bytes | memoryview, codec_options: CodecOptions[RawBSONDocument]
|
||||||
) -> Mapping[str, Any]:
|
) -> Mapping[str, Any]:
|
||||||
return _inflate_bson(bson_bytes, codec_options, raw_array=True)
|
return _inflate_bson(bson_bytes, codec_options, raw_array=True)
|
||||||
|
|
||||||
|
|||||||
18
bson/son.py
18
bson/son.py
@ -22,6 +22,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
|
import warnings
|
||||||
from collections.abc import Mapping as _Mapping
|
from collections.abc import Mapping as _Mapping
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
@ -99,13 +100,28 @@ class SON(Dict[_Key, _Value]):
|
|||||||
yield from self.__keys
|
yield from self.__keys
|
||||||
|
|
||||||
def has_key(self, key: _Key) -> bool:
|
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
|
return key in self.__keys
|
||||||
|
|
||||||
def iterkeys(self) -> Iterator[_Key]:
|
def iterkeys(self) -> Iterator[_Key]:
|
||||||
|
warnings.warn(
|
||||||
|
"SON.iterkeys() is deprecated, use the keys() method instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
return self.__iter__()
|
return self.__iter__()
|
||||||
|
|
||||||
# fourth level uses definitions from lower levels
|
# fourth level uses definitions from lower levels
|
||||||
def itervalues(self) -> Iterator[_Value]:
|
def itervalues(self) -> Iterator[_Value]:
|
||||||
|
warnings.warn(
|
||||||
|
"SON.itervalues() is deprecated, use the values() method instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
for _, v in self.items():
|
for _, v in self.items():
|
||||||
yield v
|
yield v
|
||||||
|
|
||||||
@ -143,7 +159,7 @@ class SON(Dict[_Key, _Value]):
|
|||||||
del self[k]
|
del self[k]
|
||||||
return (k, v)
|
return (k, v)
|
||||||
|
|
||||||
def update(self, other: Optional[Any] = None, **kwargs: _Value) -> None: # type: ignore[override]
|
def update(self, other: Optional[Any] = None, **kwargs: _Value) -> None:
|
||||||
# Make progressively weaker assumptions about "other"
|
# Make progressively weaker assumptions about "other"
|
||||||
if other is None:
|
if other is None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -28,4 +28,4 @@ if TYPE_CHECKING:
|
|||||||
_DocumentOut = Union[MutableMapping[str, Any], "RawBSONDocument"]
|
_DocumentOut = Union[MutableMapping[str, Any], "RawBSONDocument"]
|
||||||
_DocumentType = TypeVar("_DocumentType", bound=Mapping[str, Any])
|
_DocumentType = TypeVar("_DocumentType", bound=Mapping[str, Any])
|
||||||
_DocumentTypeArg = TypeVar("_DocumentTypeArg", bound=Mapping[str, Any])
|
_DocumentTypeArg = TypeVar("_DocumentTypeArg", bound=Mapping[str, Any])
|
||||||
_ReadableBuffer = Union[bytes, memoryview, "mmap", "array"]
|
_ReadableBuffer = Union[bytes, memoryview, bytearray, "mmap", "array"] # type: ignore[type-arg]
|
||||||
|
|||||||
@ -5,3 +5,4 @@
|
|||||||
.. automodule:: pymongo.asynchronous.command_cursor
|
.. automodule:: pymongo.asynchronous.command_cursor
|
||||||
:synopsis: Tools for iterating over MongoDB command results
|
:synopsis: Tools for iterating over MongoDB command results
|
||||||
:members:
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
.. 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)
|
.. 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:
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
|
|
||||||
.. describe:: c[index]
|
.. describe:: c[index]
|
||||||
|
|
||||||
|
|||||||
@ -4,3 +4,4 @@
|
|||||||
.. automodule:: pymongo.command_cursor
|
.. automodule:: pymongo.command_cursor
|
||||||
:synopsis: Tools for iterating over MongoDB command results
|
:synopsis: Tools for iterating over MongoDB command results
|
||||||
:members:
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
.. 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)
|
.. 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:
|
:members:
|
||||||
|
:inherited-members:
|
||||||
|
|
||||||
.. describe:: c[index]
|
.. describe:: c[index]
|
||||||
|
|
||||||
|
|||||||
@ -1,425 +0,0 @@
|
|||||||
Async Tutorial
|
|
||||||
==============
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
from pymongo import AsyncMongoClient
|
|
||||||
|
|
||||||
client = AsyncMongoClient()
|
|
||||||
await client.drop_database("test-database")
|
|
||||||
|
|
||||||
This tutorial is intended as an introduction to working with
|
|
||||||
**MongoDB** and **PyMongo** using the asynchronous API.
|
|
||||||
|
|
||||||
Prerequisites
|
|
||||||
-------------
|
|
||||||
Before we start, make sure that you have the **PyMongo** distribution
|
|
||||||
:doc:`installed <installation>`. In the Python shell, the following
|
|
||||||
should run without raising an exception:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> import pymongo
|
|
||||||
|
|
||||||
This tutorial also assumes that a MongoDB instance is running on the
|
|
||||||
default host and port. Assuming you have `downloaded and installed
|
|
||||||
<https://www.mongodb.com/docs/manual/installation/>`_ MongoDB, you
|
|
||||||
can start it like so:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ mongod
|
|
||||||
|
|
||||||
Making a Connection with AsyncMongoClient
|
|
||||||
-----------------------------------------
|
|
||||||
The first step when working with **PyMongo** is to create a
|
|
||||||
:class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` to the running **mongod**
|
|
||||||
instance. Doing so is easy:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> from pymongo import AsyncMongoClient
|
|
||||||
>>> client = AsyncMongoClient()
|
|
||||||
|
|
||||||
The above code will connect on the default host and port. We can also
|
|
||||||
specify the host and port explicitly, as follows:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> client = AsyncMongoClient("localhost", 27017)
|
|
||||||
|
|
||||||
Or use the MongoDB URI format:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> client = AsyncMongoClient("mongodb://localhost:27017/")
|
|
||||||
|
|
||||||
By default, :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` only connects to the database on its first operation.
|
|
||||||
To explicitly connect before performing an operation, use :meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.aconnect`:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> client = await AsyncMongoClient().aconnect()
|
|
||||||
|
|
||||||
Getting a Database
|
|
||||||
------------------
|
|
||||||
A single instance of MongoDB can support multiple independent
|
|
||||||
`databases <https://www.mongodb.com/docs/manual/core/databases-and-collections>`_. When
|
|
||||||
working with PyMongo you access databases using attribute style access
|
|
||||||
on :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` instances:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> db = client.test_database
|
|
||||||
|
|
||||||
If your database name is such that using attribute style access won't
|
|
||||||
work (like ``test-database``), you can use dictionary style access
|
|
||||||
instead:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> db = client["test-database"]
|
|
||||||
|
|
||||||
Getting a Collection
|
|
||||||
--------------------
|
|
||||||
A `collection <https://www.mongodb.com/docs/manual/core/databases-and-collections>`_ is a
|
|
||||||
group of documents stored in MongoDB, and can be thought of as roughly
|
|
||||||
the equivalent of a table in a relational database. Getting a
|
|
||||||
collection in PyMongo works the same as getting a database:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> collection = db.test_collection
|
|
||||||
|
|
||||||
or (using dictionary style access):
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> collection = db["test-collection"]
|
|
||||||
|
|
||||||
An important note about collections (and databases) in MongoDB is that
|
|
||||||
they are created lazily - none of the above commands have actually
|
|
||||||
performed any operations on the MongoDB server. Collections and
|
|
||||||
databases are created when the first document is inserted into them.
|
|
||||||
|
|
||||||
Documents
|
|
||||||
---------
|
|
||||||
Data in MongoDB is represented (and stored) using JSON-style
|
|
||||||
documents. In PyMongo we use dictionaries to represent documents. As
|
|
||||||
an example, the following dictionary might be used to represent a blog
|
|
||||||
post:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> import datetime
|
|
||||||
>>> post = {
|
|
||||||
... "author": "Mike",
|
|
||||||
... "text": "My first blog post!",
|
|
||||||
... "tags": ["mongodb", "python", "pymongo"],
|
|
||||||
... "date": datetime.datetime.now(tz=datetime.timezone.utc),
|
|
||||||
... }
|
|
||||||
|
|
||||||
Note that documents can contain native Python types (like
|
|
||||||
:class:`datetime.datetime` instances) which will be automatically
|
|
||||||
converted to and from the appropriate `BSON
|
|
||||||
<https://bsonspec.org/>`_ types.
|
|
||||||
|
|
||||||
Inserting a Document
|
|
||||||
--------------------
|
|
||||||
To insert a document into a collection we can use the
|
|
||||||
:meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_one` method:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> posts = db.posts
|
|
||||||
>>> post_id = (await posts.insert_one(post)).inserted_id
|
|
||||||
>>> post_id
|
|
||||||
ObjectId('...')
|
|
||||||
|
|
||||||
When a document is inserted a special key, ``"_id"``, is automatically
|
|
||||||
added if the document doesn't already contain an ``"_id"`` key. The value
|
|
||||||
of ``"_id"`` must be unique across the
|
|
||||||
collection. :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_one` returns an
|
|
||||||
instance of :class:`~pymongo.results.InsertOneResult`. For more information
|
|
||||||
on ``"_id"``, see the `documentation on _id
|
|
||||||
<https://www.mongodb.com/docs/manual/reference/method/ObjectId/>`_.
|
|
||||||
|
|
||||||
After inserting the first document, the *posts* collection has
|
|
||||||
actually been created on the server. We can verify this by listing all
|
|
||||||
of the collections in our database:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> await db.list_collection_names()
|
|
||||||
['posts']
|
|
||||||
|
|
||||||
Getting a Single Document With :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`
|
|
||||||
------------------------------------------------------------------------------------------------
|
|
||||||
The most basic type of query that can be performed in MongoDB is
|
|
||||||
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`. This method returns a
|
|
||||||
single document matching a query (or ``None`` if there are no
|
|
||||||
matches). It is useful when you know there is only one matching
|
|
||||||
document, or are only interested in the first match. Here we use
|
|
||||||
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one` to get the first
|
|
||||||
document from the posts collection:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> import pprint
|
|
||||||
>>> pprint.pprint(await posts.find_one())
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['mongodb', 'python', 'pymongo'],
|
|
||||||
'text': 'My first blog post!'}
|
|
||||||
|
|
||||||
The result is a dictionary matching the one that we inserted previously.
|
|
||||||
|
|
||||||
.. note:: The returned document contains an ``"_id"``, which was
|
|
||||||
automatically added on insert.
|
|
||||||
|
|
||||||
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one` also supports querying
|
|
||||||
on specific elements that the resulting document must match. To limit
|
|
||||||
our results to a document with author "Mike" we do:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> pprint.pprint(await posts.find_one({"author": "Mike"}))
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['mongodb', 'python', 'pymongo'],
|
|
||||||
'text': 'My first blog post!'}
|
|
||||||
|
|
||||||
If we try with a different author, like "Eliot", we'll get no result:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> await posts.find_one({"author": "Eliot"})
|
|
||||||
>>>
|
|
||||||
|
|
||||||
.. _async-querying-by-objectid:
|
|
||||||
|
|
||||||
Querying By ObjectId
|
|
||||||
--------------------
|
|
||||||
We can also find a post by its ``_id``, which in our example is an ObjectId:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> post_id
|
|
||||||
ObjectId(...)
|
|
||||||
>>> pprint.pprint(await posts.find_one({"_id": post_id}))
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['mongodb', 'python', 'pymongo'],
|
|
||||||
'text': 'My first blog post!'}
|
|
||||||
|
|
||||||
Note that an ObjectId is not the same as its string representation:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> post_id_as_str = str(post_id)
|
|
||||||
>>> await posts.find_one({"_id": post_id_as_str}) # No result
|
|
||||||
>>>
|
|
||||||
|
|
||||||
A common task in web applications is to get an ObjectId from the
|
|
||||||
request URL and find the matching document. It's necessary in this
|
|
||||||
case to **convert the ObjectId from a string** before passing it to
|
|
||||||
``find_one``::
|
|
||||||
|
|
||||||
from bson.objectid import ObjectId
|
|
||||||
|
|
||||||
# The web framework gets post_id from the URL and passes it as a string
|
|
||||||
async def get(post_id):
|
|
||||||
# Convert from string to ObjectId:
|
|
||||||
document = await client.db.collection.find_one({'_id': ObjectId(post_id)})
|
|
||||||
|
|
||||||
.. seealso:: :ref:`web-application-querying-by-objectid`
|
|
||||||
|
|
||||||
Bulk Inserts
|
|
||||||
------------
|
|
||||||
In order to make querying a little more interesting, let's insert a
|
|
||||||
few more documents. In addition to inserting a single document, we can
|
|
||||||
also perform *bulk insert* operations, by passing a list as the
|
|
||||||
first argument to :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_many`.
|
|
||||||
This will insert each document in the list, sending only a single
|
|
||||||
command to the server:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> new_posts = [
|
|
||||||
... {
|
|
||||||
... "author": "Mike",
|
|
||||||
... "text": "Another post!",
|
|
||||||
... "tags": ["bulk", "insert"],
|
|
||||||
... "date": datetime.datetime(2009, 11, 12, 11, 14),
|
|
||||||
... },
|
|
||||||
... {
|
|
||||||
... "author": "Eliot",
|
|
||||||
... "title": "MongoDB is fun",
|
|
||||||
... "text": "and pretty easy too!",
|
|
||||||
... "date": datetime.datetime(2009, 11, 10, 10, 45),
|
|
||||||
... },
|
|
||||||
... ]
|
|
||||||
>>> result = await posts.insert_many(new_posts)
|
|
||||||
>>> result.inserted_ids
|
|
||||||
[ObjectId('...'), ObjectId('...')]
|
|
||||||
|
|
||||||
There are a couple of interesting things to note about this example:
|
|
||||||
|
|
||||||
- The result from :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_many` now
|
|
||||||
returns two :class:`~bson.objectid.ObjectId` instances, one for
|
|
||||||
each inserted document.
|
|
||||||
- ``new_posts[1]`` has a different "shape" than the other posts -
|
|
||||||
there is no ``"tags"`` field and we've added a new field,
|
|
||||||
``"title"``. This is what we mean when we say that MongoDB is
|
|
||||||
*schema-free*.
|
|
||||||
|
|
||||||
Querying for More Than One Document
|
|
||||||
-----------------------------------
|
|
||||||
To get more than a single document as the result of a query we use the
|
|
||||||
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find`
|
|
||||||
method. :meth:`~pymongo.asynchronous.collection.AsyncCollection.find` returns a
|
|
||||||
:class:`~pymongo.asynchronous.cursor.AsyncCursor` instance, which allows us to iterate
|
|
||||||
over all matching documents. For example, we can iterate over every
|
|
||||||
document in the ``posts`` collection:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> async for post in posts.find():
|
|
||||||
... pprint.pprint(post)
|
|
||||||
...
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['mongodb', 'python', 'pymongo'],
|
|
||||||
'text': 'My first blog post!'}
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['bulk', 'insert'],
|
|
||||||
'text': 'Another post!'}
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Eliot',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'text': 'and pretty easy too!',
|
|
||||||
'title': 'MongoDB is fun'}
|
|
||||||
|
|
||||||
Just like we did with :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`,
|
|
||||||
we can pass a document to :meth:`~pymongo.asynchronous.collection.AsyncCollection.find`
|
|
||||||
to limit the returned results. Here, we get only those documents whose
|
|
||||||
author is "Mike":
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> async for post in posts.find({"author": "Mike"}):
|
|
||||||
... pprint.pprint(post)
|
|
||||||
...
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['mongodb', 'python', 'pymongo'],
|
|
||||||
'text': 'My first blog post!'}
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['bulk', 'insert'],
|
|
||||||
'text': 'Another post!'}
|
|
||||||
|
|
||||||
Counting
|
|
||||||
--------
|
|
||||||
If we just want to know how many documents match a query we can
|
|
||||||
perform a :meth:`~pymongo.asynchronous.collection.AsyncCollection.count_documents` operation
|
|
||||||
instead of a full query. We can get a count of all of the documents
|
|
||||||
in a collection:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> await posts.count_documents({})
|
|
||||||
3
|
|
||||||
|
|
||||||
or just of those documents that match a specific query:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> await posts.count_documents({"author": "Mike"})
|
|
||||||
2
|
|
||||||
|
|
||||||
Range Queries
|
|
||||||
-------------
|
|
||||||
MongoDB supports many different types of `advanced queries
|
|
||||||
<https://www.mongodb.com/docs/manual/reference/operator/>`_. As an
|
|
||||||
example, lets perform a query where we limit results to posts older
|
|
||||||
than a certain date, but also sort the results by author:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> d = datetime.datetime(2009, 11, 12, 12)
|
|
||||||
>>> async for post in posts.find({"date": {"$lt": d}}).sort("author"):
|
|
||||||
... pprint.pprint(post)
|
|
||||||
...
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Eliot',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'text': 'and pretty easy too!',
|
|
||||||
'title': 'MongoDB is fun'}
|
|
||||||
{'_id': ObjectId('...'),
|
|
||||||
'author': 'Mike',
|
|
||||||
'date': datetime.datetime(...),
|
|
||||||
'tags': ['bulk', 'insert'],
|
|
||||||
'text': 'Another post!'}
|
|
||||||
|
|
||||||
Here we use the special ``"$lt"`` operator to do a range query, and
|
|
||||||
also call :meth:`~pymongo.asynchronous.cursor.AsyncCursor.sort` to sort the results
|
|
||||||
by author.
|
|
||||||
|
|
||||||
Indexing
|
|
||||||
--------
|
|
||||||
|
|
||||||
Adding indexes can help accelerate certain queries and can also add additional
|
|
||||||
functionality to querying and storing documents. In this example, we'll
|
|
||||||
demonstrate how to create a `unique index
|
|
||||||
<https://mongodb.com/docs/manual/core/index-unique/>`_ on a key that rejects
|
|
||||||
documents whose value for that key already exists in the index.
|
|
||||||
|
|
||||||
First, we'll need to create the index:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> result = await db.profiles.create_index([("user_id", pymongo.ASCENDING)], unique=True)
|
|
||||||
>>> sorted(list(await db.profiles.index_information()))
|
|
||||||
['_id_', 'user_id_1']
|
|
||||||
|
|
||||||
Notice that we have two indexes now: one is the index on ``_id`` that MongoDB
|
|
||||||
creates automatically, and the other is the index on ``user_id`` we just
|
|
||||||
created.
|
|
||||||
|
|
||||||
Now let's set up some user profiles:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> user_profiles = [{"user_id": 211, "name": "Luke"}, {"user_id": 212, "name": "Ziltoid"}]
|
|
||||||
>>> result = await db.profiles.insert_many(user_profiles)
|
|
||||||
|
|
||||||
The index prevents us from inserting a document whose ``user_id`` is already in
|
|
||||||
the collection:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> new_profile = {"user_id": 213, "name": "Drew"}
|
|
||||||
>>> duplicate_profile = {"user_id": 212, "name": "Tommy"}
|
|
||||||
>>> result = await db.profiles.insert_one(new_profile) # This is fine.
|
|
||||||
>>> result = await db.profiles.insert_one(duplicate_profile)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
DuplicateKeyError: E11000 duplicate key error index: test_database.profiles.$user_id_1 dup key: { : 212 }
|
|
||||||
|
|
||||||
.. seealso:: The MongoDB documentation on `indexes <https://www.mongodb.com/docs/manual/indexes/>`_
|
|
||||||
|
|
||||||
Task Cancellation
|
|
||||||
-----------------
|
|
||||||
`Cancelling <https://docs.python.org/3/library/asyncio-task.html#task-cancellation>`_ an asyncio Task
|
|
||||||
that is running a PyMongo operation is treated as a fatal interrupt. Any connections, cursors, and transactions
|
|
||||||
involved in a cancelled Task will be safely closed and cleaned up as part of the cancellation. If those resources are
|
|
||||||
also used elsewhere, attempting to utilize them after the cancellation will result in an error.
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
Using PyMongo with MongoDB Atlas
|
|
||||||
================================
|
|
||||||
|
|
||||||
`Atlas <https://www.mongodb.com/cloud>`_ is MongoDB, Inc.'s hosted MongoDB as a
|
|
||||||
service offering. To connect to Atlas, pass the connection string provided by
|
|
||||||
Atlas to :class:`~pymongo.mongo_client.MongoClient`::
|
|
||||||
|
|
||||||
client = pymongo.MongoClient(<Atlas connection string>)
|
|
||||||
|
|
||||||
Connections to Atlas require TLS/SSL.
|
|
||||||
|
|
||||||
.. warning:: Industry best practices recommend, and some regulations require,
|
|
||||||
the use of TLS 1.1 or newer. Though no application changes are required for
|
|
||||||
PyMongo to make use of the newest protocols, some operating systems or
|
|
||||||
versions may not provide an OpenSSL version new enough to support them.
|
|
||||||
|
|
||||||
Users of macOS older than 10.13 (High Sierra) will need to install Python
|
|
||||||
from `python.org`_, `homebrew`_, `macports`_, or another similar source.
|
|
||||||
|
|
||||||
Users of Linux or other non-macOS Unix can check their OpenSSL version like
|
|
||||||
this::
|
|
||||||
|
|
||||||
$ openssl version
|
|
||||||
|
|
||||||
If the version number is less than 1.0.1 support for TLS 1.1 or newer is not
|
|
||||||
available. Contact your operating system vendor for a solution or upgrade to
|
|
||||||
a newer distribution.
|
|
||||||
|
|
||||||
You can check your Python interpreter by installing the `requests`_ module
|
|
||||||
and executing the following command::
|
|
||||||
|
|
||||||
python -c "import requests; print(requests.get('https://www.howsmyssl.com/a/check', verify=False).json()['tls_version'])"
|
|
||||||
|
|
||||||
You should see "TLS 1.X" where X is >= 1.
|
|
||||||
|
|
||||||
You can read more about TLS versions and their security implications here:
|
|
||||||
|
|
||||||
`<https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html#only-support-strong-protocols>`_
|
|
||||||
|
|
||||||
.. _python.org: https://www.python.org/downloads/
|
|
||||||
.. _homebrew: https://brew.sh/
|
|
||||||
.. _macports: https://www.macports.org/
|
|
||||||
.. _requests: https://pypi.python.org/pypi/requests
|
|
||||||
@ -1,6 +1,229 @@
|
|||||||
Changelog
|
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)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
PyMongo 4.16 brings a number of changes including:
|
||||||
|
|
||||||
|
- 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>`_.
|
||||||
|
- 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)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Version 4.15.3 is a bug fix release.
|
||||||
|
|
||||||
|
- Fixed a memory leak when raising :class:`bson.errors.InvalidDocument` with C extensions.
|
||||||
|
- Fixed the return type of the :meth:`~pymongo.asynchronous.collection.AsyncCollection.distinct`,
|
||||||
|
:meth:`~pymongo.synchronous.collection.Collection.distinct`, :meth:`pymongo.asynchronous.cursor.AsyncCursor.distinct`,
|
||||||
|
and :meth:`pymongo.asynchronous.cursor.AsyncCursor.distinct` methods.
|
||||||
|
|
||||||
|
Issues Resolved
|
||||||
|
...............
|
||||||
|
|
||||||
|
See the `PyMongo 4.15.3 release notes in JIRA`_ for the list of resolved issues
|
||||||
|
in this release.
|
||||||
|
|
||||||
|
.. _PyMongo 4.15.3 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=47293
|
||||||
|
|
||||||
|
Changes in Version 4.15.2 (2025/10/01)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Version 4.15.2 is a bug fix release.
|
||||||
|
|
||||||
|
- Add wheels for Python 3.14 and 3.14t that were missing from 4.15.0 release. Drop the 3.13t wheel.
|
||||||
|
|
||||||
|
Issues Resolved
|
||||||
|
...............
|
||||||
|
|
||||||
|
See the `PyMongo 4.15.2 release notes in JIRA`_ for the list of resolved issues
|
||||||
|
in this release.
|
||||||
|
|
||||||
|
.. _PyMongo 4.15.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=47186
|
||||||
|
|
||||||
|
Changes in Version 4.15.1 (2025/09/16)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Version 4.15.1 is a bug fix release.
|
||||||
|
|
||||||
|
- Fixed a bug in :meth:`~pymongo.synchronous.encryption.ClientEncryption.encrypt`
|
||||||
|
and :meth:`~pymongo.asynchronous.encryption.AsyncClientEncryption.encrypt`
|
||||||
|
that would cause a ``TypeError`` when using ``pymongocrypt<1.16`` by passing
|
||||||
|
an unsupported ``type_opts`` parameter even if Queryable Encryption text
|
||||||
|
queries beta was not used.
|
||||||
|
|
||||||
|
- Fixed a bug in ``AsyncMongoClient`` that caused a ``ServerSelectionTimeoutError``
|
||||||
|
when used with ``uvicorn``, ``FastAPI``, or ``uvloop``.
|
||||||
|
|
||||||
|
Issues Resolved
|
||||||
|
...............
|
||||||
|
|
||||||
|
See the `PyMongo 4.15.1 release notes in JIRA`_ for the list of resolved issues
|
||||||
|
in this release.
|
||||||
|
|
||||||
|
.. _PyMongo 4.15.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=46486
|
||||||
|
|
||||||
|
Changes in Version 4.15.0 (2025/09/10)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
PyMongo 4.15 brings a number of changes including:
|
||||||
|
|
||||||
|
- Added :class:`~pymongo.encryption_options.TextOpts`,
|
||||||
|
:attr:`~pymongo.encryption.Algorithm.TEXTPREVIEW`,
|
||||||
|
:attr:`~pymongo.encryption.QueryType.PREFIXPREVIEW`,
|
||||||
|
:attr:`~pymongo.encryption.QueryType.SUFFIXPREVIEW`,
|
||||||
|
:attr:`~pymongo.encryption.QueryType.SUBSTRINGPREVIEW`,
|
||||||
|
as part of the experimental Queryable Encryption text queries beta.
|
||||||
|
``pymongocrypt>=1.16`` is required for text query support.
|
||||||
|
- Added :class:`bson.decimal128.DecimalEncoder` and
|
||||||
|
:class:`bson.decimal128.DecimalDecoder`
|
||||||
|
to support encoding and decoding of BSON Decimal128 values to
|
||||||
|
decimal.Decimal values using the TypeRegistry API.
|
||||||
|
- Added support for Windows ``arm64`` wheels.
|
||||||
|
|
||||||
|
Changes in Version 4.14.1 (2025/08/19)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Version 4.14.1 is a bug fix release.
|
||||||
|
|
||||||
|
- Fixed a bug in ``MongoClient.append_metadata()`` and
|
||||||
|
``AsyncMongoClient.append_metadata()``
|
||||||
|
that allowed duplicate ``DriverInfo.name`` to be appended to the metadata.
|
||||||
|
|
||||||
|
Issues Resolved
|
||||||
|
...............
|
||||||
|
|
||||||
|
See the `PyMongo 4.14.1 release notes in JIRA`_ for the list of resolved issues
|
||||||
|
in this release.
|
||||||
|
|
||||||
|
.. _PyMongo 4.14.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=45256
|
||||||
|
|
||||||
|
Changes in Version 4.14.0 (2025/08/06)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
.. warning:: PyMongo 4.14 drops support for MongoDB 4.0. PyMongo now supports
|
||||||
|
MongoDB 4.2+.
|
||||||
|
|
||||||
|
PyMongo 4.14 brings a number of changes including:
|
||||||
|
|
||||||
|
- Dropped support for MongoDB 4.0.
|
||||||
|
- Added preliminary support for Python 3.14 and 3.14 with free-threading. We do
|
||||||
|
not yet support the following with Python 3.14:
|
||||||
|
|
||||||
|
- Subinterpreters (``concurrent.interpreters``)
|
||||||
|
- Free-threading with Encryption
|
||||||
|
- mod_wsgi
|
||||||
|
|
||||||
|
- Removed experimental support for free-threading support in Python 3.13.
|
||||||
|
- Added :attr:`bson.codec_options.TypeRegistry.codecs` and
|
||||||
|
:attr:`bson.codec_options.TypeRegistry.fallback_encoder` properties
|
||||||
|
to allow users to directly access the type codecs and fallback encoder for a
|
||||||
|
given :class:`bson.codec_options.TypeRegistry`.
|
||||||
|
- Added
|
||||||
|
:meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.append_metadata` and
|
||||||
|
:meth:`pymongo.mongo_client.MongoClient.append_metadata` to allow instantiated
|
||||||
|
MongoClients to send client metadata on-demand
|
||||||
|
- Improved performance of selecting a server with the Primary selector.
|
||||||
|
- Introduces a minor breaking change. When encoding
|
||||||
|
:class:`bson.binary.BinaryVector`, a ``ValueError`` will be raised if the
|
||||||
|
'padding' metadata field is < 0 or > 7, or non-zero for any type other than
|
||||||
|
PACKED_BIT.
|
||||||
|
- Changed :meth:`~pymongo.uri_parser.parse_uri`'s ``options`` return value to be
|
||||||
|
type ``dict`` instead of ``_CaseInsensitiveDictionary``.
|
||||||
|
|
||||||
|
Issues Resolved
|
||||||
|
...............
|
||||||
|
|
||||||
|
See the `PyMongo 4.14 release notes in JIRA`_ for the list of resolved issues
|
||||||
|
in this release.
|
||||||
|
|
||||||
|
.. _PyMongo 4.14 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43041
|
||||||
|
|
||||||
|
Changes in Version 4.13.2 (2025/06/17)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Version 4.13.2 is a bug fix release.
|
||||||
|
|
||||||
|
- Fixed a bug where ``AsyncMongoClient`` would block the event loop while creating new connections,
|
||||||
|
potentially significantly increasing latency for ongoing operations.
|
||||||
|
|
||||||
|
Issues Resolved
|
||||||
|
...............
|
||||||
|
|
||||||
|
See the `PyMongo 4.13.2 release notes in JIRA`_ for the list of resolved issues
|
||||||
|
in this release.
|
||||||
|
|
||||||
|
.. _PyMongo 4.13.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43937
|
||||||
|
|
||||||
|
|
||||||
|
Changes in Version 4.13.1 (2025/06/10)
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Version 4.13.1 is a bug fix release.
|
||||||
|
|
||||||
|
- Fixed a bug that could raise ``ServerSelectionTimeoutError`` when using timeouts with ``AsyncMongoClient``.
|
||||||
|
- Fixed a bug that could raise ``NetworkTimeout`` errors on Windows.
|
||||||
|
|
||||||
|
Issues Resolved
|
||||||
|
...............
|
||||||
|
|
||||||
|
See the `PyMongo 4.13.1 release notes in JIRA`_ for the list of resolved issues
|
||||||
|
in this release.
|
||||||
|
|
||||||
|
.. _PyMongo 4.13.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43924
|
||||||
|
|
||||||
Changes in Version 4.13.0 (2025/05/14)
|
Changes in Version 4.13.0 (2025/05/14)
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
@ -11,6 +234,10 @@ PyMongo 4.13 brings a number of changes including:
|
|||||||
or the `migration guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_ for more information.
|
or the `migration guide <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/migration/>`_ for more information.
|
||||||
- Fixed a bug where :class:`pymongo.write_concern.WriteConcern` repr was not eval-able
|
- Fixed a bug where :class:`pymongo.write_concern.WriteConcern` repr was not eval-able
|
||||||
when using ``w="majority"``.
|
when using ``w="majority"``.
|
||||||
|
- When padding is set, ignored bits in a BSON BinaryVector of PACKED_BIT dtype should be set to zero.
|
||||||
|
When encoding, this is enforced and is a breaking change.
|
||||||
|
It is not yet enforced when decoding, so reading from the database will not fail, however a warning will be triggered.
|
||||||
|
From PyMongo 5.0, this rule will be enforced for both encoding and decoding.
|
||||||
|
|
||||||
Issues Resolved
|
Issues Resolved
|
||||||
...............
|
...............
|
||||||
@ -58,7 +285,7 @@ PyMongo 4.12 brings a number of changes including:
|
|||||||
- Support for configuring DEK cache lifetime via the ``key_expiration_ms`` argument to
|
- Support for configuring DEK cache lifetime via the ``key_expiration_ms`` argument to
|
||||||
:class:`~pymongo.encryption_options.AutoEncryptionOpts`.
|
:class:`~pymongo.encryption_options.AutoEncryptionOpts`.
|
||||||
- Support for $lookup in CSFLE and QE supported on MongoDB 8.1+.
|
- Support for $lookup in CSFLE and QE supported on MongoDB 8.1+.
|
||||||
- pymongocrypt>=1.13 is now required for :ref:`In-Use Encryption` support.
|
- pymongocrypt>=1.13 is now required for `In-Use Encryption <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/in-use-encryption/#in-use-encryption>`_ support.
|
||||||
- Added :meth:`gridfs.asynchronous.grid_file.AsyncGridFSBucket.rename_by_name` and :meth:`gridfs.grid_file.GridFSBucket.rename_by_name`
|
- Added :meth:`gridfs.asynchronous.grid_file.AsyncGridFSBucket.rename_by_name` and :meth:`gridfs.grid_file.GridFSBucket.rename_by_name`
|
||||||
for more performant renaming of a file with multiple revisions.
|
for more performant renaming of a file with multiple revisions.
|
||||||
- Added :meth:`gridfs.asynchronous.grid_file.AsyncGridFSBucket.delete_by_name` and :meth:`gridfs.grid_file.GridFSBucket.delete_by_name`
|
- Added :meth:`gridfs.asynchronous.grid_file.AsyncGridFSBucket.delete_by_name` and :meth:`gridfs.grid_file.GridFSBucket.delete_by_name`
|
||||||
@ -120,7 +347,7 @@ PyMongo 4.11 brings a number of changes including:
|
|||||||
- Dropped support for Python 3.8 and PyPy 3.9.
|
- Dropped support for Python 3.8 and PyPy 3.9.
|
||||||
- Dropped support for MongoDB 3.6.
|
- Dropped support for MongoDB 3.6.
|
||||||
- Dropped support for the MONGODB-CR authenticate mechanism, which is no longer supported by MongoDB 4.0+.
|
- Dropped support for the MONGODB-CR authenticate mechanism, which is no longer supported by MongoDB 4.0+.
|
||||||
- pymongocrypt>=1.12 is now required for :ref:`In-Use Encryption` support.
|
- pymongocrypt>=1.12 is now required for `In-Use Encryption <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/in-use-encryption/#in-use-encryption>`_ support.
|
||||||
- Added support for free-threaded Python with the GIL disabled. For more information see:
|
- Added support for free-threaded Python with the GIL disabled. For more information see:
|
||||||
`Free-threaded CPython <https://docs.python.org/3.13/whatsnew/3.13.html#whatsnew313-free-threaded-cpython>`_.
|
`Free-threaded CPython <https://docs.python.org/3.13/whatsnew/3.13.html#whatsnew313-free-threaded-cpython>`_.
|
||||||
We do not yet support free-threaded Python on Windows (`PYTHON-5027`_) or with In-Use Encryption (`PYTHON-5024`_).
|
We do not yet support free-threaded Python on Windows (`PYTHON-5027`_) or with In-Use Encryption (`PYTHON-5024`_).
|
||||||
@ -242,7 +469,7 @@ PyMongo 4.9 brings a number of improvements including:
|
|||||||
``sparsity`` and ``trim_factor`` are now optional in :class:`~pymongo.encryption_options.RangeOpts`.
|
``sparsity`` and ``trim_factor`` are now optional in :class:`~pymongo.encryption_options.RangeOpts`.
|
||||||
- Added support for the "delegated" option for the KMIP ``master_key`` in
|
- Added support for the "delegated" option for the KMIP ``master_key`` in
|
||||||
:meth:`~pymongo.encryption.ClientEncryption.create_data_key`.
|
:meth:`~pymongo.encryption.ClientEncryption.create_data_key`.
|
||||||
- pymongocrypt>=1.10 is now required for :ref:`In-Use Encryption` support.
|
- pymongocrypt>=1.10 is now required for `In-Use Encryption <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/in-use-encryption/#in-use-encryption>`_ support.
|
||||||
- Added :meth:`~pymongo.cursor.Cursor.to_list` to :class:`~pymongo.cursor.Cursor`,
|
- Added :meth:`~pymongo.cursor.Cursor.to_list` to :class:`~pymongo.cursor.Cursor`,
|
||||||
:class:`~pymongo.command_cursor.CommandCursor`,
|
:class:`~pymongo.command_cursor.CommandCursor`,
|
||||||
:class:`~pymongo.asynchronous.cursor.AsyncCursor`,
|
:class:`~pymongo.asynchronous.cursor.AsyncCursor`,
|
||||||
@ -252,7 +479,7 @@ PyMongo 4.9 brings a number of improvements including:
|
|||||||
and :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient`,
|
and :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient`,
|
||||||
enabling users to perform insert, update, and delete operations
|
enabling users to perform insert, update, and delete operations
|
||||||
against mixed namespaces in a minimized number of round trips.
|
against mixed namespaces in a minimized number of round trips.
|
||||||
Please see :doc:`examples/client_bulk` for more information.
|
Please see `Client Bulk Write <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/bulk-write/#client-bulk-write-example>`_ for more information.
|
||||||
- Added support for the ``namespace`` parameter to the
|
- Added support for the ``namespace`` parameter to the
|
||||||
:class:`~pymongo.operations.InsertOne`,
|
:class:`~pymongo.operations.InsertOne`,
|
||||||
:class:`~pymongo.operations.ReplaceOne`,
|
:class:`~pymongo.operations.ReplaceOne`,
|
||||||
@ -282,7 +509,7 @@ PyMongo 4.9 brings a number of improvements including:
|
|||||||
unction-as-a-service (FaaS) like AWS Lambda, Google Cloud Functions, and Microsoft Azure Functions.
|
unction-as-a-service (FaaS) like AWS Lambda, Google Cloud Functions, and Microsoft Azure Functions.
|
||||||
On some FaaS systems, there is a ``fork()`` operation at function
|
On some FaaS systems, there is a ``fork()`` operation at function
|
||||||
startup. By delaying the connection to the first operation, we avoid a deadlock. See
|
startup. By delaying the connection to the first operation, we avoid a deadlock. See
|
||||||
:ref:`pymongo-fork-safe` for more information.
|
`multiple forks <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/mongoclient/#multiple-forks>`_ for more information.
|
||||||
|
|
||||||
|
|
||||||
Issues Resolved
|
Issues Resolved
|
||||||
@ -389,10 +616,10 @@ PyMongo 4.7 brings a number of improvements including:
|
|||||||
using an OpenID Connect (OIDC) access token.
|
using an OpenID Connect (OIDC) access token.
|
||||||
The driver supports OIDC for workload identity, defined as an identity you assign to a software workload
|
The driver supports OIDC for workload identity, defined as an identity you assign to a software workload
|
||||||
(such as an application, service, script, or container) to authenticate and access other services and resources.
|
(such as an application, service, script, or container) to authenticate and access other services and resources.
|
||||||
Please see :doc:`examples/authentication` for more information.
|
Please see `Authentication <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/#authentication-mechanisms>`_ for more information.
|
||||||
- Added support for Python's `native logging library <https://docs.python.org/3/howto/logging.html>`_,
|
- Added support for Python's `native logging library <https://docs.python.org/3/howto/logging.html>`_,
|
||||||
enabling developers to customize the verbosity of log messages for their applications.
|
enabling developers to customize the verbosity of log messages for their applications.
|
||||||
Please see :doc:`examples/logging` for more information.
|
Please see `Logging <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/monitoring-and-logging/logging/#logging>`_ for more information.
|
||||||
- Significantly improved the performance of encoding BSON documents to JSON.
|
- Significantly improved the performance of encoding BSON documents to JSON.
|
||||||
- Added support for named KMS providers for client side field level encryption.
|
- Added support for named KMS providers for client side field level encryption.
|
||||||
Previously supported KMS providers were only: aws, azure, gcp, kmip, and local.
|
Previously supported KMS providers were only: aws, azure, gcp, kmip, and local.
|
||||||
@ -551,7 +778,7 @@ PyMongo 4.6 brings a number of improvements including:
|
|||||||
"mongodb://example.com?tls=true" is now a valid URI.
|
"mongodb://example.com?tls=true" is now a valid URI.
|
||||||
- Fixed a bug where PyMongo would incorrectly promote all cursors to exhaust cursors
|
- Fixed a bug where PyMongo would incorrectly promote all cursors to exhaust cursors
|
||||||
when connected to load balanced MongoDB clusters or Serverless clusters.
|
when connected to load balanced MongoDB clusters or Serverless clusters.
|
||||||
- Added the :ref:`network-compression-example` documentation page.
|
- Added the `network compression <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/network-compression/#compress-network-traffic>`_ documentation page.
|
||||||
- Added more timeout information to network errors.
|
- Added more timeout information to network errors.
|
||||||
|
|
||||||
Issues Resolved
|
Issues Resolved
|
||||||
@ -576,7 +803,7 @@ PyMongo 4.5 brings a number of improvements including:
|
|||||||
- Added :meth:`~pymongo.database.Database.cursor_command`
|
- Added :meth:`~pymongo.database.Database.cursor_command`
|
||||||
and :meth:`~pymongo.command_cursor.CommandCursor.try_next` to support
|
and :meth:`~pymongo.command_cursor.CommandCursor.try_next` to support
|
||||||
executing an arbitrary command that returns a cursor.
|
executing an arbitrary command that returns a cursor.
|
||||||
- ``cryptography`` 2.5 or later is now required for :ref:`OCSP` support.
|
- ``cryptography`` 2.5 or later is now required for `OCSP <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/tls/#ocsp>`_ support.
|
||||||
- Improved bson encoding and decoding performance by up to 134%(`PYTHON-3729`_, `PYTHON-3797`_, `PYTHON-3816`_, `PYTHON-3817`_, `PYTHON-3820`_, `PYTHON-3824`_, and `PYTHON-3846`_).
|
- Improved bson encoding and decoding performance by up to 134%(`PYTHON-3729`_, `PYTHON-3797`_, `PYTHON-3816`_, `PYTHON-3817`_, `PYTHON-3820`_, `PYTHON-3824`_, and `PYTHON-3846`_).
|
||||||
|
|
||||||
.. warning:: PyMongo no longer supports PyPy3 versions older than 3.8. Users
|
.. warning:: PyMongo no longer supports PyPy3 versions older than 3.8. Users
|
||||||
@ -637,7 +864,7 @@ PyMongo 4.4 brings a number of improvements including:
|
|||||||
:class:`~pymongo.encryption_options.RangeOpts`,
|
:class:`~pymongo.encryption_options.RangeOpts`,
|
||||||
and :attr:`~pymongo.encryption.Algorithm.RANGEPREVIEW` as part of the experimental
|
and :attr:`~pymongo.encryption.Algorithm.RANGEPREVIEW` as part of the experimental
|
||||||
Queryable Encryption beta.
|
Queryable Encryption beta.
|
||||||
- pymongocrypt 1.6.0 or later is now required for :ref:`In-Use Encryption` support. MongoDB
|
- pymongocrypt 1.6.0 or later is now required for `In-Use Encryption <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/in-use-encryption/#in-use-encryption>`_ support. MongoDB
|
||||||
Server 7.0 introduced a backwards breaking change to the QE protocol. Users taking
|
Server 7.0 introduced a backwards breaking change to the QE protocol. Users taking
|
||||||
advantage of the Queryable Encryption beta must now upgrade to MongoDB 7.0+ and
|
advantage of the Queryable Encryption beta must now upgrade to MongoDB 7.0+ and
|
||||||
PyMongo 4.4+.
|
PyMongo 4.4+.
|
||||||
@ -665,9 +892,9 @@ Changes in Version 4.3.3 (2022/11/17)
|
|||||||
|
|
||||||
Version 4.3.3 documents support for the following:
|
Version 4.3.3 documents support for the following:
|
||||||
|
|
||||||
- :ref:`CSFLE on-demand credentials` for cloud KMS providers.
|
- `CSFLE on-demand credentials <https://www.mongodb.com/docs/v7.0/core/csfle/tutorials/aws/aws-automatic/?interface=driver&language=python#use-automatic-client-side-field-level-encryption-with-aws>`_ for cloud KMS providers.
|
||||||
- Authentication support for :ref:`EKS Clusters`.
|
- Authentication support for `EKS Clusters <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/aws-iam/#assumerolewithwebidentity>`_.
|
||||||
- Added the :ref:`timeout-example` example page to improve the documentation
|
- Added the `timeout <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/csot/#limit-server-execution-time>`_ example page to improve the documentation
|
||||||
for :func:`pymongo.timeout`.
|
for :func:`pymongo.timeout`.
|
||||||
|
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
@ -702,7 +929,7 @@ PyMongo 4.3 brings a number of improvements including:
|
|||||||
|
|
||||||
- Added support for decoding BSON datetimes outside of the range supported
|
- Added support for decoding BSON datetimes outside of the range supported
|
||||||
by Python's :class:`~datetime.datetime` builtin. See
|
by Python's :class:`~datetime.datetime` builtin. See
|
||||||
:ref:`handling-out-of-range-datetimes` for examples, as well as
|
`handling out of range datetimes <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes>`_ for examples, as well as
|
||||||
:class:`bson.datetime_ms.DatetimeMS`,
|
:class:`bson.datetime_ms.DatetimeMS`,
|
||||||
:class:`bson.codec_options.DatetimeConversion`, and
|
:class:`bson.codec_options.DatetimeConversion`, and
|
||||||
:class:`bson.codec_options.CodecOptions`'s ``datetime_conversion``
|
:class:`bson.codec_options.CodecOptions`'s ``datetime_conversion``
|
||||||
@ -711,7 +938,7 @@ PyMongo 4.3 brings a number of improvements including:
|
|||||||
after a :py:func:`os.fork` to reduce the frequency of deadlocks. Note that
|
after a :py:func:`os.fork` to reduce the frequency of deadlocks. Note that
|
||||||
deadlocks are still possible because libraries that PyMongo depends like
|
deadlocks are still possible because libraries that PyMongo depends like
|
||||||
OpenSSL cannot be made fork() safe in multithreaded applications.
|
OpenSSL cannot be made fork() safe in multithreaded applications.
|
||||||
(`PYTHON-2484`_). For more info see :ref:`pymongo-fork-safe`.
|
(`PYTHON-2484`_). For more info see `multiple forks <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/mongoclient/#multiple-forks>`_.
|
||||||
- When used with MongoDB 6.0+, :class:`~pymongo.change_stream.ChangeStream` s
|
- When used with MongoDB 6.0+, :class:`~pymongo.change_stream.ChangeStream` s
|
||||||
now allow for new types of events (such as DDL and C2C replication events)
|
now allow for new types of events (such as DDL and C2C replication events)
|
||||||
to be recorded with the new parameter ``show_expanded_events``
|
to be recorded with the new parameter ``show_expanded_events``
|
||||||
@ -721,7 +948,7 @@ PyMongo 4.3 brings a number of improvements including:
|
|||||||
credentials expire or an error is encountered.
|
credentials expire or an error is encountered.
|
||||||
- When using the ``MONGODB-AWS`` authentication mechanism with the
|
- When using the ``MONGODB-AWS`` authentication mechanism with the
|
||||||
``aws`` extra, the behavior of credential fetching has changed with
|
``aws`` extra, the behavior of credential fetching has changed with
|
||||||
``pymongo_auth_aws>=1.1.0``. Please see :doc:`examples/authentication` for
|
``pymongo_auth_aws>=1.1.0``. Please see `Authentication <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/#authentication-mechanisms>`_ for
|
||||||
more information.
|
more information.
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
@ -754,9 +981,9 @@ PyMongo 4.2 brings a number of improvements including:
|
|||||||
|
|
||||||
- Support for MongoDB 6.0.
|
- Support for MongoDB 6.0.
|
||||||
- Support for the Queryable Encryption beta with MongoDB 6.0. Note that backwards-breaking
|
- Support for the Queryable Encryption beta with MongoDB 6.0. Note that backwards-breaking
|
||||||
changes may be made before the final release. See :ref:`automatic-queryable-client-side-encryption` for example usage.
|
changes may be made before the final release. See `automatic queryable client-side encryption <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/in-use-encryption/#queryable-encryption>`_ for example usage.
|
||||||
- Provisional (beta) support for :func:`pymongo.timeout` to apply a single timeout
|
- Provisional (beta) support for :func:`pymongo.timeout` to apply a single timeout
|
||||||
to an entire block of pymongo operations. See :ref:`timeout-example` for examples.
|
to an entire block of pymongo operations. See `timeout <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/csot/#limit-server-execution-time>`_ for examples.
|
||||||
- Added the ``timeoutMS`` URI and keyword argument to :class:`~pymongo.mongo_client.MongoClient`.
|
- Added the ``timeoutMS`` URI and keyword argument to :class:`~pymongo.mongo_client.MongoClient`.
|
||||||
- Added the :attr:`pymongo.errors.PyMongoError.timeout` property which is ``True`` when
|
- Added the :attr:`pymongo.errors.PyMongoError.timeout` property which is ``True`` when
|
||||||
the error was caused by a timeout.
|
the error was caused by a timeout.
|
||||||
@ -804,7 +1031,7 @@ Unavoidable breaking changes
|
|||||||
encryption support.
|
encryption support.
|
||||||
- :meth:`~pymongo.collection.Collection.estimated_document_count` now always uses
|
- :meth:`~pymongo.collection.Collection.estimated_document_count` now always uses
|
||||||
the `count`_ command. Due to an oversight in versions 5.0.0-5.0.8 of MongoDB,
|
the `count`_ command. Due to an oversight in versions 5.0.0-5.0.8 of MongoDB,
|
||||||
the count command was not included in V1 of the :ref:`versioned-api-ref`.
|
the count command was not included in V1 of the `Stable API <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/stable-api/#stable-api>`_.
|
||||||
Users of the Stable API with estimated_document_count are recommended to upgrade
|
Users of the Stable API with estimated_document_count are recommended to upgrade
|
||||||
their server version to 5.0.9+ or set :attr:`pymongo.server_api.ServerApi.strict`
|
their server version to 5.0.9+ or set :attr:`pymongo.server_api.ServerApi.strict`
|
||||||
to ``False`` to avoid encountering errors (`PYTHON-3167`_).
|
to ``False`` to avoid encountering errors (`PYTHON-3167`_).
|
||||||
@ -867,7 +1094,7 @@ Changes in Version 4.1 (2021/12/07)
|
|||||||
|
|
||||||
PyMongo 4.1 brings a number of improvements including:
|
PyMongo 4.1 brings a number of improvements including:
|
||||||
|
|
||||||
- Type Hinting support (formerly provided by `pymongo-stubs`_). See :doc:`examples/type_hints` for more information.
|
- Type Hinting support (formerly provided by `pymongo-stubs`_). See `Type Hints <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/run-command/#type-hints>`_ for more information.
|
||||||
- Added support for the ``comment`` parameter to all helpers. For example see
|
- Added support for the ``comment`` parameter to all helpers. For example see
|
||||||
:meth:`~pymongo.collection.Collection.insert_one`.
|
:meth:`~pymongo.collection.Collection.insert_one`.
|
||||||
- Added support for the ``let`` parameter to
|
- Added support for the ``let`` parameter to
|
||||||
@ -956,7 +1183,7 @@ Breaking Changes in 4.0
|
|||||||
:data:`bson.binary.UuidRepresentation.PYTHON_LEGACY` to
|
:data:`bson.binary.UuidRepresentation.PYTHON_LEGACY` to
|
||||||
:data:`bson.binary.UuidRepresentation.UNSPECIFIED`. Attempting to encode a
|
:data:`bson.binary.UuidRepresentation.UNSPECIFIED`. Attempting to encode a
|
||||||
:class:`uuid.UUID` instance to BSON or JSON now produces an error by default.
|
:class:`uuid.UUID` instance to BSON or JSON now produces an error by default.
|
||||||
See :ref:`handling-uuid-data-example` for details.
|
See `UUID representations <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#universally-unique-ids--uuids->`_ for details.
|
||||||
- Removed the ``waitQueueMultiple`` keyword argument to
|
- Removed the ``waitQueueMultiple`` keyword argument to
|
||||||
:class:`~pymongo.mongo_client.MongoClient` and removed
|
:class:`~pymongo.mongo_client.MongoClient` and removed
|
||||||
:exc:`pymongo.errors.ExceededMaxWaiters`.
|
:exc:`pymongo.errors.ExceededMaxWaiters`.
|
||||||
@ -1295,7 +1522,7 @@ Notable improvements
|
|||||||
|
|
||||||
- Added support for MongoDB 5.0.
|
- Added support for MongoDB 5.0.
|
||||||
- Support for MongoDB Stable API, see :class:`~pymongo.server_api.ServerApi`.
|
- Support for MongoDB Stable API, see :class:`~pymongo.server_api.ServerApi`.
|
||||||
- Support for snapshot reads on secondaries (see :ref:`snapshot-reads-ref`).
|
- Support for snapshot reads on secondaries (see `snapshot reads <https://www.mongodb.com/docs/manual/reference/read-concern-snapshot/#read-concern--snapshot->`_).
|
||||||
- Support for Azure and GCP KMS providers for client side field level
|
- Support for Azure and GCP KMS providers for client side field level
|
||||||
encryption. See the docstring for :class:`~pymongo.mongo_client.MongoClient`,
|
encryption. See the docstring for :class:`~pymongo.mongo_client.MongoClient`,
|
||||||
:class:`~pymongo.encryption_options.AutoEncryptionOpts`,
|
:class:`~pymongo.encryption_options.AutoEncryptionOpts`,
|
||||||
@ -1352,7 +1579,7 @@ Deprecations
|
|||||||
same API.
|
same API.
|
||||||
- Deprecated the :mod:`pymongo.messeage` module.
|
- Deprecated the :mod:`pymongo.messeage` module.
|
||||||
- Deprecated the ``ssl_keyfile`` and ``ssl_certfile`` URI options in favor
|
- Deprecated the ``ssl_keyfile`` and ``ssl_certfile`` URI options in favor
|
||||||
of ``tlsCertificateKeyFile`` (see :doc:`examples/tls`).
|
of ``tlsCertificateKeyFile`` (see `TLS <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/tls/#configure-transport-layer-security--tls->`_).
|
||||||
|
|
||||||
.. _PYTHON-2466: https://jira.mongodb.org/browse/PYTHON-2466
|
.. _PYTHON-2466: https://jira.mongodb.org/browse/PYTHON-2466
|
||||||
.. _PYTHON-1690: https://jira.mongodb.org/browse/PYTHON-1690
|
.. _PYTHON-1690: https://jira.mongodb.org/browse/PYTHON-1690
|
||||||
@ -1450,12 +1677,12 @@ Changes in Version 3.11.0 (2020/07/30)
|
|||||||
Version 3.11 adds support for MongoDB 4.4 and includes a number of bug fixes.
|
Version 3.11 adds support for MongoDB 4.4 and includes a number of bug fixes.
|
||||||
Highlights include:
|
Highlights include:
|
||||||
|
|
||||||
- Support for :ref:`OCSP` (Online Certificate Status Protocol).
|
- Support for `OCSP <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/tls/#ocsp>`_ (Online Certificate Status Protocol).
|
||||||
- Support for `PyOpenSSL <https://pypi.org/project/pyOpenSSL/>`_ as an
|
- Support for `PyOpenSSL <https://pypi.org/project/pyOpenSSL/>`_ as an
|
||||||
alternative TLS implementation. PyOpenSSL is required for :ref:`OCSP`
|
alternative TLS implementation. PyOpenSSL is required for `OCSP <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/tls/#ocsp>`_
|
||||||
support. It will also be installed when using the "tls" extra if the
|
support. It will also be installed when using the "tls" extra if the
|
||||||
version of Python in use is older than 2.7.9.
|
version of Python in use is older than 2.7.9.
|
||||||
- Support for the :ref:`MONGODB-AWS` authentication mechanism.
|
- Support for the `MONGODB-AWS <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/aws-iam/#aws-identity-and-access-management>`_ authentication mechanism.
|
||||||
- Support for the ``directConnection`` URI option and kwarg to
|
- Support for the ``directConnection`` URI option and kwarg to
|
||||||
:class:`~pymongo.mongo_client.MongoClient`.
|
:class:`~pymongo.mongo_client.MongoClient`.
|
||||||
- Support for speculative authentication attempts in connection handshakes
|
- Support for speculative authentication attempts in connection handshakes
|
||||||
@ -1481,7 +1708,7 @@ Highlights include:
|
|||||||
- Added support for :data:`bson.binary.UuidRepresentation.UNSPECIFIED` and
|
- Added support for :data:`bson.binary.UuidRepresentation.UNSPECIFIED` and
|
||||||
``MongoClient(uuidRepresentation='unspecified')`` which will become the
|
``MongoClient(uuidRepresentation='unspecified')`` which will become the
|
||||||
default UUID representation starting in PyMongo 4.0. See
|
default UUID representation starting in PyMongo 4.0. See
|
||||||
:ref:`handling-uuid-data-example` for details.
|
`UUID representations <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/uuid/#universally-unique-ids--uuids->`_ for details.
|
||||||
- New methods :meth:`bson.binary.Binary.from_uuid` and
|
- New methods :meth:`bson.binary.Binary.from_uuid` and
|
||||||
:meth:`bson.binary.Binary.as_uuid`.
|
:meth:`bson.binary.Binary.as_uuid`.
|
||||||
- Added the ``background`` parameter to
|
- Added the ``background`` parameter to
|
||||||
@ -1565,7 +1792,7 @@ Version 3.10 includes a number of improvements and bug fixes. Highlights
|
|||||||
include:
|
include:
|
||||||
|
|
||||||
- Support for Client-Side Field Level Encryption with MongoDB 4.2. See
|
- Support for Client-Side Field Level Encryption with MongoDB 4.2. See
|
||||||
:doc:`examples/encryption` for examples.
|
`Client-Side Field Level Encryption <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/in-use-encryption/#client-side-field-level-encryption>`_ for examples.
|
||||||
- Support for Python 3.8.
|
- Support for Python 3.8.
|
||||||
- Added :attr:`pymongo.client_session.ClientSession.in_transaction`.
|
- Added :attr:`pymongo.client_session.ClientSession.in_transaction`.
|
||||||
- Do not hold the Topology lock while creating connections in a MongoClient's
|
- Do not hold the Topology lock while creating connections in a MongoClient's
|
||||||
@ -1591,7 +1818,7 @@ Changes in Version 3.9.0 (2019/08/13)
|
|||||||
Version 3.9 adds support for MongoDB 4.2. Highlights include:
|
Version 3.9 adds support for MongoDB 4.2. Highlights include:
|
||||||
|
|
||||||
- Support for MongoDB 4.2 sharded transactions. Sharded transactions have
|
- Support for MongoDB 4.2 sharded transactions. Sharded transactions have
|
||||||
the same API as replica set transactions. See :ref:`transactions-ref`.
|
the same API as replica set transactions. See `Transactions <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/transactions/#transactions>`_.
|
||||||
- New method :meth:`pymongo.client_session.ClientSession.with_transaction` to
|
- New method :meth:`pymongo.client_session.ClientSession.with_transaction` to
|
||||||
support conveniently running a transaction in a session with automatic
|
support conveniently running a transaction in a session with automatic
|
||||||
retries and at-most-once semantics.
|
retries and at-most-once semantics.
|
||||||
@ -1693,8 +1920,7 @@ Changes in Version 3.8.0 (2019/04/22)
|
|||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
.. warning:: PyMongo no longer supports Python 2.6. RHEL 6 users should install
|
.. warning:: PyMongo no longer supports Python 2.6. RHEL 6 users should install
|
||||||
Python 2.7 or newer from `Red Hat Software Collections
|
Python 2.7 or newer from Red Hat Software Collections.
|
||||||
<https://developers.redhat.com/products/softwarecollections/overview>`_.
|
|
||||||
CentOS 6 users should install Python 2.7 or newer from `SCL
|
CentOS 6 users should install Python 2.7 or newer from `SCL
|
||||||
<https://wiki.centos.org/AdditionalResources/Repositories/SCL>`_
|
<https://wiki.centos.org/AdditionalResources/Repositories/SCL>`_
|
||||||
|
|
||||||
@ -1720,7 +1946,7 @@ Changes in Version 3.8.0 (2019/04/22)
|
|||||||
- Custom types can now be directly encoded to, and decoded from MongoDB using
|
- Custom types can now be directly encoded to, and decoded from MongoDB using
|
||||||
the :class:`~bson.codec_options.TypeCodec` and
|
the :class:`~bson.codec_options.TypeCodec` and
|
||||||
:class:`~bson.codec_options.TypeRegistry` APIs. For more information, see
|
:class:`~bson.codec_options.TypeRegistry` APIs. For more information, see
|
||||||
the :doc:`custom type example <examples/custom_type>`.
|
`Custom Types <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/custom-types/type-codecs/#encode-data-with-type-codecs>`_.
|
||||||
- Attempting a multi-document transaction on a sharded cluster now raises a
|
- Attempting a multi-document transaction on a sharded cluster now raises a
|
||||||
:exc:`~pymongo.errors.ConfigurationError`.
|
:exc:`~pymongo.errors.ConfigurationError`.
|
||||||
- :meth:`pymongo.cursor.Cursor.distinct` and
|
- :meth:`pymongo.cursor.Cursor.distinct` and
|
||||||
@ -1750,7 +1976,7 @@ Changes in Version 3.8.0 (2019/04/22)
|
|||||||
- Iterating over a :class:`~bson.raw_bson.RawBSONDocument` now maintains the
|
- Iterating over a :class:`~bson.raw_bson.RawBSONDocument` now maintains the
|
||||||
same field order of the underlying raw BSON document.
|
same field order of the underlying raw BSON document.
|
||||||
- Applications can now register a custom server selector. For more information
|
- Applications can now register a custom server selector. For more information
|
||||||
see the :doc:`server selector example <examples/server_selection>`.
|
see `Customize Server Selection <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/server-selection/#customize-server-selection>`_.
|
||||||
- The connection pool now implements a LIFO policy.
|
- The connection pool now implements a LIFO policy.
|
||||||
|
|
||||||
Unavoidable breaking changes:
|
Unavoidable breaking changes:
|
||||||
@ -1818,9 +2044,9 @@ Changes in Version 3.7.0 (2018/06/26)
|
|||||||
Version 3.7 adds support for MongoDB 4.0. Highlights include:
|
Version 3.7 adds support for MongoDB 4.0. Highlights include:
|
||||||
|
|
||||||
- Support for single replica set multi-document ACID transactions.
|
- Support for single replica set multi-document ACID transactions.
|
||||||
See :ref:`transactions-ref`.
|
See `transactions <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/transactions/#transactions>`_.
|
||||||
- Support for wire protocol compression via the new ``compressors`` URI and keyword argument to
|
- Support for wire protocol compression via the new ``compressors`` URI and keyword argument to
|
||||||
:meth:`~pymongo.mongo_client.MongoClient`. See :ref:`network-compression-example` for details.
|
:meth:`~pymongo.mongo_client.MongoClient`. See `network compression <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/network-compression/#compress-network-traffic>`_ for details.
|
||||||
- Support for Python 3.7.
|
- Support for Python 3.7.
|
||||||
- New count methods, :meth:`~pymongo.collection.Collection.count_documents`
|
- New count methods, :meth:`~pymongo.collection.Collection.count_documents`
|
||||||
and :meth:`~pymongo.collection.Collection.estimated_document_count`.
|
and :meth:`~pymongo.collection.Collection.estimated_document_count`.
|
||||||
@ -1841,9 +2067,9 @@ Version 3.7 adds support for MongoDB 4.0. Highlights include:
|
|||||||
the following features and changes allow PyMongo to function when MD5 support
|
the following features and changes allow PyMongo to function when MD5 support
|
||||||
is disabled in OpenSSL by the FIPS Object Module:
|
is disabled in OpenSSL by the FIPS Object Module:
|
||||||
|
|
||||||
- Support for the :ref:`SCRAM-SHA-256 <scram_sha_256>`
|
- Support for the `SCRAM-SHA-256 <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/scram/#scram>`_
|
||||||
authentication mechanism. The :ref:`GSSAPI <gssapi>`,
|
authentication mechanism. The `GSSAPI <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/kerberos/#kerberos--gssapi->`_,
|
||||||
:ref:`PLAIN <sasl_plain>`, and :ref:`MONGODB-X509 <mongodb_x509>`
|
`PLAIN <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/ldap/#overview>`_, and `MONGODB-X509 <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/x509/#x.509>`_
|
||||||
mechanisms can also be used to avoid issues with OpenSSL in FIPS
|
mechanisms can also be used to avoid issues with OpenSSL in FIPS
|
||||||
environments.
|
environments.
|
||||||
- MD5 checksums are now optional in GridFS. See the ``disable_md5`` option
|
- MD5 checksums are now optional in GridFS. See the ``disable_md5`` option
|
||||||
@ -1861,7 +2087,7 @@ Version 3.7 adds support for MongoDB 4.0. Highlights include:
|
|||||||
class which is a subclass of :class:`~pymongo.change_stream.ChangeStream`.
|
class which is a subclass of :class:`~pymongo.change_stream.ChangeStream`.
|
||||||
- SCRAM client and server keys are cached for improved performance, following
|
- SCRAM client and server keys are cached for improved performance, following
|
||||||
`RFC 5802 <https://tools.ietf.org/html/rfc5802>`_.
|
`RFC 5802 <https://tools.ietf.org/html/rfc5802>`_.
|
||||||
- If not specified, the authSource for the :ref:`PLAIN <sasl_plain>`
|
- If not specified, the authSource for the `PLAIN <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/ldap/#overview>`_
|
||||||
authentication mechanism defaults to $external.
|
authentication mechanism defaults to $external.
|
||||||
- wtimeoutMS is once again supported as a URI option.
|
- wtimeoutMS is once again supported as a URI option.
|
||||||
- When using unacknowledged write concern and connected to MongoDB server
|
- When using unacknowledged write concern and connected to MongoDB server
|
||||||
@ -2111,7 +2337,7 @@ Changes and Deprecations:
|
|||||||
consistent across all MongoDB versions.
|
consistent across all MongoDB versions.
|
||||||
- In Python 3, :meth:`~bson.json_util.loads` now automatically decodes JSON
|
- In Python 3, :meth:`~bson.json_util.loads` now automatically decodes JSON
|
||||||
$binary with a subtype of 0 into :class:`bytes` instead of
|
$binary with a subtype of 0 into :class:`bytes` instead of
|
||||||
:class:`~bson.binary.Binary`. See the :doc:`/python3` for more details.
|
:class:`~bson.binary.Binary`.
|
||||||
- :meth:`~bson.json_util.loads` now raises ``TypeError`` or ``ValueError``
|
- :meth:`~bson.json_util.loads` now raises ``TypeError`` or ``ValueError``
|
||||||
when parsing JSON type wrappers with values of the wrong type or any
|
when parsing JSON type wrappers with values of the wrong type or any
|
||||||
extra keys.
|
extra keys.
|
||||||
@ -2140,7 +2366,7 @@ Highlights include:
|
|||||||
|
|
||||||
- Complete support for MongoDB 3.4:
|
- Complete support for MongoDB 3.4:
|
||||||
|
|
||||||
- Unicode aware string comparison using :doc:`examples/collations`.
|
- Unicode aware string comparison using `Collation <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/configure/#collation>`_.
|
||||||
- Support for the new :class:`~bson.decimal128.Decimal128` BSON type.
|
- Support for the new :class:`~bson.decimal128.Decimal128` BSON type.
|
||||||
- A new maxStalenessSeconds read preference option.
|
- A new maxStalenessSeconds read preference option.
|
||||||
- A username is no longer required for the MONGODB-X509 authentication
|
- A username is no longer required for the MONGODB-X509 authentication
|
||||||
@ -2478,7 +2704,7 @@ In PyMongo 3.0, the ``use_greenlets`` option is gone. To use PyMongo with
|
|||||||
Gevent simply call ``gevent.monkey.patch_all()``.
|
Gevent simply call ``gevent.monkey.patch_all()``.
|
||||||
|
|
||||||
For more information,
|
For more information,
|
||||||
see :doc:`PyMongo's Gevent documentation <examples/gevent>`.
|
see `Gevent <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/integrations/#gevent>`_.
|
||||||
|
|
||||||
:class:`~pymongo.mongo_client.MongoClient` changes
|
:class:`~pymongo.mongo_client.MongoClient` changes
|
||||||
..................................................
|
..................................................
|
||||||
@ -2522,7 +2748,7 @@ the list, and used it until a network error prompted it to re-evaluate all
|
|||||||
mongoses' latencies and reconnect to one of them. In PyMongo 3, the client
|
mongoses' latencies and reconnect to one of them. In PyMongo 3, the client
|
||||||
monitors its network latency to all the mongoses continuously, and distributes
|
monitors its network latency to all the mongoses continuously, and distributes
|
||||||
operations evenly among those with the lowest latency.
|
operations evenly among those with the lowest latency.
|
||||||
See :ref:`mongos-load-balancing` for more information.
|
See `load balancing <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-targets/#replica-sets>`_ for more information.
|
||||||
|
|
||||||
The client methods ``start_request``, ``in_request``, and ``end_request``
|
The client methods ``start_request``, ``in_request``, and ``end_request``
|
||||||
are removed, and so is the ``auto_start_request`` option. Requests were
|
are removed, and so is the ``auto_start_request`` option. Requests were
|
||||||
@ -2530,7 +2756,7 @@ designed to make read-your-writes consistency more likely with the ``w=0``
|
|||||||
write concern. Additionally, a thread in a request used the same member for
|
write concern. Additionally, a thread in a request used the same member for
|
||||||
all secondary reads in a replica set. To ensure read-your-writes consistency
|
all secondary reads in a replica set. To ensure read-your-writes consistency
|
||||||
in PyMongo 3.0, do not override the default write concern with ``w=0``, and
|
in PyMongo 3.0, do not override the default write concern with ``w=0``, and
|
||||||
do not override the default :ref:`read preference <secondary-reads>` of
|
do not override the default `read preference <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/configure/#read-and-write-settings>`_ of
|
||||||
PRIMARY.
|
PRIMARY.
|
||||||
|
|
||||||
Support for the ``slaveOk`` (or ``slave_okay``), ``safe``, and
|
Support for the ``slaveOk`` (or ``slave_okay``), ``safe``, and
|
||||||
@ -2544,8 +2770,7 @@ The ``max_pool_size`` option has been removed. It is replaced by the
|
|||||||
``maxPoolSize`` MongoDB URI option. ``maxPoolSize`` is now a supported URI
|
``maxPoolSize`` MongoDB URI option. ``maxPoolSize`` is now a supported URI
|
||||||
option in PyMongo and can be passed as a keyword argument.
|
option in PyMongo and can be passed as a keyword argument.
|
||||||
|
|
||||||
The ``copy_database`` method is removed, see the
|
The ``copy_database`` method is removed, see `Copy and Clone Databases <https://www.mongodb.com/docs/database-tools/mongodump/mongodump-examples/#copy-and-clone-databases>`_ for alternatives.
|
||||||
:doc:`copy_database examples </examples/copydb>` for alternatives.
|
|
||||||
|
|
||||||
The ``disconnect`` method is removed. Use
|
The ``disconnect`` method is removed. Use
|
||||||
:meth:`~pymongo.mongo_client.MongoClient.close` instead.
|
:meth:`~pymongo.mongo_client.MongoClient.close` instead.
|
||||||
@ -2882,7 +3107,7 @@ Version 2.9.4 fixes issues reported since the release of 2.9.3.
|
|||||||
- Fixed :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient` handling of
|
- Fixed :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient` handling of
|
||||||
uuidRepresentation.
|
uuidRepresentation.
|
||||||
- Fixed building and testing the documentation with python 3.x.
|
- Fixed building and testing the documentation with python 3.x.
|
||||||
- New documentation for :doc:`examples/tls` and :doc:`atlas`.
|
- New documentation for `TLS <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/tls/#configure-transport-layer-security--tls->`_ and `Atlas <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-targets/#atlas>`_.
|
||||||
|
|
||||||
Issues Resolved
|
Issues Resolved
|
||||||
...............
|
...............
|
||||||
@ -3121,7 +3346,7 @@ PyMongo 2.7 is a major release with a large number of new features and bug
|
|||||||
fixes. Highlights include:
|
fixes. Highlights include:
|
||||||
|
|
||||||
- Full support for MongoDB 2.6.
|
- Full support for MongoDB 2.6.
|
||||||
- A new :doc:`bulk write operations API </examples/bulk>`.
|
- A new `bulk write operations API <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/bulk-write/#collection-bulk-write-example>`_.
|
||||||
- Support for server side query timeouts using
|
- Support for server side query timeouts using
|
||||||
:meth:`~pymongo.cursor.Cursor.max_time_ms`.
|
:meth:`~pymongo.cursor.Cursor.max_time_ms`.
|
||||||
- Support for writing :meth:`~pymongo.collection.Collection.aggregate`
|
- Support for writing :meth:`~pymongo.collection.Collection.aggregate`
|
||||||
@ -3132,7 +3357,7 @@ fixes. Highlights include:
|
|||||||
error details from the server.
|
error details from the server.
|
||||||
- A new GridFS :meth:`~gridfs.GridFS.find` method that returns a
|
- A new GridFS :meth:`~gridfs.GridFS.find` method that returns a
|
||||||
:class:`~gridfs.grid_file.GridOutCursor`.
|
:class:`~gridfs.grid_file.GridOutCursor`.
|
||||||
- Greatly improved :doc:`support for mod_wsgi </examples/mod_wsgi>` when using
|
- Greatly improved `support for mod_wsgi <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/integrations/#mod_wsgi>`_ when using
|
||||||
PyMongo's C extensions. Read `Jesse's blog post
|
PyMongo's C extensions. Read `Jesse's blog post
|
||||||
<https://emptysqua.re/blog/python-c-extensions-and-mod-wsgi/>`_ for details.
|
<https://emptysqua.re/blog/python-c-extensions-and-mod-wsgi/>`_ for details.
|
||||||
- Improved C extension support for ARM little endian.
|
- Improved C extension support for ARM little endian.
|
||||||
@ -3212,14 +3437,14 @@ Important new features:
|
|||||||
``waitQueueTimeoutMS`` is set, an operation that blocks waiting for a socket
|
``waitQueueTimeoutMS`` is set, an operation that blocks waiting for a socket
|
||||||
will raise :exc:`~pymongo.errors.ConnectionFailure` after the timeout. By
|
will raise :exc:`~pymongo.errors.ConnectionFailure` after the timeout. By
|
||||||
default ``waitQueueTimeoutMS`` is not set.
|
default ``waitQueueTimeoutMS`` is not set.
|
||||||
See :ref:`connection-pooling` for more information.
|
See `connection pooling <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/connection-pools/#connection-pools>`_ for more information.
|
||||||
- The :meth:`~pymongo.collection.Collection.insert` method automatically splits
|
- The :meth:`~pymongo.collection.Collection.insert` method automatically splits
|
||||||
large batches of documents into multiple insert messages based on
|
large batches of documents into multiple insert messages based on
|
||||||
:attr:`~pymongo.mongo_client.MongoClient.max_message_size`
|
:attr:`~pymongo.mongo_client.MongoClient.max_message_size`
|
||||||
- Support for the exhaust cursor flag.
|
- Support for the exhaust cursor flag.
|
||||||
See :meth:`~pymongo.collection.Collection.find` for details and caveats.
|
See :meth:`~pymongo.collection.Collection.find` for details and caveats.
|
||||||
- Support for the PLAIN and MONGODB-X509 authentication mechanisms.
|
- Support for the PLAIN and MONGODB-X509 authentication mechanisms.
|
||||||
See :doc:`the authentication docs </examples/authentication>` for more
|
See `the authentication docs <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/#authentication-mechanisms>`_ for more
|
||||||
information.
|
information.
|
||||||
- Support aggregation output as a :class:`~pymongo.cursor.Cursor`. See
|
- Support aggregation output as a :class:`~pymongo.cursor.Cursor`. See
|
||||||
:meth:`~pymongo.collection.Collection.aggregate` for details.
|
:meth:`~pymongo.collection.Collection.aggregate` for details.
|
||||||
@ -3232,7 +3457,7 @@ Important new features:
|
|||||||
to having a ``max_pool_size`` larger than necessary. Err towards a larger
|
to having a ``max_pool_size`` larger than necessary. Err towards a larger
|
||||||
value.) If your application accepts the default, continue to do so.
|
value.) If your application accepts the default, continue to do so.
|
||||||
|
|
||||||
See :ref:`connection-pooling` for more information.
|
See `connection pooling <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/connection-pools/#connection-pools>`_ for more information.
|
||||||
|
|
||||||
Issues Resolved
|
Issues Resolved
|
||||||
...............
|
...............
|
||||||
@ -3278,7 +3503,7 @@ Version 2.5 includes changes to support new features in MongoDB 2.4.
|
|||||||
|
|
||||||
Important new features:
|
Important new features:
|
||||||
|
|
||||||
- Support for :ref:`GSSAPI (Kerberos) authentication <gssapi>`.
|
- Support for `GSSAPI (Kerberos) <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/authentication/kerberos/#kerberos--gssapi->`_.
|
||||||
- Support for SSL certificate validation with hostname matching.
|
- Support for SSL certificate validation with hostname matching.
|
||||||
- Support for delegated and role based authentication.
|
- Support for delegated and role based authentication.
|
||||||
- New GEOSPHERE (2dsphere) and HASHED index constants.
|
- New GEOSPHERE (2dsphere) and HASHED index constants.
|
||||||
@ -3385,7 +3610,7 @@ Version 2.3 adds support for new features and behavior changes in MongoDB
|
|||||||
Important New Features:
|
Important New Features:
|
||||||
|
|
||||||
- Support for expanded read preferences including directing reads to tagged
|
- Support for expanded read preferences including directing reads to tagged
|
||||||
servers - See :ref:`secondary-reads` for more information.
|
servers - See `secondary reads <https://www.mongodb.com/docs/manual/core/read-preference/#mongodb-readmode-secondary>`_ for more information.
|
||||||
- Support for mongos failover.
|
- Support for mongos failover.
|
||||||
- A new :meth:`~pymongo.collection.Collection.aggregate` method to support
|
- A new :meth:`~pymongo.collection.Collection.aggregate` method to support
|
||||||
MongoDB's new `aggregation framework
|
MongoDB's new `aggregation framework
|
||||||
@ -3439,10 +3664,10 @@ to this release.
|
|||||||
|
|
||||||
Important New Features:
|
Important New Features:
|
||||||
|
|
||||||
- Support for Python 3 -
|
- Support for Python 3.
|
||||||
See the :doc:`python3` for more information.
|
See `Python 3 <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/reference/upgrade/#upgrade-pymongo-versions>`_ for more information.
|
||||||
- Support for Gevent -
|
- Support for Gevent -
|
||||||
See :doc:`examples/gevent` for more information.
|
See `Gevent <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/integrations/#gevent>`_ for more information.
|
||||||
- Improved connection pooling.
|
- Improved connection pooling.
|
||||||
See `PYTHON-287 <https://jira.mongodb.org/browse/PYTHON-287>`_.
|
See `PYTHON-287 <https://jira.mongodb.org/browse/PYTHON-287>`_.
|
||||||
|
|
||||||
@ -4048,7 +4273,7 @@ Other changes:
|
|||||||
- clean up all cases where :class:`~pymongo.errors.ConnectionFailure`
|
- clean up all cases where :class:`~pymongo.errors.ConnectionFailure`
|
||||||
is raised.
|
is raised.
|
||||||
- simplification of connection pooling - makes driver ~2x faster for
|
- simplification of connection pooling - makes driver ~2x faster for
|
||||||
simple benchmarks. see :ref:`connection-pooling` for more information.
|
simple benchmarks. see `connection pooling <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/connect/connection-options/connection-pools/#connection-pools>`_ for more information.
|
||||||
- DEPRECATED ``pool_size``, ``auto_start_request`` and ``timeout``
|
- DEPRECATED ``pool_size``, ``auto_start_request`` and ``timeout``
|
||||||
parameters to :class:`~pymongo.connection.Connection`. DEPRECATED
|
parameters to :class:`~pymongo.connection.Connection`. DEPRECATED
|
||||||
:meth:`~pymongo.connection.Connection.start_request`.
|
:meth:`~pymongo.connection.Connection.start_request`.
|
||||||
@ -4115,7 +4340,7 @@ Changes in Version 1.2 (2009/12/09)
|
|||||||
get around some issues with queries on fields named ``query``
|
get around some issues with queries on fields named ``query``
|
||||||
- enforce 4MB document limit on the client side
|
- enforce 4MB document limit on the client side
|
||||||
- added :meth:`~pymongo.collection.Collection.map_reduce` helper - see
|
- added :meth:`~pymongo.collection.Collection.map_reduce` helper - see
|
||||||
:doc:`example <examples/aggregation>`
|
`Aggregation <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/aggregation/#transform-your-data-with-aggregation>`_
|
||||||
- added :meth:`~pymongo.cursor.Cursor.distinct` method on
|
- added :meth:`~pymongo.cursor.Cursor.distinct` method on
|
||||||
:class:`~pymongo.cursor.Cursor` instances to allow distinct with
|
:class:`~pymongo.cursor.Cursor` instances to allow distinct with
|
||||||
queries
|
queries
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
Frequently Encountered Issues
|
|
||||||
=============================
|
|
||||||
|
|
||||||
Also see the :ref:`TLSErrors` section.
|
|
||||||
|
|
||||||
Server reports wire version X, PyMongo requires Y
|
|
||||||
-------------------------------------------------
|
|
||||||
|
|
||||||
When one attempts to connect to a <=3.6 version server, PyMongo will throw the following error::
|
|
||||||
|
|
||||||
>>> client.admin.command('ping')
|
|
||||||
...
|
|
||||||
pymongo.errors.ConfigurationError: Server at localhost:27017 reports wire version 6, but this version of PyMongo requires at least 7 (MongoDB 4.0).
|
|
||||||
|
|
||||||
This is caused by the driver being too new for the server it is being run against.
|
|
||||||
To resolve this issue either upgrade your database to version >= 4.0 or downgrade to an early version of PyMongo which supports MongoDB < 4.0.
|
|
||||||
|
|
||||||
|
|
||||||
'Cursor' object has no attribute '_Cursor__killed'
|
|
||||||
--------------------------------------------------
|
|
||||||
|
|
||||||
On versions of PyMongo <3.9, when supplying invalid arguments the constructor of Cursor,
|
|
||||||
there will be a TypeError raised, and an AttributeError printed to ``stderr``. The AttributeError is not relevant,
|
|
||||||
instead look at the TypeError for debugging information::
|
|
||||||
|
|
||||||
>>> coll.find(wrong=1)
|
|
||||||
Exception ignored in: <function Cursor.__del__ at 0x1048129d8>
|
|
||||||
...
|
|
||||||
AttributeError: 'Cursor' object has no attribute '_Cursor__killed'
|
|
||||||
...
|
|
||||||
TypeError: __init__() got an unexpected keyword argument 'wrong'
|
|
||||||
|
|
||||||
To fix this, make sure that you are supplying the correct keyword arguments.
|
|
||||||
In addition, you can also upgrade to PyMongo >=3.9, which will remove the spurious error.
|
|
||||||
|
|
||||||
|
|
||||||
MongoClient fails ConfigurationError
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
This is a common issue stemming from using incorrect keyword argument names.
|
|
||||||
|
|
||||||
>>> client = MongoClient(wrong=1)
|
|
||||||
...
|
|
||||||
pymongo.errors.ConfigurationError: Unknown option wrong
|
|
||||||
|
|
||||||
To fix this, check your spelling and make sure that the keyword argument you are specifying exists.
|
|
||||||
|
|
||||||
|
|
||||||
DeprecationWarning: count is deprecated
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
PyMongo no longer supports :meth:`pymongo.cursor.count`.
|
|
||||||
Instead, use :meth:`pymongo.collection.count_documents`::
|
|
||||||
|
|
||||||
>>> client = MongoClient()
|
|
||||||
>>> d = datetime.datetime(2009, 11, 12, 12)
|
|
||||||
>>> list(client.db.coll.find({"date": {"$lt": d}}, limit=2))
|
|
||||||
[{'_id': ObjectId('6247b058cebb8b179b7039f8'), 'date': datetime.datetime(1, 1, 1, 0, 0)}, {'_id': ObjectId('6247b059cebb8b179b7039f9'), 'date': datetime.datetime(1, 1, 1, 0, 0)}]
|
|
||||||
>>> client.db.coll.count_documents({"date": {"$lt": d}}, limit=2)
|
|
||||||
2
|
|
||||||
|
|
||||||
Note that this is NOT the same as ``Cursor.count_documents`` (which does not exist),
|
|
||||||
this is a method of the Collection class, so you must call it on a collection object
|
|
||||||
or you will receive the following error::
|
|
||||||
|
|
||||||
>>> Cursor(MongoClient().db.coll).count()
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "<stdin>", line 1, in <module>
|
|
||||||
AttributeError: 'Cursor' object has no attribute 'count'
|
|
||||||
>>>
|
|
||||||
|
|
||||||
Timeout when accessing MongoDB from PyMongo with tunneling
|
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
When attempting to connect to a replica set MongoDB instance over an SSH tunnel you
|
|
||||||
will receive the following error::
|
|
||||||
|
|
||||||
File "/Library/Python/2.7/site-packages/pymongo/collection.py", line 1560, in count
|
|
||||||
return self._count(cmd, collation, session)
|
|
||||||
File "/Library/Python/2.7/site-packages/pymongo/collection.py", line 1504, in _count
|
|
||||||
with self._socket_for_reads() as (connection, slave_ok):
|
|
||||||
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/contextlib.py", line 17, in __enter__
|
|
||||||
return self.gen.next()
|
|
||||||
File "/Library/Python/2.7/site-packages/pymongo/mongo_client.py", line 982, in _socket_for_reads
|
|
||||||
server = topology.select_server(read_preference)
|
|
||||||
File "/Library/Python/2.7/site-packages/pymongo/topology.py", line 224, in select_server
|
|
||||||
address))
|
|
||||||
File "/Library/Python/2.7/site-packages/pymongo/topology.py", line 183, in select_servers
|
|
||||||
selector, server_timeout, address)
|
|
||||||
File "/Library/Python/2.7/site-packages/pymongo/topology.py", line 199, in _select_servers_loop
|
|
||||||
self._error_message(selector))
|
|
||||||
pymongo.errors.ServerSelectionTimeoutError: localhost:27017: timed out
|
|
||||||
|
|
||||||
This is due to the fact that PyMongo discovers replica set members using the response from the isMaster command which
|
|
||||||
then contains the address and ports of the other members. However, these addresses and ports will not be accessible through the SSH tunnel. Thus, this behavior is unsupported.
|
|
||||||
You can, however, connect directly to a single MongoDB node using the directConnection=True option with SSH tunneling.
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
Compatibility Policy
|
|
||||||
====================
|
|
||||||
|
|
||||||
Semantic Versioning
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
PyMongo's version numbers follow `semantic versioning`_: each version number
|
|
||||||
is structured "major.minor.patch". Patch releases fix bugs, minor releases
|
|
||||||
add features (and may fix bugs), and major releases include API changes that
|
|
||||||
break backwards compatibility (and may add features and fix bugs).
|
|
||||||
|
|
||||||
Deprecation
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Before we remove a feature in a major release, PyMongo's maintainers make an
|
|
||||||
effort to release at least one minor version that *deprecates* it. We add
|
|
||||||
"**DEPRECATED**" to the feature's documentation, and update the code to raise a
|
|
||||||
`DeprecationWarning`_. You can ensure your code is future-proof by running
|
|
||||||
your code with the latest PyMongo release and looking for DeprecationWarnings.
|
|
||||||
|
|
||||||
The interpreter silences DeprecationWarnings by default. For example, the
|
|
||||||
following code uses the deprecated ``insert`` method but does not raise any
|
|
||||||
warning:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# "insert.py" (with PyMongo 3.X)
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client.test.test.insert({})
|
|
||||||
|
|
||||||
To print deprecation warnings to stderr, run python with "-Wd"::
|
|
||||||
|
|
||||||
$ python3 -Wd insert.py
|
|
||||||
insert.py:4: DeprecationWarning: insert is deprecated. Use insert_one or insert_many instead.
|
|
||||||
client.test.test.insert({})
|
|
||||||
|
|
||||||
You can turn warnings into exceptions with "python -We"::
|
|
||||||
|
|
||||||
$ python3 -We insert.py
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "insert.py", line 4, in <module>
|
|
||||||
client.test.test.insert({})
|
|
||||||
File "/home/durin/work/mongo-python-driver/pymongo/collection.py", line 2906, in insert
|
|
||||||
"instead.", DeprecationWarning, stacklevel=2)
|
|
||||||
DeprecationWarning: insert is deprecated. Use insert_one or insert_many instead.
|
|
||||||
|
|
||||||
If your own code's test suite passes with "python -We" then it uses no
|
|
||||||
deprecated PyMongo features.
|
|
||||||
|
|
||||||
.. seealso:: The Python documentation on `the warnings module`_,
|
|
||||||
and `the -W command line option`_.
|
|
||||||
|
|
||||||
.. _semantic versioning: https://semver.org/
|
|
||||||
|
|
||||||
.. _DeprecationWarning:
|
|
||||||
https://docs.python.org/3/library/exceptions.html#DeprecationWarning
|
|
||||||
|
|
||||||
.. _the warnings module: https://docs.python.org/3/library/warnings.html
|
|
||||||
|
|
||||||
.. _the -W command line option: https://docs.python.org/3/using/cmdline.html#cmdoption-W
|
|
||||||
14
doc/conf.py
14
doc/conf.py
@ -82,15 +82,23 @@ pygments_style = "sphinx"
|
|||||||
# Options for link checking
|
# Options for link checking
|
||||||
# The anchors on the rendered markdown page are created after the fact,
|
# The anchors on the rendered markdown page are created after the fact,
|
||||||
# so those link results in a 404.
|
# so those link results in a 404.
|
||||||
# wiki.centos.org has been flakey.
|
# wiki.centos.org has been flaky.
|
||||||
# sourceforge.net is giving a 403 error, but is still accessible from the browser.
|
# sourceforge.net is giving a 403 error, but is still accessible from the browser.
|
||||||
|
# Links to release notes in jira give 401 error: unauthorized. PYTHON-5585
|
||||||
linkcheck_ignore = [
|
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/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",
|
"https://github.com/mongodb/libmongocrypt/blob/master/bindings/python/README.rst#installing-from-source",
|
||||||
r"https://wiki.centos.org/[\w/]*",
|
r"https://wiki.centos.org/[\w/]*",
|
||||||
r"https://sourceforge.net/",
|
r"https://sourceforge.net/",
|
||||||
|
r"https://jira\.mongodb\.org/secure/ReleaseNote\.jspa.*",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Allow for flaky links.
|
||||||
|
linkcheck_retries = 3
|
||||||
|
|
||||||
# -- Options for extensions ----------------------------------------------------
|
# -- Options for extensions ----------------------------------------------------
|
||||||
autoclass_content = "init"
|
autoclass_content = "init"
|
||||||
|
|
||||||
@ -180,8 +188,8 @@ latex_documents = [
|
|||||||
("index", "PyMongo.tex", "PyMongo Documentation", "Michael Dirolf", "manual"),
|
("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 name of an image file (relative to this directory) to place at the top
|
||||||
# the title page.
|
# of the title page.
|
||||||
# latex_logo = None
|
# latex_logo = None
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
|||||||
@ -103,3 +103,8 @@ The following is a list of people who have contributed to
|
|||||||
- Terry Patterson
|
- Terry Patterson
|
||||||
- Romain Morotti
|
- Romain Morotti
|
||||||
- Navjot Singh (navjots18)
|
- Navjot Singh (navjots18)
|
||||||
|
- Jib Adegunloye (Jibola)
|
||||||
|
- Jeffrey A. Clark (aclark4life)
|
||||||
|
- Steven Silvester (blink1073)
|
||||||
|
- Noah Stapp (NoahStapp)
|
||||||
|
- Cal Jacobson (cj81499)
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
Developer Guide
|
|
||||||
===============
|
|
||||||
|
|
||||||
Technical guide for contributors to PyMongo.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
periodic_executor
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
Periodic Executors
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. currentmodule:: pymongo
|
|
||||||
|
|
||||||
PyMongo implements a :class:`~periodic_executor.PeriodicExecutor` for two
|
|
||||||
purposes: as the background thread for :class:`~monitor.Monitor`, and to
|
|
||||||
regularly check if there are ``OP_KILL_CURSORS`` messages that must be sent to the server.
|
|
||||||
|
|
||||||
Killing Cursors
|
|
||||||
---------------
|
|
||||||
|
|
||||||
An incompletely iterated :class:`~cursor.Cursor` on the client represents an
|
|
||||||
open cursor object on the server. In code like this, we lose a reference to
|
|
||||||
the cursor before finishing iteration::
|
|
||||||
|
|
||||||
for doc in collection.find():
|
|
||||||
raise Exception()
|
|
||||||
|
|
||||||
We try to send an ``OP_KILL_CURSORS`` to the server to tell it to clean up the
|
|
||||||
server-side cursor. But we must not take any locks directly from the cursor's
|
|
||||||
destructor (see `PYTHON-799`_), so we cannot safely use the PyMongo data
|
|
||||||
structures required to send a message. The solution is to add the cursor's id
|
|
||||||
to an array on the :class:`~mongo_client.MongoClient` without taking any locks.
|
|
||||||
|
|
||||||
Each client has a :class:`~periodic_executor.PeriodicExecutor` devoted to
|
|
||||||
checking the array for cursor ids. Any it sees are the result of cursors that
|
|
||||||
were freed while the server-side cursor was still open. The executor can safely
|
|
||||||
take the locks it needs in order to send the ``OP_KILL_CURSORS`` message.
|
|
||||||
|
|
||||||
.. _PYTHON-799: https://jira.mongodb.org/browse/PYTHON-799
|
|
||||||
|
|
||||||
Stopping Executors
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Just as :class:`~cursor.Cursor` must not take any locks from its destructor,
|
|
||||||
neither can :class:`~mongo_client.MongoClient` and :class:`~topology.Topology`.
|
|
||||||
Thus, although the client calls :meth:`close` on its kill-cursors thread, and
|
|
||||||
the topology calls :meth:`close` on all its monitor threads, the :meth:`close`
|
|
||||||
method cannot actually call :meth:`wake` on the executor, since :meth:`wake`
|
|
||||||
takes a lock.
|
|
||||||
|
|
||||||
Instead, executors wake periodically to check if ``self.close`` is set,
|
|
||||||
and if so they exit.
|
|
||||||
|
|
||||||
A thread can log spurious errors if it wakes late in the Python interpreter's
|
|
||||||
shutdown sequence, so we try to join threads before then. Each periodic
|
|
||||||
executor (either a monitor or a kill-cursors thread) adds a weakref to itself
|
|
||||||
to a set called ``_EXECUTORS``, in the ``periodic_executor`` module.
|
|
||||||
|
|
||||||
An `exit handler`_ runs on shutdown and tells all executors to stop, then
|
|
||||||
tries (with a short timeout) to join all executor threads.
|
|
||||||
|
|
||||||
.. _exit handler: https://docs.python.org/2/library/atexit.html
|
|
||||||
|
|
||||||
Monitoring
|
|
||||||
----------
|
|
||||||
|
|
||||||
For each server in the topology, :class:`~topology.Topology` uses a periodic
|
|
||||||
executor to launch a monitor thread. This thread must not prevent the topology
|
|
||||||
from being freed, so it weakrefs the topology. Furthermore, it uses a weakref
|
|
||||||
callback to terminate itself soon after the topology is freed.
|
|
||||||
|
|
||||||
Solid lines represent strong references, dashed lines weak ones:
|
|
||||||
|
|
||||||
.. generated with graphviz: "dot -Tpng periodic-executor-refs.dot > periodic-executor-refs.png"
|
|
||||||
|
|
||||||
.. image:: ../static/periodic-executor-refs.png
|
|
||||||
|
|
||||||
See `Stopping Executors`_ above for an explanation of the ``_EXECUTORS`` set.
|
|
||||||
|
|
||||||
It is a requirement of the `Server Discovery And Monitoring Spec`_ that a
|
|
||||||
sleeping monitor can be awakened early. Aside from infrequent wakeups to do
|
|
||||||
their appointed chores, and occasional interruptions, periodic executors also
|
|
||||||
wake periodically to check if they should terminate.
|
|
||||||
|
|
||||||
Our first implementation of this idea was the obvious one: use the Python
|
|
||||||
standard library's threading.Condition.wait with a timeout. Another thread
|
|
||||||
wakes the executor early by signaling the condition variable.
|
|
||||||
|
|
||||||
A topology cannot signal the condition variable to tell the executor to
|
|
||||||
terminate, because it would risk a deadlock in the garbage collector: no
|
|
||||||
destructor or weakref callback can take a lock to signal the condition variable
|
|
||||||
(see `PYTHON-863`_); thus the only way for a dying object to terminate a
|
|
||||||
periodic executor is to set its "stopped" flag and let the executor see the
|
|
||||||
flag next time it wakes.
|
|
||||||
|
|
||||||
We erred on the side of prompt cleanup, and set the check interval at 100ms. We
|
|
||||||
assumed that checking a flag and going back to sleep 10 times a second was
|
|
||||||
cheap on modern machines.
|
|
||||||
|
|
||||||
Starting in Python 3.2, the builtin C implementation of lock.acquire takes a
|
|
||||||
timeout parameter, so Python 3.2+ Condition variables sleep simply by calling
|
|
||||||
lock.acquire; they are implemented as efficiently as expected.
|
|
||||||
|
|
||||||
But in Python 2, lock.acquire has no timeout. To wait with a timeout, a Python
|
|
||||||
2 condition variable sleeps a millisecond, tries to acquire the lock, sleeps
|
|
||||||
twice as long, and tries again. This exponential backoff reaches a maximum
|
|
||||||
sleep time of 50ms.
|
|
||||||
|
|
||||||
If PyMongo calls the condition variable's "wait" method with a short timeout,
|
|
||||||
the exponential backoff is restarted frequently. Overall, the condition variable
|
|
||||||
is not waking a few times a second, but hundreds of times. (See `PYTHON-983`_.)
|
|
||||||
|
|
||||||
Thus the current design of periodic executors is surprisingly simple: they
|
|
||||||
do a simple ``time.sleep`` for a half-second, check if it is time to wake or
|
|
||||||
terminate, and sleep again.
|
|
||||||
|
|
||||||
.. _Server Discovery And Monitoring Spec: https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-monitoring.md#requesting-an-immediate-check
|
|
||||||
|
|
||||||
.. _PYTHON-863: https://jira.mongodb.org/browse/PYTHON-863
|
|
||||||
|
|
||||||
.. _PYTHON-983: https://jira.mongodb.org/browse/PYTHON-983
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
Aggregation Examples
|
|
||||||
====================
|
|
||||||
|
|
||||||
There are several methods of performing aggregations in MongoDB. These
|
|
||||||
examples cover the new aggregation framework, using map reduce and using the
|
|
||||||
group method.
|
|
||||||
|
|
||||||
.. testsetup::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client.drop_database("aggregation_example")
|
|
||||||
|
|
||||||
Setup
|
|
||||||
-----
|
|
||||||
To start, we'll insert some example data which we can perform
|
|
||||||
aggregations on:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> db = MongoClient().aggregation_example
|
|
||||||
>>> result = db.things.insert_many(
|
|
||||||
... [
|
|
||||||
... {"x": 1, "tags": ["dog", "cat"]},
|
|
||||||
... {"x": 2, "tags": ["cat"]},
|
|
||||||
... {"x": 2, "tags": ["mouse", "cat", "dog"]},
|
|
||||||
... {"x": 3, "tags": []},
|
|
||||||
... ]
|
|
||||||
... )
|
|
||||||
>>> result.inserted_ids
|
|
||||||
[ObjectId('...'), ObjectId('...'), ObjectId('...'), ObjectId('...')]
|
|
||||||
|
|
||||||
.. _aggregate-examples:
|
|
||||||
|
|
||||||
Aggregation Framework
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
This example shows how to use the
|
|
||||||
:meth:`~pymongo.collection.Collection.aggregate` method to use the aggregation
|
|
||||||
framework. We'll perform a simple aggregation to count the number of
|
|
||||||
occurrences for each tag in the ``tags`` array, across the entire collection.
|
|
||||||
To achieve this we need to pass in three operations to the pipeline.
|
|
||||||
First, we need to unwind the ``tags`` array, then group by the tags and
|
|
||||||
sum them up, finally we sort by count.
|
|
||||||
|
|
||||||
Python dictionaries prior to 3.7 don't maintain order. You should use :class:`~bson.son.SON`
|
|
||||||
or :class:`collections.OrderedDict` where explicit ordering is required for an older Python version
|
|
||||||
eg "$sort":
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
aggregate requires server version **>= 2.1.0**.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from bson.son import SON
|
|
||||||
>>> pipeline = [
|
|
||||||
... {"$unwind": "$tags"},
|
|
||||||
... {"$group": {"_id": "$tags", "count": {"$sum": 1}}},
|
|
||||||
... {"$sort": SON([("count", -1), ("_id", -1)])},
|
|
||||||
... ]
|
|
||||||
>>> import pprint
|
|
||||||
>>> pprint.pprint(list(db.things.aggregate(pipeline)))
|
|
||||||
[{'_id': 'cat', 'count': 3},
|
|
||||||
{'_id': 'dog', 'count': 2},
|
|
||||||
{'_id': 'mouse', 'count': 1}]
|
|
||||||
|
|
||||||
To run an explain plan for this aggregation use
|
|
||||||
`PyMongoExplain <https://pypi.org/project/pymongoexplain/>`_,
|
|
||||||
a companion library for PyMongo. It allows you to explain any CRUD operation
|
|
||||||
by providing a few convenience classes::
|
|
||||||
|
|
||||||
>>> from pymongoexplain import ExplainableCollection
|
|
||||||
>>> ExplainableCollection(collection).aggregate(pipeline)
|
|
||||||
{'ok': 1.0, 'queryPlanner': [...]}
|
|
||||||
|
|
||||||
Or, use the :meth:`~pymongo.database.Database.command` method::
|
|
||||||
|
|
||||||
>>> db.command('aggregate', 'things', pipeline=pipeline, explain=True)
|
|
||||||
{'ok': 1.0, 'stages': [...]}
|
|
||||||
|
|
||||||
As well as simple aggregations the aggregation framework provides projection
|
|
||||||
capabilities to reshape the returned data. Using projections and aggregation,
|
|
||||||
you can add computed fields, create new virtual sub-objects, and extract
|
|
||||||
sub-fields into the top-level of results.
|
|
||||||
|
|
||||||
.. seealso:: The full documentation for MongoDB's `aggregation framework
|
|
||||||
<https://mongodb.com/docs/manual/applications/aggregation>`_
|
|
||||||
@ -1,528 +0,0 @@
|
|||||||
Authentication Examples
|
|
||||||
=======================
|
|
||||||
|
|
||||||
MongoDB supports several different authentication mechanisms. These examples
|
|
||||||
cover all authentication methods currently supported by PyMongo, documenting
|
|
||||||
Python module and MongoDB version dependencies.
|
|
||||||
|
|
||||||
.. _percent escaped:
|
|
||||||
|
|
||||||
Percent-Escaping Username and Password
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
Username and password must be percent-escaped with
|
|
||||||
:py:func:`urllib.parse.quote_plus`, to be used in a MongoDB URI. For example::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> import urllib.parse
|
|
||||||
>>> username = urllib.parse.quote_plus('user')
|
|
||||||
>>> username
|
|
||||||
'user'
|
|
||||||
>>> password = urllib.parse.quote_plus('pass/word')
|
|
||||||
>>> password
|
|
||||||
'pass%2Fword'
|
|
||||||
>>> MongoClient('mongodb://%s:%s@127.0.0.1' % (username, password))
|
|
||||||
...
|
|
||||||
|
|
||||||
.. _scram_sha_256:
|
|
||||||
|
|
||||||
SCRAM-SHA-256 (RFC 7677)
|
|
||||||
------------------------
|
|
||||||
.. versionadded:: 3.7
|
|
||||||
|
|
||||||
SCRAM-SHA-256 is the default authentication mechanism supported by a cluster
|
|
||||||
configured for authentication with MongoDB 4.0 or later. Authentication
|
|
||||||
requires a username, a password, and a database name. The default database
|
|
||||||
name is "admin", this can be overridden with the ``authSource`` option.
|
|
||||||
Credentials can be specified as arguments to
|
|
||||||
:class:`~pymongo.mongo_client.MongoClient`::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> client = MongoClient('example.com',
|
|
||||||
... username='user',
|
|
||||||
... password='password',
|
|
||||||
... authSource='the_database',
|
|
||||||
... authMechanism='SCRAM-SHA-256')
|
|
||||||
|
|
||||||
Or through the MongoDB URI::
|
|
||||||
|
|
||||||
>>> uri = "mongodb://user:password@example.com/?authSource=the_database&authMechanism=SCRAM-SHA-256"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
SCRAM-SHA-1 (RFC 5802)
|
|
||||||
----------------------
|
|
||||||
.. versionadded:: 2.8
|
|
||||||
|
|
||||||
SCRAM-SHA-1 is the default authentication mechanism supported by a cluster
|
|
||||||
configured for authentication with MongoDB 3.0 or later. Authentication
|
|
||||||
requires a username, a password, and a database name. The default database
|
|
||||||
name is "admin", this can be overridden with the ``authSource`` option.
|
|
||||||
Credentials can be specified as arguments to
|
|
||||||
:class:`~pymongo.mongo_client.MongoClient`::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> client = MongoClient('example.com',
|
|
||||||
... username='user',
|
|
||||||
... password='password',
|
|
||||||
... authSource='the_database',
|
|
||||||
... authMechanism='SCRAM-SHA-1')
|
|
||||||
|
|
||||||
Or through the MongoDB URI::
|
|
||||||
|
|
||||||
>>> uri = "mongodb://user:password@example.com/?authSource=the_database&authMechanism=SCRAM-SHA-1"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
For best performance on Python versions older than 2.7.8 install `backports.pbkdf2`_.
|
|
||||||
|
|
||||||
.. _backports.pbkdf2: https://pypi.python.org/pypi/backports.pbkdf2/
|
|
||||||
|
|
||||||
Default Authentication Mechanism
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
If no mechanism is specified, PyMongo automatically negotiates the mechanism to use (SCRAM-SHA-1
|
|
||||||
or SCRAM-SHA-256) with the MongoDB server.
|
|
||||||
|
|
||||||
Default Database and "authSource"
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
You can specify both a default database and the authentication database in the
|
|
||||||
URI::
|
|
||||||
|
|
||||||
>>> uri = "mongodb://user:password@example.com/default_db?authSource=admin"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
PyMongo will authenticate on the "admin" database, but the default database
|
|
||||||
will be "default_db"::
|
|
||||||
|
|
||||||
>>> # get_database with no "name" argument chooses the DB from the URI
|
|
||||||
>>> db = MongoClient(uri).get_database()
|
|
||||||
>>> print(db.name)
|
|
||||||
'default_db'
|
|
||||||
|
|
||||||
.. _mongodb_x509:
|
|
||||||
|
|
||||||
MONGODB-X509
|
|
||||||
------------
|
|
||||||
.. versionadded:: 2.6
|
|
||||||
|
|
||||||
The MONGODB-X509 mechanism authenticates via the X.509 certificate presented
|
|
||||||
by the driver during TLS/SSL negotiation. This authentication method requires
|
|
||||||
the use of TLS/SSL connections with certificate validation::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> client = MongoClient('example.com',
|
|
||||||
... authMechanism="MONGODB-X509",
|
|
||||||
... tls=True,
|
|
||||||
... tlsCertificateKeyFile='/path/to/client.pem',
|
|
||||||
... tlsCAFile='/path/to/ca.pem')
|
|
||||||
|
|
||||||
MONGODB-X509 authenticates against the $external virtual database, so you
|
|
||||||
do not have to specify a database in the URI::
|
|
||||||
|
|
||||||
>>> uri = "mongodb://example.com/?authMechanism=MONGODB-X509"
|
|
||||||
>>> client = MongoClient(uri,
|
|
||||||
... tls=True,
|
|
||||||
... tlsCertificateKeyFile='/path/to/client.pem',
|
|
||||||
... tlsCAFile='/path/to/ca.pem')
|
|
||||||
>>>
|
|
||||||
|
|
||||||
.. _gssapi:
|
|
||||||
|
|
||||||
GSSAPI (Kerberos)
|
|
||||||
-----------------
|
|
||||||
.. versionadded:: 2.5
|
|
||||||
|
|
||||||
GSSAPI (Kerberos) authentication is available in the Enterprise Edition of
|
|
||||||
MongoDB.
|
|
||||||
|
|
||||||
Unix
|
|
||||||
~~~~
|
|
||||||
|
|
||||||
To authenticate using GSSAPI you must first install the python `kerberos`_ or
|
|
||||||
`pykerberos`_ module using pip. Make sure you run kinit before
|
|
||||||
using the following authentication methods::
|
|
||||||
|
|
||||||
$ kinit mongodbuser@EXAMPLE.COM
|
|
||||||
mongodbuser@EXAMPLE.COM's Password:
|
|
||||||
$ klist
|
|
||||||
Credentials cache: FILE:/tmp/krb5cc_1000
|
|
||||||
Principal: mongodbuser@EXAMPLE.COM
|
|
||||||
|
|
||||||
Issued Expires Principal
|
|
||||||
Feb 9 13:48:51 2013 Feb 9 23:48:51 2013 krbtgt/EXAMPLE.COM@EXAMPLE.COM
|
|
||||||
|
|
||||||
Now authenticate using the MongoDB URI. GSSAPI authenticates against the
|
|
||||||
$external virtual database so you do not have to specify a database in the
|
|
||||||
URI::
|
|
||||||
|
|
||||||
>>> # Note: the kerberos principal must be url encoded.
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb://mongodbuser%40EXAMPLE.COM@mongo-server.example.com/?authMechanism=GSSAPI"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
>>>
|
|
||||||
|
|
||||||
The default service name used by MongoDB and PyMongo is ``mongodb``. You can
|
|
||||||
specify a custom service name with the ``authMechanismProperties`` option::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb://mongodbuser%40EXAMPLE.COM@mongo-server.example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:myservicename"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
Windows (SSPI)
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
.. versionadded:: 3.3
|
|
||||||
|
|
||||||
First install the `winkerberos`_ module. Unlike authentication on Unix kinit is
|
|
||||||
not used. If the user to authenticate is different from the user that owns the
|
|
||||||
application process provide a password to authenticate::
|
|
||||||
|
|
||||||
>>> uri = "mongodb://mongodbuser%40EXAMPLE.COM:mongodbuserpassword@example.com/?authMechanism=GSSAPI"
|
|
||||||
|
|
||||||
Two extra ``authMechanismProperties`` are supported on Windows platforms:
|
|
||||||
|
|
||||||
- CANONICALIZE_HOST_NAME - Uses the fully qualified domain name (FQDN) of the
|
|
||||||
MongoDB host for the server principal (GSSAPI libraries on Unix do this by
|
|
||||||
default)::
|
|
||||||
|
|
||||||
>>> uri = "mongodb://mongodbuser%40EXAMPLE.COM@example.com/?authMechanism=GSSAPI&authMechanismProperties=CANONICALIZE_HOST_NAME:true"
|
|
||||||
|
|
||||||
- SERVICE_REALM - This is used when the user's realm is different from the service's realm::
|
|
||||||
|
|
||||||
>>> uri = "mongodb://mongodbuser%40EXAMPLE.COM@example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_REALM:otherrealm"
|
|
||||||
|
|
||||||
|
|
||||||
.. _kerberos: https://pypi.python.org/pypi/kerberos
|
|
||||||
.. _pykerberos: https://pypi.python.org/pypi/pykerberos
|
|
||||||
.. _winkerberos: https://pypi.python.org/pypi/winkerberos/
|
|
||||||
|
|
||||||
.. _sasl_plain:
|
|
||||||
|
|
||||||
SASL PLAIN (RFC 4616)
|
|
||||||
---------------------
|
|
||||||
.. versionadded:: 2.6
|
|
||||||
|
|
||||||
MongoDB Enterprise Edition version 2.6 and newer support the SASL PLAIN
|
|
||||||
authentication mechanism, initially intended for delegating authentication
|
|
||||||
to an LDAP server. These examples use the $external virtual database for LDAP support::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb://user:password@example.com/?authMechanism=PLAIN"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
>>>
|
|
||||||
|
|
||||||
SASL PLAIN is a clear-text authentication mechanism. We **strongly** recommend
|
|
||||||
that you connect to MongoDB using TLS/SSL with certificate validation when
|
|
||||||
using the SASL PLAIN mechanism::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb://user:password@example.com/?authMechanism=PLAIN"
|
|
||||||
>>> client = MongoClient(uri,
|
|
||||||
... tls=True,
|
|
||||||
... tlsCertificateKeyFile='/path/to/client.pem',
|
|
||||||
... tlsCAFile='/path/to/ca.pem')
|
|
||||||
>>>
|
|
||||||
|
|
||||||
.. _MONGODB-AWS:
|
|
||||||
|
|
||||||
MONGODB-AWS
|
|
||||||
-----------
|
|
||||||
.. versionadded:: 3.11
|
|
||||||
|
|
||||||
The MONGODB-AWS authentication mechanism is available in MongoDB 4.4+ and
|
|
||||||
requires extra pymongo dependencies. To use it, install pymongo with the
|
|
||||||
``aws`` extra::
|
|
||||||
|
|
||||||
$ python -m pip install 'pymongo[aws]'
|
|
||||||
|
|
||||||
The MONGODB-AWS mechanism authenticates using AWS IAM credentials (an access
|
|
||||||
key ID and a secret access key), `temporary AWS IAM credentials`_ obtained
|
|
||||||
from an `AWS Security Token Service (STS)`_ `Assume Role`_ request,
|
|
||||||
AWS Lambda `environment variables`_, or temporary AWS IAM credentials assigned
|
|
||||||
to an `EC2 instance`_ or ECS task. The use of temporary credentials, in
|
|
||||||
addition to an access key ID and a secret access key, also requires a
|
|
||||||
security (or session) token.
|
|
||||||
|
|
||||||
Credentials can be configured through the MongoDB URI, environment variables,
|
|
||||||
or the local EC2 or ECS endpoint. The order in which the client searches for
|
|
||||||
`credentials`_ is the same as the one used by the AWS ``boto3`` library
|
|
||||||
when using ``pymongo_auth_aws>=1.1.0``.
|
|
||||||
|
|
||||||
Because we are now using ``boto3`` to handle credentials, the order and
|
|
||||||
locations of credentials are slightly different from before. Particularly,
|
|
||||||
if you have a shared AWS credentials or config file,
|
|
||||||
then those credentials will be used by default if AWS auth environment
|
|
||||||
variables are not set. To override this behavior, set
|
|
||||||
``AWS_SHARED_CREDENTIALS_FILE=""`` in your shell or add
|
|
||||||
``os.environ["AWS_SHARED_CREDENTIALS_FILE"] = ""`` to your script or
|
|
||||||
application. Alternatively, you can create an AWS profile specifically for
|
|
||||||
your MongoDB credentials and set ``AWS_PROFILE`` to that profile name.
|
|
||||||
|
|
||||||
MONGODB-AWS authenticates against the "$external" virtual database, so none of
|
|
||||||
the URIs in this section need to include the ``authSource`` URI option.
|
|
||||||
|
|
||||||
.. _credentials: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html
|
|
||||||
|
|
||||||
AWS IAM credentials
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Applications can authenticate using AWS IAM credentials by providing a valid
|
|
||||||
access key id and secret access key pair as the username and password,
|
|
||||||
respectively, in the MongoDB URI. A sample URI would be::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb+srv://<access_key_id>:<secret_access_key>@example.mongodb.net/?authMechanism=MONGODB-AWS"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
.. note:: The access_key_id and secret_access_key passed into the URI MUST
|
|
||||||
be `percent escaped`_.
|
|
||||||
|
|
||||||
AssumeRole
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
Applications can authenticate using temporary credentials returned from an
|
|
||||||
assume role request. These temporary credentials consist of an access key
|
|
||||||
ID, a secret access key, and a security token passed into the URI.
|
|
||||||
A sample URI would be::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb+srv://<access_key_id>:<secret_access_key>@example.mongodb.net/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:<session_token>"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
.. note:: The access_key_id, secret_access_key, and session_token passed into
|
|
||||||
the URI MUST be `percent escaped`_.
|
|
||||||
|
|
||||||
|
|
||||||
AWS Lambda (Environment Variables)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
When the username and password are not provided and the MONGODB-AWS mechanism
|
|
||||||
is set, the client will fallback to using the `environment variables`_
|
|
||||||
``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``, and ``AWS_SESSION_TOKEN``
|
|
||||||
for the access key ID, secret access key, and session token, respectively::
|
|
||||||
|
|
||||||
$ export AWS_ACCESS_KEY_ID=<access_key_id>
|
|
||||||
$ export AWS_SECRET_ACCESS_KEY=<secret_access_key>
|
|
||||||
$ export AWS_SESSION_TOKEN=<session_token>
|
|
||||||
$ python
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb+srv://example.mongodb.net/?authMechanism=MONGODB-AWS"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
.. note:: No username, password, or session token is passed into the URI.
|
|
||||||
PyMongo will use credentials set via the environment variables.
|
|
||||||
These environment variables MUST NOT be `percent escaped`_.
|
|
||||||
|
|
||||||
|
|
||||||
.. _EKS Clusters:
|
|
||||||
|
|
||||||
EKS Clusters
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Applications using the `Authenticating users for your cluster from an OpenID Connect identity provider <https://docs.aws.amazon.com/eks/latest/userguide/authenticate-oidc-identity-provider.html>`_ capability on EKS can now
|
|
||||||
use the provided credentials, by giving the associated IAM User
|
|
||||||
`sts:AssumeRoleWithWebIdentity <https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html>`_
|
|
||||||
permission.
|
|
||||||
|
|
||||||
When the username and password are not provided, the MONGODB-AWS mechanism
|
|
||||||
is set, and ``AWS_WEB_IDENTITY_TOKEN_FILE``, ``AWS_ROLE_ARN``, and
|
|
||||||
optional ``AWS_ROLE_SESSION_NAME`` are available, the driver will use
|
|
||||||
an ``AssumeRoleWithWebIdentity`` call to retrieve temporary credentials.
|
|
||||||
The application must be using ``pymongo_auth_aws`` >= 1.1.0 for EKS support.
|
|
||||||
|
|
||||||
ECS Container
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Applications can authenticate from an ECS container via temporary
|
|
||||||
credentials assigned to the machine. A sample URI on an ECS container
|
|
||||||
would be::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb+srv://example.mongodb.com/?authMechanism=MONGODB-AWS"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
.. note:: No username, password, or session token is passed into the URI.
|
|
||||||
PyMongo will query the ECS container endpoint to obtain these
|
|
||||||
credentials.
|
|
||||||
|
|
||||||
EC2 Instance
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Applications can authenticate from an EC2 instance via temporary
|
|
||||||
credentials assigned to the machine. A sample URI on an EC2 machine
|
|
||||||
would be::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> uri = "mongodb+srv://example.mongodb.com/?authMechanism=MONGODB-AWS"
|
|
||||||
>>> client = MongoClient(uri)
|
|
||||||
|
|
||||||
.. note:: No username, password, or session token is passed into the URI.
|
|
||||||
PyMongo will query the EC2 instance endpoint to obtain these
|
|
||||||
credentials.
|
|
||||||
|
|
||||||
.. _temporary AWS IAM credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html
|
|
||||||
.. _AWS Security Token Service (STS): https://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html
|
|
||||||
.. _Assume Role: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
|
||||||
.. _EC2 instance: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html
|
|
||||||
.. _environment variables: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
|
|
||||||
|
|
||||||
MONGODB-OIDC
|
|
||||||
------------
|
|
||||||
.. versionadded:: 4.7
|
|
||||||
|
|
||||||
The `MONGODB-OIDC authentication mechanism`_ is available in MongoDB 7.0+ on Linux platforms.
|
|
||||||
|
|
||||||
The MONGODB-OIDC mechanism authenticates using an OpenID Connect (OIDC) access token.
|
|
||||||
The driver supports OIDC for workload identity, defined as an identity you assign to a software workload
|
|
||||||
(such as an application, service, script, or container) to authenticate and access other services and resources.
|
|
||||||
|
|
||||||
Credentials can be configured through the MongoDB URI or as arguments to
|
|
||||||
:class:`~pymongo.mongo_client.MongoClient`.
|
|
||||||
|
|
||||||
Built-in Support
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The driver has built-in support for Azure IMDS and GCP IMDS environments. Other environments
|
|
||||||
are supported with `Custom Callbacks`_.
|
|
||||||
|
|
||||||
Azure IMDS
|
|
||||||
^^^^^^^^^^
|
|
||||||
|
|
||||||
For an application running on an Azure VM or otherwise using the `Azure Internal Metadata Service`_,
|
|
||||||
you can use the built-in support for Azure. If using an Azure managed identity, the "<client_id>" is
|
|
||||||
the client ID. If using a service principal to represent an enterprise application, the "<client_id>" is
|
|
||||||
the application ID of the service principal. The ``<audience>`` value is the ``audience``
|
|
||||||
`configured on your MongoDB deployment`_.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
uri = os.environ["MONGODB_URI"]
|
|
||||||
|
|
||||||
props = {"ENVIRONMENT": "azure", "TOKEN_RESOURCE": "<audience>"}
|
|
||||||
c = MongoClient(
|
|
||||||
uri,
|
|
||||||
username="<client_id>",
|
|
||||||
authMechanism="MONGODB-OIDC",
|
|
||||||
authMechanismProperties=props,
|
|
||||||
)
|
|
||||||
c.test.test.insert_one({})
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
If the application is running on an Azure VM and only one managed identity is associated with the
|
|
||||||
VM, ``username`` can be omitted.
|
|
||||||
|
|
||||||
If providing the ``TOKEN_RESOURCE`` as part of a connection string, it can be given as follows.
|
|
||||||
If the ``TOKEN_RESOURCE`` contains any of the following characters [``,``, ``+``, ``&``], then
|
|
||||||
it MUST be url-encoded.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
uri = f'{os.environ["MONGODB_URI"]}?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:<audience>'
|
|
||||||
c = MongoClient(uri)
|
|
||||||
c.test.test.insert_one({})
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
GCP IMDS
|
|
||||||
^^^^^^^^
|
|
||||||
|
|
||||||
For an application running on an GCP VM or otherwise using the `GCP Internal Metadata Service`_,
|
|
||||||
you can use the built-in support for GCP, where ``<audience>`` below is the ``audience``
|
|
||||||
`configured on your MongoDB deployment`_.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
uri = os.environ["MONGODB_URI"]
|
|
||||||
|
|
||||||
props = {"ENVIRONMENT": "gcp", "TOKEN_RESOURCE": "<audience>"}
|
|
||||||
c = MongoClient(uri, authMechanism="MONGODB-OIDC", authMechanismProperties=props)
|
|
||||||
c.test.test.insert_one({})
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
If providing the ``TOKEN_RESOURCE`` as part of a connection string, it can be given as follows.
|
|
||||||
If the ``TOKEN_RESOURCE`` contains any of the following characters [``,``, ``+``, ``&``], then
|
|
||||||
it MUST be url-encoded.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
uri = f'{os.environ["MONGODB_URI"]}?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:<audience>'
|
|
||||||
c = MongoClient(uri)
|
|
||||||
c.test.test.insert_one({})
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
Custom Callbacks
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
For environments that are not directly supported by the driver, you can use :class:`~pymongo.auth_oidc.OIDCCallback`.
|
|
||||||
Some examples are given below.
|
|
||||||
|
|
||||||
Other Azure Environments
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
For applications running on Azure Functions, App Service Environment (ASE), or
|
|
||||||
Azure Kubernetes Service (AKS), you can use the `azure-identity package`_
|
|
||||||
to fetch the credentials. This example assumes you have set environment variables for
|
|
||||||
the ``audience`` `configured on your MongoDB deployment`_, and for the client id of the Azure
|
|
||||||
managed identity.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
from azure.identity import DefaultAzureCredential
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult
|
|
||||||
|
|
||||||
audience = os.environ["AZURE_AUDIENCE"]
|
|
||||||
client_id = os.environ["AZURE_IDENTITY_CLIENT_ID"]
|
|
||||||
uri = os.environ["MONGODB_URI"]
|
|
||||||
|
|
||||||
|
|
||||||
class MyCallback(OIDCCallback):
|
|
||||||
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
|
|
||||||
credential = DefaultAzureCredential(managed_identity_client_id=client_id)
|
|
||||||
token = credential.get_token(f"{audience}/.default").token
|
|
||||||
return OIDCCallbackResult(access_token=token)
|
|
||||||
|
|
||||||
|
|
||||||
props = {"OIDC_CALLBACK": MyCallback()}
|
|
||||||
c = MongoClient(uri, authMechanism="MONGODB-OIDC", authMechanismProperties=props)
|
|
||||||
c.test.test.insert_one({})
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
GCP GKE
|
|
||||||
^^^^^^^
|
|
||||||
|
|
||||||
For a Google Kubernetes Engine cluster with a `configured service account`_, the token can be read from the standard
|
|
||||||
service account token file location.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pymongo.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult
|
|
||||||
|
|
||||||
|
|
||||||
class MyCallback(OIDCCallback):
|
|
||||||
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
|
|
||||||
with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as fid:
|
|
||||||
token = fid.read()
|
|
||||||
return OIDCCallbackResult(access_token=token)
|
|
||||||
|
|
||||||
|
|
||||||
uri = os.environ["MONGODB_URI"]
|
|
||||||
props = {"OIDC_CALLBACK": MyCallback()}
|
|
||||||
c = MongoClient(uri, authMechanism="MONGODB-OIDC", authMechanismProperties=props)
|
|
||||||
c.test.test.insert_one({})
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
.. _MONGODB-OIDC authentication mechanism: https://www.mongodb.com/docs/manual/core/security-oidc/
|
|
||||||
.. _Azure Internal Metadata Service: https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service
|
|
||||||
.. _configured on your MongoDB deployment: https://www.mongodb.com/docs/manual/reference/parameters/#mongodb-parameter-param.oidcIdentityProviders
|
|
||||||
.. _GCP Internal Metadata Service: https://cloud.google.com/compute/docs/metadata/querying-metadata
|
|
||||||
.. _azure-identity package: https://pypi.org/project/azure-identity/
|
|
||||||
.. _configured service account: https://cloud.google.com/kubernetes-engine/docs/how-to/service-accounts
|
|
||||||
@ -1,184 +0,0 @@
|
|||||||
Bulk Write Operations
|
|
||||||
=====================
|
|
||||||
|
|
||||||
.. testsetup::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client.drop_database("bulk_example")
|
|
||||||
|
|
||||||
This tutorial explains how to take advantage of PyMongo's bulk
|
|
||||||
write operation features. Executing write operations in batches
|
|
||||||
reduces the number of network round trips, increasing write
|
|
||||||
throughput.
|
|
||||||
|
|
||||||
Bulk Insert
|
|
||||||
-----------
|
|
||||||
|
|
||||||
.. versionadded:: 2.6
|
|
||||||
|
|
||||||
A batch of documents can be inserted by passing a list to the
|
|
||||||
:meth:`~pymongo.collection.Collection.insert_many` method. PyMongo
|
|
||||||
will automatically split the batch into smaller sub-batches based on
|
|
||||||
the maximum message size accepted by MongoDB, supporting very large
|
|
||||||
bulk insert operations.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> import pymongo
|
|
||||||
>>> db = pymongo.MongoClient().bulk_example
|
|
||||||
>>> db.test.insert_many([{"i": i} for i in range(10000)]).inserted_ids
|
|
||||||
[...]
|
|
||||||
>>> db.test.count_documents({})
|
|
||||||
10000
|
|
||||||
|
|
||||||
Mixed Bulk Write Operations
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
.. versionadded:: 2.7
|
|
||||||
|
|
||||||
PyMongo also supports executing mixed bulk write operations. A batch
|
|
||||||
of insert, update, and remove operations can be executed together using
|
|
||||||
the bulk write operations API.
|
|
||||||
|
|
||||||
.. _ordered_bulk:
|
|
||||||
|
|
||||||
Ordered Bulk Write Operations
|
|
||||||
.............................
|
|
||||||
|
|
||||||
Ordered bulk write operations are batched and sent to the server in the
|
|
||||||
order provided for serial execution. The return value is an instance of
|
|
||||||
:class:`~pymongo.results.BulkWriteResult` describing the type and count
|
|
||||||
of operations performed.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
:options: +NORMALIZE_WHITESPACE
|
|
||||||
|
|
||||||
>>> from pprint import pprint
|
|
||||||
>>> from pymongo import InsertOne, DeleteMany, ReplaceOne, UpdateOne
|
|
||||||
>>> result = db.test.bulk_write(
|
|
||||||
... [
|
|
||||||
... DeleteMany({}), # Remove all documents from the previous example.
|
|
||||||
... InsertOne({"_id": 1}),
|
|
||||||
... InsertOne({"_id": 2}),
|
|
||||||
... InsertOne({"_id": 3}),
|
|
||||||
... UpdateOne({"_id": 1}, {"$set": {"foo": "bar"}}),
|
|
||||||
... UpdateOne({"_id": 4}, {"$inc": {"j": 1}}, upsert=True),
|
|
||||||
... ReplaceOne({"j": 1}, {"j": 2}),
|
|
||||||
... ]
|
|
||||||
... )
|
|
||||||
>>> pprint(result.bulk_api_result)
|
|
||||||
{'nInserted': 3,
|
|
||||||
'nMatched': 2,
|
|
||||||
'nModified': 2,
|
|
||||||
'nRemoved': 10000,
|
|
||||||
'nUpserted': 1,
|
|
||||||
'upserted': [{'_id': 4, 'index': 5}],
|
|
||||||
'writeConcernErrors': [],
|
|
||||||
'writeErrors': []}
|
|
||||||
|
|
||||||
The first write failure that occurs (e.g. duplicate key error) aborts the
|
|
||||||
remaining operations, and PyMongo raises
|
|
||||||
:class:`~pymongo.errors.BulkWriteError`. The :attr:`details` attribute of
|
|
||||||
the exception instance provides the execution results up until the failure
|
|
||||||
occurred and details about the failure - including the operation that caused
|
|
||||||
the failure.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
:options: +NORMALIZE_WHITESPACE
|
|
||||||
|
|
||||||
>>> from pymongo import InsertOne, DeleteOne, ReplaceOne
|
|
||||||
>>> from pymongo.errors import BulkWriteError
|
|
||||||
>>> requests = [
|
|
||||||
... ReplaceOne({"j": 2}, {"i": 5}),
|
|
||||||
... InsertOne({"_id": 4}), # Violates the unique key constraint on _id.
|
|
||||||
... DeleteOne({"i": 5}),
|
|
||||||
... ]
|
|
||||||
>>> try:
|
|
||||||
... db.test.bulk_write(requests)
|
|
||||||
... except BulkWriteError as bwe:
|
|
||||||
... pprint(bwe.details)
|
|
||||||
...
|
|
||||||
{'nInserted': 0,
|
|
||||||
'nMatched': 1,
|
|
||||||
'nModified': 1,
|
|
||||||
'nRemoved': 0,
|
|
||||||
'nUpserted': 0,
|
|
||||||
'upserted': [],
|
|
||||||
'writeConcernErrors': [],
|
|
||||||
'writeErrors': [{'code': 11000,
|
|
||||||
'errmsg': '...E11000...duplicate key error...',
|
|
||||||
'index': 1,...
|
|
||||||
'op': {'_id': 4}}]}
|
|
||||||
|
|
||||||
.. _unordered_bulk:
|
|
||||||
|
|
||||||
Unordered Bulk Write Operations
|
|
||||||
...............................
|
|
||||||
|
|
||||||
Unordered bulk write operations are batched and sent to the server in
|
|
||||||
**arbitrary order** where they may be executed in parallel. Any errors
|
|
||||||
that occur are reported after all operations are attempted.
|
|
||||||
|
|
||||||
In the next example the first and third operations fail due to the unique
|
|
||||||
constraint on _id. Since we are doing unordered execution the second
|
|
||||||
and fourth operations succeed.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
:options: +NORMALIZE_WHITESPACE
|
|
||||||
|
|
||||||
>>> requests = [
|
|
||||||
... InsertOne({"_id": 1}),
|
|
||||||
... DeleteOne({"_id": 2}),
|
|
||||||
... InsertOne({"_id": 3}),
|
|
||||||
... ReplaceOne({"_id": 4}, {"i": 1}),
|
|
||||||
... ]
|
|
||||||
>>> try:
|
|
||||||
... db.test.bulk_write(requests, ordered=False)
|
|
||||||
... except BulkWriteError as bwe:
|
|
||||||
... pprint(bwe.details)
|
|
||||||
...
|
|
||||||
{'nInserted': 0,
|
|
||||||
'nMatched': 1,
|
|
||||||
'nModified': 1,
|
|
||||||
'nRemoved': 1,
|
|
||||||
'nUpserted': 0,
|
|
||||||
'upserted': [],
|
|
||||||
'writeConcernErrors': [],
|
|
||||||
'writeErrors': [{'code': 11000,
|
|
||||||
'errmsg': '...E11000...duplicate key error...',
|
|
||||||
'index': 0,...
|
|
||||||
'op': {'_id': 1}},
|
|
||||||
{'code': 11000,
|
|
||||||
'errmsg': '...',
|
|
||||||
'index': 2,...
|
|
||||||
'op': {'_id': 3}}]}
|
|
||||||
|
|
||||||
Write Concern
|
|
||||||
.............
|
|
||||||
|
|
||||||
Bulk operations are executed with the
|
|
||||||
:attr:`~pymongo.collection.Collection.write_concern` of the collection they
|
|
||||||
are executed against. Write concern errors (e.g. wtimeout) will be reported
|
|
||||||
after all operations are attempted, regardless of execution order.
|
|
||||||
|
|
||||||
::
|
|
||||||
>>> from pymongo import WriteConcern
|
|
||||||
>>> coll = db.get_collection(
|
|
||||||
... 'test', write_concern=WriteConcern(w=3, wtimeout=1))
|
|
||||||
>>> try:
|
|
||||||
... coll.bulk_write([InsertOne({'a': i}) for i in range(4)])
|
|
||||||
... except BulkWriteError as bwe:
|
|
||||||
... pprint(bwe.details)
|
|
||||||
...
|
|
||||||
{'nInserted': 4,
|
|
||||||
'nMatched': 0,
|
|
||||||
'nModified': 0,
|
|
||||||
'nRemoved': 0,
|
|
||||||
'nUpserted': 0,
|
|
||||||
'upserted': [],
|
|
||||||
'writeConcernErrors': [{'code': 64...
|
|
||||||
'errInfo': {'wtimeout': True},
|
|
||||||
'errmsg': 'waiting for replication timed out'}],
|
|
||||||
'writeErrors': []}
|
|
||||||
@ -1,192 +0,0 @@
|
|||||||
Client Bulk Write Operations
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. testsetup::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client.drop_database("client_bulk_example")
|
|
||||||
db = client.client_bulk_example
|
|
||||||
client.db.drop_collection("test_one")
|
|
||||||
client.db.drop_collection("test_two")
|
|
||||||
client.db.drop_collection("test_three")
|
|
||||||
client.db.drop_collection("test_four")
|
|
||||||
client.db.drop_collection("test_five")
|
|
||||||
client.db.drop_collection("test_six")
|
|
||||||
|
|
||||||
The :meth:`~pymongo.mongo_client.MongoClient.bulk_write`
|
|
||||||
method has been added to :class:`~pymongo.mongo_client.MongoClient` in PyMongo 4.9.
|
|
||||||
This method enables users to perform batches of write operations **across
|
|
||||||
multiple namespaces** in a minimized number of round trips, and
|
|
||||||
to receive detailed results for each operation performed.
|
|
||||||
|
|
||||||
.. note:: This method requires MongoDB server version 8.0+.
|
|
||||||
|
|
||||||
Basic Usage
|
|
||||||
------------
|
|
||||||
|
|
||||||
A list of insert, update, and delete operations can be passed into the
|
|
||||||
:meth:`~pymongo.mongo_client.MongoClient.bulk_write` method. Each request
|
|
||||||
must include the namespace on which to perform the operation.
|
|
||||||
|
|
||||||
PyMongo will automatically split the given requests into smaller sub-batches based on
|
|
||||||
the maximum message size accepted by MongoDB, supporting very large bulk write operations.
|
|
||||||
|
|
||||||
The return value is an instance of
|
|
||||||
:class:`~pymongo.results.ClientBulkWriteResult`.
|
|
||||||
|
|
||||||
.. _summary_client_bulk:
|
|
||||||
|
|
||||||
Summary Results
|
|
||||||
.................
|
|
||||||
|
|
||||||
By default, the returned :class:`~pymongo.results.ClientBulkWriteResult` instance will contain a
|
|
||||||
summary of the types of operations performed in the bulk write, along with their respective counts.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
:options: +NORMALIZE_WHITESPACE
|
|
||||||
:skipif: server_major_version < 8
|
|
||||||
|
|
||||||
>>> from pymongo import InsertOne, DeleteOne, UpdateOne
|
|
||||||
>>> models = [
|
|
||||||
... InsertOne(namespace="db.test_one", document={"_id": 1}),
|
|
||||||
... InsertOne(namespace="db.test_two", document={"_id": 2}),
|
|
||||||
... DeleteOne(namespace="db.test_one", filter={"_id": 1}),
|
|
||||||
... UpdateOne(
|
|
||||||
... namespace="db.test_two",
|
|
||||||
... filter={"_id": 4},
|
|
||||||
... update={"$inc": {"j": 1}},
|
|
||||||
... upsert=True,
|
|
||||||
... ),
|
|
||||||
... ]
|
|
||||||
>>> result = client.bulk_write(models)
|
|
||||||
>>> result.inserted_count
|
|
||||||
2
|
|
||||||
>>> result.deleted_count
|
|
||||||
1
|
|
||||||
>>> result.modified_count
|
|
||||||
0
|
|
||||||
>>> result.upserted_count
|
|
||||||
1
|
|
||||||
|
|
||||||
.. _verbose_client_bulk:
|
|
||||||
|
|
||||||
Verbose Results
|
|
||||||
.................
|
|
||||||
|
|
||||||
If the ``verbose_results`` parameter is set to True, the returned :class:`~pymongo.results.ClientBulkWriteResult`
|
|
||||||
instance will also include detailed results about each successful operation performed as part of the bulk write.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
:options: +NORMALIZE_WHITESPACE
|
|
||||||
:skipif: server_major_version < 8
|
|
||||||
|
|
||||||
>>> from pymongo import InsertOne, DeleteMany, ReplaceOne, UpdateMany
|
|
||||||
>>> models = [
|
|
||||||
... DeleteMany(
|
|
||||||
... namespace="db.test_two", filter={}
|
|
||||||
... ), # Delete all documents from the previous example
|
|
||||||
... InsertOne(namespace="db.test_one", document={"_id": 1}),
|
|
||||||
... InsertOne(namespace="db.test_one", document={"_id": 2}),
|
|
||||||
... InsertOne(namespace="db.test_two", document={"_id": 3}),
|
|
||||||
... UpdateMany(namespace="db.test_one", filter={}, update={"$set": {"foo": "bar"}}),
|
|
||||||
... ReplaceOne(
|
|
||||||
... namespace="db.test_two", filter={"j": 1}, replacement={"_id": 4}, upsert=True
|
|
||||||
... ),
|
|
||||||
... ]
|
|
||||||
>>> result = client.bulk_write(models, verbose_results=True)
|
|
||||||
>>> result.delete_results
|
|
||||||
{0: DeleteResult({'ok': 1.0, 'idx': 0, 'n': 2}, ...)}
|
|
||||||
>>> result.insert_results
|
|
||||||
{1: InsertOneResult(1, ...),
|
|
||||||
2: InsertOneResult(2, ...),
|
|
||||||
3: InsertOneResult(3, ...)}
|
|
||||||
>>> result.update_results
|
|
||||||
{4: UpdateResult({'ok': 1.0, 'idx': 4, 'n': 2, 'nModified': 2}, ...),
|
|
||||||
5: UpdateResult({'ok': 1.0, 'idx': 5, 'n': 1, 'nModified': 0, 'upserted': {'_id': 4}}, ...)}
|
|
||||||
|
|
||||||
|
|
||||||
Handling Errors
|
|
||||||
----------------
|
|
||||||
|
|
||||||
If any errors occur during the bulk write, a :class:`~pymongo.errors.ClientBulkWriteException` will be raised.
|
|
||||||
If a server, connection, or network error occurred, the ``error`` field of the exception will contain
|
|
||||||
that error.
|
|
||||||
|
|
||||||
Individual write errors or write concern errors get recorded in the ``write_errors`` and ``write_concern_errors`` fields of the exception.
|
|
||||||
The ``partial_result`` field gets populated with the results of any operations that were successfully completed before the exception was raised.
|
|
||||||
|
|
||||||
.. _ordered_client_bulk:
|
|
||||||
|
|
||||||
Ordered Operations
|
|
||||||
....................
|
|
||||||
|
|
||||||
In an ordered bulk write (the default), if an individual write fails, no further operations will get executed.
|
|
||||||
For example, a duplicate key error on the third operation below aborts the remaining two operations.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
:options: +NORMALIZE_WHITESPACE
|
|
||||||
:skipif: server_major_version < 8
|
|
||||||
|
|
||||||
>>> from pymongo import InsertOne, DeleteOne
|
|
||||||
>>> from pymongo.errors import ClientBulkWriteException
|
|
||||||
>>> models = [
|
|
||||||
... InsertOne(namespace="db.test_three", document={"_id": 3}),
|
|
||||||
... InsertOne(namespace="db.test_four", document={"_id": 4}),
|
|
||||||
... InsertOne(namespace="db.test_three", document={"_id": 3}), # Duplicate _id
|
|
||||||
... InsertOne(namespace="db.test_four", document={"_id": 5}),
|
|
||||||
... DeleteOne(namespace="db.test_three", filter={"_id": 3}),
|
|
||||||
... ]
|
|
||||||
>>> try:
|
|
||||||
... client.bulk_write(models)
|
|
||||||
... except ClientBulkWriteException as cbwe:
|
|
||||||
... exception = cbwe
|
|
||||||
...
|
|
||||||
>>> exception.write_errors
|
|
||||||
[{'ok': 0.0,
|
|
||||||
'idx': 2,
|
|
||||||
'code': 11000,
|
|
||||||
'errmsg': 'E11000 duplicate key error ... dup key: { _id: 3 }', ...
|
|
||||||
'op': {'insert': 0, 'document': {'_id': 3}}}]
|
|
||||||
>>> exception.partial_result.inserted_count
|
|
||||||
2
|
|
||||||
>>> exception.partial_result.deleted_count
|
|
||||||
0
|
|
||||||
|
|
||||||
.. _unordered_client_bulk:
|
|
||||||
|
|
||||||
Unordered Operations
|
|
||||||
.....................
|
|
||||||
|
|
||||||
If the ``ordered`` parameter is set to False, all operations in the bulk write will be attempted, regardless of any individual write errors that occur.
|
|
||||||
For example, the fourth and fifth write operations below get executed successfully, despite the duplicate key error on the third operation.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
:options: +NORMALIZE_WHITESPACE
|
|
||||||
:skipif: server_major_version < 8
|
|
||||||
|
|
||||||
>>> from pymongo import InsertOne, DeleteOne
|
|
||||||
>>> from pymongo.errors import ClientBulkWriteException
|
|
||||||
>>> models = [
|
|
||||||
... InsertOne(namespace="db.test_five", document={"_id": 5}),
|
|
||||||
... InsertOne(namespace="db.test_six", document={"_id": 6}),
|
|
||||||
... InsertOne(namespace="db.test_five", document={"_id": 5}), # Duplicate _id
|
|
||||||
... InsertOne(namespace="db.test_six", document={"_id": 7}),
|
|
||||||
... DeleteOne(namespace="db.test_five", filter={"_id": 5}),
|
|
||||||
... ]
|
|
||||||
>>> try:
|
|
||||||
... client.bulk_write(models, ordered=False)
|
|
||||||
... except ClientBulkWriteException as cbwe:
|
|
||||||
... exception = cbwe
|
|
||||||
...
|
|
||||||
>>> exception.write_errors
|
|
||||||
[{'ok': 0.0,
|
|
||||||
'idx': 2,
|
|
||||||
'code': 11000,
|
|
||||||
'errmsg': 'E11000 duplicate key error ... dup key: { _id: 5 }', ...
|
|
||||||
'op': {'insert': 0, 'document': {'_id': 5}}}]
|
|
||||||
>>> exception.partial_result.inserted_count
|
|
||||||
3
|
|
||||||
>>> exception.partial_result.deleted_count
|
|
||||||
1
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
Collations
|
|
||||||
==========
|
|
||||||
|
|
||||||
.. seealso:: The API docs for :mod:`~pymongo.collation`.
|
|
||||||
|
|
||||||
Collations are a new feature in MongoDB version 3.4. They provide a set of rules
|
|
||||||
to use when comparing strings that comply with the conventions of a particular
|
|
||||||
language, such as Spanish or German. If no collation is specified, the server
|
|
||||||
sorts strings based on a binary comparison. Many languages have specific
|
|
||||||
ordering rules, and collations allow users to build applications that adhere to
|
|
||||||
language-specific comparison rules.
|
|
||||||
|
|
||||||
In French, for example, the last accent in a given word determines the sorting
|
|
||||||
order. The correct sorting order for the following four words in French is::
|
|
||||||
|
|
||||||
cote < côte < coté < côté
|
|
||||||
|
|
||||||
Specifying a French collation allows users to sort string fields using the
|
|
||||||
French sort order.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
Users can specify a collation for a
|
|
||||||
:ref:`collection<collation-on-collection>`, an
|
|
||||||
:ref:`index<collation-on-index>`, or a
|
|
||||||
:ref:`CRUD command <collation-on-operation>`.
|
|
||||||
|
|
||||||
Collation Parameters:
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Collations can be specified with the :class:`~pymongo.collation.Collation` model
|
|
||||||
or with plain Python dictionaries. The structure is the same::
|
|
||||||
|
|
||||||
Collation(locale=<string>,
|
|
||||||
caseLevel=<bool>,
|
|
||||||
caseFirst=<string>,
|
|
||||||
strength=<int>,
|
|
||||||
numericOrdering=<bool>,
|
|
||||||
alternate=<string>,
|
|
||||||
maxVariable=<string>,
|
|
||||||
backwards=<bool>)
|
|
||||||
|
|
||||||
The only required parameter is ``locale``, which the server parses as
|
|
||||||
an `ICU format locale ID <https://www.mongodb.com/docs/manual/reference/collation-locales-defaults/>`_.
|
|
||||||
For example, set ``locale`` to ``en_US`` to represent US English
|
|
||||||
or ``fr_CA`` to represent Canadian French.
|
|
||||||
|
|
||||||
For a complete description of the available parameters, see the MongoDB `manual
|
|
||||||
</>`_.
|
|
||||||
|
|
||||||
.. COMMENT add link for manual entry.
|
|
||||||
|
|
||||||
.. _collation-on-collection:
|
|
||||||
|
|
||||||
Assign a Default Collation to a Collection
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The following example demonstrates how to create a new collection called
|
|
||||||
``contacts`` and assign a default collation with the ``fr_CA`` locale. This
|
|
||||||
operation ensures that all queries that are run against the ``contacts``
|
|
||||||
collection use the ``fr_CA`` collation unless another collation is explicitly
|
|
||||||
specified::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.collation import Collation
|
|
||||||
|
|
||||||
db = MongoClient().test
|
|
||||||
collection = db.create_collection('contacts',
|
|
||||||
collation=Collation(locale='fr_CA'))
|
|
||||||
|
|
||||||
.. _collation-on-index:
|
|
||||||
|
|
||||||
Assign a Default Collation to an Index
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
When creating a new index, you can specify a default collation.
|
|
||||||
|
|
||||||
The following example shows how to create an index on the ``name``
|
|
||||||
field of the ``contacts`` collection, with the ``unique`` parameter
|
|
||||||
enabled and a default collation with ``locale`` set to ``fr_CA``::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.collation import Collation
|
|
||||||
|
|
||||||
contacts = MongoClient().test.contacts
|
|
||||||
contacts.create_index('name',
|
|
||||||
unique=True,
|
|
||||||
collation=Collation(locale='fr_CA'))
|
|
||||||
|
|
||||||
.. _collation-on-operation:
|
|
||||||
|
|
||||||
Specify a Collation for a Query
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Individual queries can specify a collation to use when sorting
|
|
||||||
results. The following example demonstrates a query that runs on the
|
|
||||||
``contacts`` collection in database ``test``. It matches on
|
|
||||||
documents that contain ``New York`` in the ``city`` field,
|
|
||||||
and sorts on the ``name`` field with the ``fr_CA`` collation::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.collation import Collation
|
|
||||||
|
|
||||||
collection = MongoClient().test.contacts
|
|
||||||
docs = collection.find({'city': 'New York'}).sort('name').collation(
|
|
||||||
Collation(locale='fr_CA'))
|
|
||||||
|
|
||||||
Other Query Types
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
You can use collations to control document matching rules for several different
|
|
||||||
types of queries. All the various update and delete methods
|
|
||||||
(:meth:`~pymongo.collection.Collection.update_one`,
|
|
||||||
:meth:`~pymongo.collection.Collection.update_many`,
|
|
||||||
:meth:`~pymongo.collection.Collection.delete_one`, etc.) support collation, and
|
|
||||||
you can create query filters which employ collations to comply with any of the
|
|
||||||
languages and variants available to the ``locale`` parameter.
|
|
||||||
|
|
||||||
The following example uses a collation with ``strength`` set to
|
|
||||||
:const:`~pymongo.collation.CollationStrength.SECONDARY`, which considers only
|
|
||||||
the base character and character accents in string comparisons, but not case
|
|
||||||
sensitivity, for example. All documents in the ``contacts`` collection with
|
|
||||||
``jürgen`` (case-insensitive) in the ``first_name`` field are updated::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.collation import Collation, CollationStrength
|
|
||||||
|
|
||||||
contacts = MongoClient().test.contacts
|
|
||||||
result = contacts.update_many(
|
|
||||||
{'first_name': 'jürgen'},
|
|
||||||
{'$set': {'verified': 1}},
|
|
||||||
collation=Collation(locale='de',
|
|
||||||
strength=CollationStrength.SECONDARY))
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
Copying a Database
|
|
||||||
==================
|
|
||||||
|
|
||||||
MongoDB >= 4.2
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Starting in MongoDB version 4.2, the server removes the deprecated ``copydb`` command.
|
|
||||||
As an alternative, users can use ``mongodump`` and ``mongorestore`` (with the ``mongorestore``
|
|
||||||
options ``--nsFrom`` and ``--nsTo``).
|
|
||||||
|
|
||||||
For example, to copy the ``test`` database from a local instance running on the
|
|
||||||
default port 27017 to the ``examples`` database on the same instance, you can:
|
|
||||||
|
|
||||||
#. Use ``mongodump`` to dump the test database to an archive ``mongodump-test-db``::
|
|
||||||
|
|
||||||
mongodump --archive="mongodump-test-db" --db=test
|
|
||||||
|
|
||||||
#. Use ``mongorestore`` with ``--nsFrom`` and ``--nsTo`` to restore (with database name change)
|
|
||||||
from the archive::
|
|
||||||
|
|
||||||
mongorestore --archive="mongodump-test-db" --nsFrom='test.*' --nsTo='examples.*'
|
|
||||||
|
|
||||||
Include additional options as necessary, such as to specify the uri or host, username,
|
|
||||||
password and authentication database.
|
|
||||||
|
|
||||||
For more info about using ``mongodump`` and ``mongorestore`` see the `Copy a Database`_ example
|
|
||||||
in the official ``mongodump`` documentation.
|
|
||||||
|
|
||||||
MongoDB <= 4.0
|
|
||||||
--------------
|
|
||||||
|
|
||||||
When using MongoDB <= 4.0, it is possible to use the deprecated ``copydb`` command
|
|
||||||
to copy a database. To copy a database within a single ``mongod`` process, or
|
|
||||||
between ``mongod`` servers, connect to the target ``mongod`` and use the
|
|
||||||
:meth:`~pymongo.database.Database.command` method::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> client = MongoClient('target.example.com')
|
|
||||||
>>> client.admin.command('copydb',
|
|
||||||
fromdb='source_db_name',
|
|
||||||
todb='target_db_name')
|
|
||||||
|
|
||||||
To copy from a different mongod server that is not password-protected::
|
|
||||||
|
|
||||||
>>> client.admin.command('copydb',
|
|
||||||
fromdb='source_db_name',
|
|
||||||
todb='target_db_name',
|
|
||||||
fromhost='source.example.com')
|
|
||||||
|
|
||||||
If the target server is password-protected, authenticate to the "admin"
|
|
||||||
database::
|
|
||||||
|
|
||||||
>>> client = MongoClient('target.example.com',
|
|
||||||
... username='administrator',
|
|
||||||
... password='pwd')
|
|
||||||
>>> client.admin.command('copydb',
|
|
||||||
fromdb='source_db_name',
|
|
||||||
todb='target_db_name',
|
|
||||||
fromhost='source.example.com')
|
|
||||||
|
|
||||||
See the :doc:`authentication examples </examples/authentication>`.
|
|
||||||
|
|
||||||
If the **source** server is password-protected, use the `copyDatabase
|
|
||||||
function in the mongo shell`_.
|
|
||||||
|
|
||||||
Versions of PyMongo before 3.0 included a ``copy_database`` helper method,
|
|
||||||
but it has been removed.
|
|
||||||
|
|
||||||
.. _copyDatabase function in the mongo shell:
|
|
||||||
https://mongodb.com/docs/manual/reference/method/db.copyDatabase/
|
|
||||||
|
|
||||||
.. _Copy a Database:
|
|
||||||
https://www.mongodb.com/docs/database-tools/mongodump/mongodump-examples/#copy-and-clone-databases
|
|
||||||
@ -1,436 +0,0 @@
|
|||||||
Custom Type Example
|
|
||||||
===================
|
|
||||||
|
|
||||||
This is an example of using a custom type with PyMongo. The example here shows
|
|
||||||
how to subclass :class:`~bson.codec_options.TypeCodec` to write a type
|
|
||||||
codec, which is used to populate a :class:`~bson.codec_options.TypeRegistry`.
|
|
||||||
The type registry can then be used to create a custom-type-aware
|
|
||||||
:class:`~pymongo.collection.Collection`. Read and write operations
|
|
||||||
issued against the resulting collection object transparently manipulate
|
|
||||||
documents as they are saved to or retrieved from MongoDB.
|
|
||||||
|
|
||||||
|
|
||||||
Setting Up
|
|
||||||
----------
|
|
||||||
|
|
||||||
We'll start by getting a clean database to use for the example:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> client = MongoClient()
|
|
||||||
>>> client.drop_database("custom_type_example")
|
|
||||||
>>> db = client.custom_type_example
|
|
||||||
|
|
||||||
|
|
||||||
Since the purpose of the example is to demonstrate working with custom types,
|
|
||||||
we'll need a custom data type to use. For this example, we will be working with
|
|
||||||
the :py:class:`~decimal.Decimal` type from Python's standard library. Since the
|
|
||||||
BSON library's :class:`~bson.decimal128.Decimal128` type (that implements
|
|
||||||
the IEEE 754 decimal128 decimal-based floating-point numbering format) is
|
|
||||||
distinct from Python's built-in :py:class:`~decimal.Decimal` type, attempting
|
|
||||||
to save an instance of ``Decimal`` with PyMongo, results in an
|
|
||||||
:exc:`~bson.errors.InvalidDocument` exception.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from decimal import Decimal
|
|
||||||
>>> num = Decimal("45.321")
|
|
||||||
>>> db.test.insert_one({"num": num})
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
bson.errors.InvalidDocument: cannot encode object: Decimal('45.321'), of type: <class 'decimal.Decimal'>
|
|
||||||
|
|
||||||
|
|
||||||
.. _custom-type-type-codec:
|
|
||||||
|
|
||||||
The :class:`~bson.codec_options.TypeCodec` Class
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. versionadded:: 3.8
|
|
||||||
|
|
||||||
In order to encode a custom type, we must first define a **type codec** for
|
|
||||||
that type. A type codec describes how an instance of a custom type can be
|
|
||||||
*transformed* to and/or from one of the types :mod:`~bson` already understands.
|
|
||||||
Depending on the desired functionality, users must choose from the following
|
|
||||||
base classes when defining type codecs:
|
|
||||||
|
|
||||||
* :class:`~bson.codec_options.TypeEncoder`: subclass this to define a codec that
|
|
||||||
encodes a custom Python type to a known BSON type. Users must implement the
|
|
||||||
``python_type`` property/attribute and the ``transform_python`` method.
|
|
||||||
* :class:`~bson.codec_options.TypeDecoder`: subclass this to define a codec that
|
|
||||||
decodes a specified BSON type into a custom Python type. Users must implement
|
|
||||||
the ``bson_type`` property/attribute and the ``transform_bson`` method.
|
|
||||||
* :class:`~bson.codec_options.TypeCodec`: subclass this to define a codec that
|
|
||||||
can both encode and decode a custom type. Users must implement the
|
|
||||||
``python_type`` and ``bson_type`` properties/attributes, as well as the
|
|
||||||
``transform_python`` and ``transform_bson`` methods.
|
|
||||||
|
|
||||||
|
|
||||||
The type codec for our custom type simply needs to define how a
|
|
||||||
:py:class:`~decimal.Decimal` instance can be converted into a
|
|
||||||
:class:`~bson.decimal128.Decimal128` instance and vice-versa. Since we are
|
|
||||||
interested in both encoding and decoding our custom type, we use the
|
|
||||||
``TypeCodec`` base class to define our codec:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from bson.decimal128 import Decimal128
|
|
||||||
>>> from bson.codec_options import TypeCodec
|
|
||||||
>>> class DecimalCodec(TypeCodec):
|
|
||||||
... python_type = Decimal # the Python type acted upon by this type codec
|
|
||||||
... bson_type = Decimal128 # the BSON type acted upon by this type codec
|
|
||||||
... def transform_python(self, value):
|
|
||||||
... """Function that transforms a custom type value into a type
|
|
||||||
... that BSON can encode."""
|
|
||||||
... return Decimal128(value)
|
|
||||||
... def transform_bson(self, value):
|
|
||||||
... """Function that transforms a vanilla BSON type value into our
|
|
||||||
... custom type."""
|
|
||||||
... return value.to_decimal()
|
|
||||||
...
|
|
||||||
>>> decimal_codec = DecimalCodec()
|
|
||||||
|
|
||||||
|
|
||||||
.. _custom-type-type-registry:
|
|
||||||
|
|
||||||
The :class:`~bson.codec_options.TypeRegistry` Class
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. versionadded:: 3.8
|
|
||||||
|
|
||||||
Before we can begin encoding and decoding our custom type objects, we must
|
|
||||||
first inform PyMongo about the corresponding codec. This is done by creating
|
|
||||||
a :class:`~bson.codec_options.TypeRegistry` instance:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from bson.codec_options import TypeRegistry
|
|
||||||
>>> type_registry = TypeRegistry([decimal_codec])
|
|
||||||
|
|
||||||
|
|
||||||
Note that type registries can be instantiated with any number of type codecs.
|
|
||||||
Once instantiated, registries are immutable and the only way to add codecs
|
|
||||||
to a registry is to create a new one.
|
|
||||||
|
|
||||||
|
|
||||||
Putting It Together
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Finally, we can define a :class:`~bson.codec_options.CodecOptions` instance
|
|
||||||
with our ``type_registry`` and use it to get a
|
|
||||||
:class:`~pymongo.collection.Collection` object that understands the
|
|
||||||
:py:class:`~decimal.Decimal` data type:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from bson.codec_options import CodecOptions
|
|
||||||
>>> codec_options = CodecOptions(type_registry=type_registry)
|
|
||||||
>>> collection = db.get_collection("test", codec_options=codec_options)
|
|
||||||
|
|
||||||
|
|
||||||
Now, we can seamlessly encode and decode instances of
|
|
||||||
:py:class:`~decimal.Decimal`:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> collection.insert_one({"num": Decimal("45.321")})
|
|
||||||
InsertOneResult(ObjectId('...'), acknowledged=True)
|
|
||||||
>>> mydoc = collection.find_one()
|
|
||||||
>>> import pprint
|
|
||||||
>>> pprint.pprint(mydoc)
|
|
||||||
{'_id': ObjectId('...'), 'num': Decimal('45.321')}
|
|
||||||
|
|
||||||
|
|
||||||
We can see what's actually being saved to the database by creating a fresh
|
|
||||||
collection object without the customized codec options and using that to query
|
|
||||||
MongoDB:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> vanilla_collection = db.get_collection("test")
|
|
||||||
>>> pprint.pprint(vanilla_collection.find_one())
|
|
||||||
{'_id': ObjectId('...'), 'num': Decimal128('45.321')}
|
|
||||||
|
|
||||||
|
|
||||||
Encoding Subtypes
|
|
||||||
^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Consider the situation where, in addition to encoding
|
|
||||||
:py:class:`~decimal.Decimal`, we also need to encode a type that subclasses
|
|
||||||
``Decimal``. PyMongo does this automatically for types that inherit from
|
|
||||||
Python types that are BSON-encodable by default, but the type codec system
|
|
||||||
described above does not offer the same flexibility.
|
|
||||||
|
|
||||||
Consider this subtype of ``Decimal`` that has a method to return its value as
|
|
||||||
an integer:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> class DecimalInt(Decimal):
|
|
||||||
... def my_method(self):
|
|
||||||
... """Method implementing some custom logic."""
|
|
||||||
... return int(self)
|
|
||||||
...
|
|
||||||
|
|
||||||
If we try to save an instance of this type without first registering a type
|
|
||||||
codec for it, we get an error:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> collection.insert_one({"num": DecimalInt("45.321")})
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
bson.errors.InvalidDocument: cannot encode object: Decimal('45.321'), of type: <class 'decimal.Decimal'>
|
|
||||||
|
|
||||||
In order to proceed further, we must define a type codec for ``DecimalInt``.
|
|
||||||
This is trivial to do since the same transformation as the one used for
|
|
||||||
``Decimal`` is adequate for encoding ``DecimalInt`` as well:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> class DecimalIntCodec(DecimalCodec):
|
|
||||||
... @property
|
|
||||||
... def python_type(self):
|
|
||||||
... """The Python type acted upon by this type codec."""
|
|
||||||
... return DecimalInt
|
|
||||||
...
|
|
||||||
>>> decimalint_codec = DecimalIntCodec()
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
No attempt is made to modify decoding behavior because without additional
|
|
||||||
information, it is impossible to discern which incoming
|
|
||||||
:class:`~bson.decimal128.Decimal128` value needs to be decoded as ``Decimal``
|
|
||||||
and which needs to be decoded as ``DecimalInt``. This example only considers
|
|
||||||
the situation where a user wants to *encode* documents containing either
|
|
||||||
of these types.
|
|
||||||
|
|
||||||
After creating a new codec options object and using it to get a collection
|
|
||||||
object, we can seamlessly encode instances of ``DecimalInt``:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> type_registry = TypeRegistry([decimal_codec, decimalint_codec])
|
|
||||||
>>> codec_options = CodecOptions(type_registry=type_registry)
|
|
||||||
>>> collection = db.get_collection("test", codec_options=codec_options)
|
|
||||||
>>> collection.drop()
|
|
||||||
>>> collection.insert_one({"num": DecimalInt("45.321")})
|
|
||||||
InsertOneResult(ObjectId('...'), acknowledged=True)
|
|
||||||
>>> mydoc = collection.find_one()
|
|
||||||
>>> pprint.pprint(mydoc)
|
|
||||||
{'_id': ObjectId('...'), 'num': Decimal('45.321')}
|
|
||||||
|
|
||||||
Note that the ``transform_bson`` method of the base codec class results in
|
|
||||||
these values being decoded as ``Decimal`` (and not ``DecimalInt``).
|
|
||||||
|
|
||||||
|
|
||||||
.. _decoding-binary-types:
|
|
||||||
|
|
||||||
Decoding :class:`~bson.binary.Binary` Types
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
The decoding treatment of :class:`~bson.binary.Binary` types having
|
|
||||||
``subtype = 0`` by the :mod:`bson` module varies slightly depending on the
|
|
||||||
version of the Python runtime in use. This must be taken into account while
|
|
||||||
writing a ``TypeDecoder`` that modifies how this datatype is decoded.
|
|
||||||
|
|
||||||
On Python 3.x, :class:`~bson.binary.Binary` data (``subtype = 0``) is decoded
|
|
||||||
as a ``bytes`` instance:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> # On Python 3.x.
|
|
||||||
>>> from bson.binary import Binary
|
|
||||||
>>> newcoll = db.get_collection("new")
|
|
||||||
>>> newcoll.insert_one({"_id": 1, "data": Binary(b"123", subtype=0)})
|
|
||||||
>>> doc = newcoll.find_one()
|
|
||||||
>>> type(doc["data"])
|
|
||||||
bytes
|
|
||||||
|
|
||||||
|
|
||||||
On Python 2.7.x, the same data is decoded as a :class:`~bson.binary.Binary`
|
|
||||||
instance:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> # On Python 2.7.x
|
|
||||||
>>> newcoll = db.get_collection("new")
|
|
||||||
>>> doc = newcoll.find_one()
|
|
||||||
>>> type(doc["data"])
|
|
||||||
bson.binary.Binary
|
|
||||||
|
|
||||||
|
|
||||||
As a consequence of this disparity, users must set the ``bson_type`` attribute
|
|
||||||
on their :class:`~bson.codec_options.TypeDecoder` classes differently,
|
|
||||||
depending on the python version in use.
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
For codebases requiring compatibility with both Python 2 and 3, type
|
|
||||||
decoders will have to be registered for both possible ``bson_type`` values.
|
|
||||||
|
|
||||||
|
|
||||||
.. _fallback-encoder-callable:
|
|
||||||
|
|
||||||
The ``fallback_encoder`` Callable
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
.. versionadded:: 3.8
|
|
||||||
|
|
||||||
|
|
||||||
In addition to type codecs, users can also register a callable to encode types
|
|
||||||
that BSON doesn't recognize and for which no type codec has been registered.
|
|
||||||
This callable is the **fallback encoder** and like the ``transform_python``
|
|
||||||
method, it accepts an unencodable value as a parameter and returns a
|
|
||||||
BSON-encodable value. The following fallback encoder encodes python's
|
|
||||||
:py:class:`~decimal.Decimal` type to a :class:`~bson.decimal128.Decimal128`:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> def fallback_encoder(value):
|
|
||||||
... if isinstance(value, Decimal):
|
|
||||||
... return Decimal128(value)
|
|
||||||
... return value
|
|
||||||
...
|
|
||||||
|
|
||||||
After declaring the callback, we must create a type registry and codec options
|
|
||||||
with this fallback encoder before it can be used for initializing a collection:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> type_registry = TypeRegistry(fallback_encoder=fallback_encoder)
|
|
||||||
>>> codec_options = CodecOptions(type_registry=type_registry)
|
|
||||||
>>> collection = db.get_collection("test", codec_options=codec_options)
|
|
||||||
>>> collection.drop()
|
|
||||||
|
|
||||||
We can now seamlessly encode instances of :py:class:`~decimal.Decimal`:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> collection.insert_one({"num": Decimal("45.321")})
|
|
||||||
InsertOneResult(ObjectId('...'), acknowledged=True)
|
|
||||||
>>> mydoc = collection.find_one()
|
|
||||||
>>> pprint.pprint(mydoc)
|
|
||||||
{'_id': ObjectId('...'), 'num': Decimal128('45.321')}
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Fallback encoders are invoked *after* attempts to encode the given value
|
|
||||||
with standard BSON encoders and any configured type encoders have failed.
|
|
||||||
Therefore, in a type registry configured with a type encoder and fallback
|
|
||||||
encoder that both target the same custom type, the behavior specified in
|
|
||||||
the type encoder will prevail.
|
|
||||||
|
|
||||||
|
|
||||||
Because fallback encoders don't need to declare the types that they encode
|
|
||||||
beforehand, they can be used to support interesting use-cases that cannot be
|
|
||||||
serviced by ``TypeEncoder``. One such use-case is described in the next
|
|
||||||
section.
|
|
||||||
|
|
||||||
|
|
||||||
Encoding Unknown Types
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
In this example, we demonstrate how a fallback encoder can be used to save
|
|
||||||
arbitrary objects to the database. We will use the the standard library's
|
|
||||||
:py:mod:`pickle` module to serialize the unknown types and so naturally, this
|
|
||||||
approach only works for types that are picklable.
|
|
||||||
|
|
||||||
We start by defining some arbitrary custom types:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class MyStringType(object):
|
|
||||||
def __init__(self, value):
|
|
||||||
self.__value = value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "MyStringType('%s')" % (self.__value,)
|
|
||||||
|
|
||||||
|
|
||||||
class MyNumberType(object):
|
|
||||||
def __init__(self, value):
|
|
||||||
self.__value = value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "MyNumberType(%s)" % (self.__value,)
|
|
||||||
|
|
||||||
We also define a fallback encoder that pickles whatever objects it receives
|
|
||||||
and returns them as :class:`~bson.binary.Binary` instances with a custom
|
|
||||||
subtype. The custom subtype, in turn, allows us to write a TypeDecoder that
|
|
||||||
identifies pickled artifacts upon retrieval and transparently decodes them
|
|
||||||
back into Python objects:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import pickle
|
|
||||||
from bson.binary import Binary, USER_DEFINED_SUBTYPE
|
|
||||||
|
|
||||||
|
|
||||||
def fallback_pickle_encoder(value):
|
|
||||||
return Binary(pickle.dumps(value), USER_DEFINED_SUBTYPE)
|
|
||||||
|
|
||||||
|
|
||||||
class PickledBinaryDecoder(TypeDecoder):
|
|
||||||
bson_type = Binary
|
|
||||||
|
|
||||||
def transform_bson(self, value):
|
|
||||||
if value.subtype == USER_DEFINED_SUBTYPE:
|
|
||||||
return pickle.loads(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The above example is written assuming the use of Python 3. If you are using
|
|
||||||
Python 2, ``bson_type`` must be set to ``Binary``. See the
|
|
||||||
:ref:`decoding-binary-types` section for a detailed explanation.
|
|
||||||
|
|
||||||
|
|
||||||
Finally, we create a ``CodecOptions`` instance:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
codec_options = CodecOptions(
|
|
||||||
type_registry=TypeRegistry(
|
|
||||||
[PickledBinaryDecoder()], fallback_encoder=fallback_pickle_encoder
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
We can now round trip our custom objects to MongoDB:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
collection = db.get_collection("test_fe", codec_options=codec_options)
|
|
||||||
collection.insert_one(
|
|
||||||
{"_id": 1, "str": MyStringType("hello world"), "num": MyNumberType(2)}
|
|
||||||
)
|
|
||||||
mydoc = collection.find_one()
|
|
||||||
assert isinstance(mydoc["str"], MyStringType)
|
|
||||||
assert isinstance(mydoc["num"], MyNumberType)
|
|
||||||
|
|
||||||
|
|
||||||
Limitations
|
|
||||||
-----------
|
|
||||||
|
|
||||||
PyMongo's type codec and fallback encoder features have the following
|
|
||||||
limitations:
|
|
||||||
|
|
||||||
#. Users cannot customize the encoding behavior of Python types that PyMongo
|
|
||||||
already understands like ``int`` and ``str`` (the 'built-in types').
|
|
||||||
Attempting to instantiate a type registry with one or more codecs that act
|
|
||||||
upon a built-in type results in a ``TypeError``. This limitation extends
|
|
||||||
to all subtypes of the standard types.
|
|
||||||
#. Chaining type encoders is not supported. A custom type value, once
|
|
||||||
transformed by a codec's ``transform_python`` method, *must* result in a
|
|
||||||
type that is either BSON-encodable by default, or can be
|
|
||||||
transformed by the fallback encoder into something BSON-encodable--it
|
|
||||||
*cannot* be transformed a second time by a different type codec.
|
|
||||||
#. The :meth:`~pymongo.database.Database.command` method does not apply the
|
|
||||||
user's TypeDecoders while decoding the command response document.
|
|
||||||
#. :mod:`gridfs` does not apply custom type encoding or decoding to any
|
|
||||||
documents received from or to returned to the user.
|
|
||||||
@ -1,177 +0,0 @@
|
|||||||
Datetimes and Timezones
|
|
||||||
=======================
|
|
||||||
|
|
||||||
.. testsetup::
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from bson.codec_options import CodecOptions
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client.drop_database("dt_example")
|
|
||||||
db = client.dt_example
|
|
||||||
|
|
||||||
These examples show how to handle Python :class:`datetime.datetime` objects
|
|
||||||
correctly in PyMongo.
|
|
||||||
|
|
||||||
Basic Usage
|
|
||||||
-----------
|
|
||||||
|
|
||||||
PyMongo uses :class:`datetime.datetime` objects for representing dates and times
|
|
||||||
in MongoDB documents. Because MongoDB assumes that dates and times are in UTC,
|
|
||||||
care should be taken to ensure that dates and times written to the database
|
|
||||||
reflect UTC. For example, the following code stores the current UTC date and
|
|
||||||
time into MongoDB:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> result = db.objects.insert_one(
|
|
||||||
... {"last_modified": datetime.datetime.now(tz=datetime.timezone.utc)}
|
|
||||||
... )
|
|
||||||
|
|
||||||
Always use :meth:`datetime.datetime.now(tz=datetime.timezone.utc)`, which explicitly returns the current time in
|
|
||||||
UTC, instead of :meth:`datetime.datetime.now`, with no arguments, which returns the current local
|
|
||||||
time. Avoid doing this:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> result = db.objects.insert_one({"last_modified": datetime.datetime.now()})
|
|
||||||
|
|
||||||
The value for ``last_modified`` is very different between these two examples, even
|
|
||||||
though both documents were stored at around the same local time. This will be
|
|
||||||
confusing to the application that reads them:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> [doc["last_modified"] for doc in db.objects.find()] # doctest: +SKIP
|
|
||||||
[datetime.datetime(2015, 7, 8, 18, 17, 28, 324000),
|
|
||||||
datetime.datetime(2015, 7, 8, 11, 17, 42, 911000)]
|
|
||||||
|
|
||||||
:class:`bson.codec_options.CodecOptions` has a ``tz_aware`` option that enables
|
|
||||||
"aware" :class:`datetime.datetime` objects, i.e., datetimes that know what
|
|
||||||
timezone they're in. By default, PyMongo retrieves naive datetimes:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> result = db.tzdemo.insert_one({"date": datetime.datetime(2002, 10, 27, 6, 0, 0)})
|
|
||||||
>>> db.tzdemo.find_one()["date"]
|
|
||||||
datetime.datetime(2002, 10, 27, 6, 0)
|
|
||||||
>>> options = CodecOptions(tz_aware=True)
|
|
||||||
>>> db.get_collection("tzdemo", codec_options=options).find_one()["date"] # doctest: +SKIP
|
|
||||||
datetime.datetime(2002, 10, 27, 6, 0,
|
|
||||||
tzinfo=<bson.tz_util.FixedOffset object at 0x10583a050>)
|
|
||||||
|
|
||||||
Saving Datetimes with Timezones
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
When storing :class:`datetime.datetime` objects that specify a timezone
|
|
||||||
(i.e. they have a ``tzinfo`` property that isn't ``None``), PyMongo will convert
|
|
||||||
those datetimes to UTC automatically:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from zoneinfo import ZoneInfo
|
|
||||||
>>> from datetime import datetime
|
|
||||||
>>> aware_datetime = datetime(2002, 10, 27, 6, 0, 0, tzinfo=ZoneInfo("US/Pacific"))
|
|
||||||
>>> result = db.times.insert_one({"date": aware_datetime})
|
|
||||||
>>> db.times.find_one()["date"]
|
|
||||||
datetime.datetime(2002, 10, 27, 14, 0)
|
|
||||||
|
|
||||||
Reading Time
|
|
||||||
------------
|
|
||||||
|
|
||||||
As previously mentioned, by default all :class:`datetime.datetime` objects
|
|
||||||
returned by PyMongo will be naive but reflect UTC (i.e. the time as stored in
|
|
||||||
MongoDB). By setting the ``tz_aware`` option on
|
|
||||||
:class:`~bson.codec_options.CodecOptions`, :class:`datetime.datetime` objects
|
|
||||||
will be timezone-aware and have a ``tzinfo`` property that reflects the UTC
|
|
||||||
timezone.
|
|
||||||
|
|
||||||
PyMongo 3.1 introduced a ``tzinfo`` property that can be set on
|
|
||||||
:class:`~bson.codec_options.CodecOptions` to convert :class:`datetime.datetime`
|
|
||||||
objects to local time automatically. For example, if we wanted to read all times
|
|
||||||
out of MongoDB in US/Pacific time:
|
|
||||||
|
|
||||||
>>> from bson.codec_options import CodecOptions
|
|
||||||
>>> db.times.find_one()['date']
|
|
||||||
datetime.datetime(2002, 10, 27, 14, 0)
|
|
||||||
>>> aware_times = db.times.with_options(codec_options=CodecOptions(
|
|
||||||
... tz_aware=True,
|
|
||||||
... tzinfo=ZoneInfo("US/Pacific")))
|
|
||||||
>>> result = aware_times.find_one()['date']
|
|
||||||
datetime.datetime(2002, 10, 27, 6, 0, # doctest: +NORMALIZE_WHITESPACE
|
|
||||||
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)
|
|
||||||
|
|
||||||
.. _handling-out-of-range-datetimes:
|
|
||||||
|
|
||||||
Handling out of range datetimes
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Python's :class:`~datetime.datetime` can only represent datetimes within the
|
|
||||||
range allowed by
|
|
||||||
:attr:`~datetime.datetime.min` and :attr:`~datetime.datetime.max`, whereas
|
|
||||||
the range of datetimes allowed in BSON can represent any 64-bit number
|
|
||||||
of milliseconds from the Unix epoch. To deal with this, we can use the
|
|
||||||
:class:`bson.datetime_ms.DatetimeMS` object, which is a wrapper for the
|
|
||||||
:class:`int` built-in.
|
|
||||||
|
|
||||||
To decode UTC datetime values as :class:`~bson.datetime_ms.DatetimeMS`,
|
|
||||||
:class:`~bson.codec_options.CodecOptions` should have its
|
|
||||||
``datetime_conversion`` parameter set to one of the options available in
|
|
||||||
:class:`bson.datetime_ms.DatetimeConversion`. These include
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME`,
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_MS`,
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_AUTO`,
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_CLAMP`.
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME` is the default
|
|
||||||
option and has the behavior of raising an :class:`~builtin.OverflowError` upon
|
|
||||||
attempting to decode an out-of-range date.
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_MS` will only return
|
|
||||||
:class:`~bson.datetime_ms.DatetimeMS` objects, regardless of whether the
|
|
||||||
represented datetime is in- or out-of-range:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from datetime import datetime
|
|
||||||
>>> from bson import encode, decode
|
|
||||||
>>> from bson.datetime_ms import DatetimeMS
|
|
||||||
>>> from bson.codec_options import CodecOptions, DatetimeConversion
|
|
||||||
>>> x = encode({"x": datetime(1970, 1, 1)})
|
|
||||||
>>> codec_ms = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_MS)
|
|
||||||
>>> decode(x, codec_options=codec_ms)
|
|
||||||
{'x': DatetimeMS(0)}
|
|
||||||
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_AUTO` will return
|
|
||||||
:class:`~datetime.datetime` if the underlying UTC datetime is within range,
|
|
||||||
or :class:`~bson.datetime_ms.DatetimeMS` if the underlying datetime
|
|
||||||
cannot be represented using the builtin Python :class:`~datetime.datetime`:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> x = encode({"x": datetime(1970, 1, 1)})
|
|
||||||
>>> y = encode({"x": DatetimeMS(-(2**62))})
|
|
||||||
>>> codec_auto = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_AUTO)
|
|
||||||
>>> decode(x, codec_options=codec_auto)
|
|
||||||
{'x': datetime.datetime(1970, 1, 1, 0, 0)}
|
|
||||||
>>> decode(y, codec_options=codec_auto)
|
|
||||||
{'x': DatetimeMS(-4611686018427387904)}
|
|
||||||
|
|
||||||
:attr:`~bson.datetime_ms.DatetimeConversion.DATETIME_CLAMP` will clamp
|
|
||||||
resulting :class:`~datetime.datetime` objects to be within
|
|
||||||
:attr:`~datetime.datetime.min` and :attr:`~datetime.datetime.max`
|
|
||||||
(trimmed to ``999000`` microseconds):
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> x = encode({"x": DatetimeMS(2**62)})
|
|
||||||
>>> y = encode({"x": DatetimeMS(-(2**62))})
|
|
||||||
>>> codec_clamp = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_CLAMP)
|
|
||||||
>>> decode(x, codec_options=codec_clamp)
|
|
||||||
{'x': datetime.datetime(9999, 12, 31, 23, 59, 59, 999000)}
|
|
||||||
>>> decode(y, codec_options=codec_clamp)
|
|
||||||
{'x': datetime.datetime(1, 1, 1, 0, 0)}
|
|
||||||
|
|
||||||
:class:`~bson.datetime_ms.DatetimeMS` objects have support for rich comparison
|
|
||||||
methods against other instances of :class:`~bson.datetime_ms.DatetimeMS`.
|
|
||||||
They can also be converted to :class:`~datetime.datetime` objects with
|
|
||||||
:meth:`~bson.datetime_ms.DatetimeMS.to_datetime()`.
|
|
||||||
@ -1,844 +0,0 @@
|
|||||||
.. _In-Use Encryption:
|
|
||||||
|
|
||||||
In-Use Encryption
|
|
||||||
=================
|
|
||||||
|
|
||||||
.. _Client-Side Field Level Encryption:
|
|
||||||
|
|
||||||
Client-Side Field Level Encryption
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
New in MongoDB 4.2, client-side field level encryption allows an application
|
|
||||||
to encrypt specific data fields in addition to pre-existing MongoDB
|
|
||||||
encryption features such as `Encryption at Rest
|
|
||||||
<https://dochub.mongodb.org/core/security-encryption-at-rest>`_ and
|
|
||||||
`TLS/SSL (Transport Encryption)
|
|
||||||
<https://dochub.mongodb.org/core/security-tls-transport-encryption>`_.
|
|
||||||
|
|
||||||
With field level encryption, applications can encrypt fields in documents
|
|
||||||
*prior* to transmitting data over the wire to the server. Client-side field
|
|
||||||
level encryption supports workloads where applications must guarantee that
|
|
||||||
unauthorized parties, including server administrators, cannot read the
|
|
||||||
encrypted data.
|
|
||||||
|
|
||||||
.. seealso:: The MongoDB documentation on `Client Side Field Level Encryption <https://dochub.mongodb.org/core/client-side-field-level-encryption>`_.
|
|
||||||
|
|
||||||
Dependencies
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
To get started using client-side field level encryption in your project,
|
|
||||||
you will need to install the
|
|
||||||
`pymongocrypt <https://pypi.org/project/pymongocrypt/>`_ and
|
|
||||||
`pymongo-auth-aws <https://pypi.org/project/pymongo-auth-aws/>`_ libraries
|
|
||||||
as well as the driver itself. Install both the driver and a compatible
|
|
||||||
version of the dependencies like this::
|
|
||||||
|
|
||||||
$ python -m pip install 'pymongo[encryption]'
|
|
||||||
|
|
||||||
Note that installing on Linux requires pip 19 or later for manylinux2010 wheel
|
|
||||||
support. For more information about installing pymongocrypt see
|
|
||||||
`the installation instructions on the project's PyPI page
|
|
||||||
<https://pypi.org/project/pymongocrypt/>`_.
|
|
||||||
|
|
||||||
Additionally, either `crypt_shared`_ or `mongocryptd`_ are required in order
|
|
||||||
to use *automatic* client-side encryption.
|
|
||||||
|
|
||||||
crypt_shared
|
|
||||||
````````````
|
|
||||||
|
|
||||||
The Automatic Encryption Shared Library (crypt_shared) provides the same
|
|
||||||
functionality as `mongocryptd`_, but does not require you to spawn another
|
|
||||||
process to perform automatic encryption.
|
|
||||||
|
|
||||||
By default, pymongo attempts to load crypt_shared from the system and if
|
|
||||||
found uses it automatically. To load crypt_shared from another location,
|
|
||||||
use the ``crypt_shared_lib_path`` argument to
|
|
||||||
:class:`~pymongo.encryption_options.AutoEncryptionOpts`.
|
|
||||||
If pymongo cannot load crypt_shared it will attempt to fallback to using
|
|
||||||
`mongocryptd`_ by default. Set ``crypt_shared_lib_required=True`` to make
|
|
||||||
the app always use crypt_shared and fail if it could not be loaded.
|
|
||||||
|
|
||||||
For detailed installation instructions see
|
|
||||||
`the MongoDB documentation on Automatic Encryption Shared Library
|
|
||||||
<https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/shared-library>`_.
|
|
||||||
|
|
||||||
mongocryptd
|
|
||||||
```````````
|
|
||||||
|
|
||||||
The ``mongocryptd`` binary is required for automatic client-side encryption
|
|
||||||
and is included as a component in the `MongoDB Enterprise Server package
|
|
||||||
<https://dochub.mongodb.org/core/install-mongodb-enterprise>`_.
|
|
||||||
For detailed installation instructions see
|
|
||||||
`the MongoDB documentation on mongocryptd
|
|
||||||
<https://dochub.mongodb.org/core/client-side-field-level-encryption-mongocryptd>`_.
|
|
||||||
|
|
||||||
``mongocryptd`` performs the following:
|
|
||||||
|
|
||||||
- Parses the automatic encryption rules specified to the database connection.
|
|
||||||
If the JSON schema contains invalid automatic encryption syntax or any
|
|
||||||
document validation syntax, ``mongocryptd`` returns an error.
|
|
||||||
- Uses the specified automatic encryption rules to mark fields in read and
|
|
||||||
write operations for encryption.
|
|
||||||
- Rejects read/write operations that may return unexpected or incorrect results
|
|
||||||
when applied to an encrypted field. For supported and unsupported operations,
|
|
||||||
see `Read/Write Support with Automatic Field Level Encryption
|
|
||||||
<https://dochub.mongodb.org/core/client-side-field-level-encryption-read-write-support>`_.
|
|
||||||
|
|
||||||
A MongoClient configured with auto encryption will automatically spawn the
|
|
||||||
``mongocryptd`` process from the application's ``PATH``. Applications can
|
|
||||||
control the spawning behavior as part of the automatic encryption options.
|
|
||||||
For example to set the path to the ``mongocryptd`` process::
|
|
||||||
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(
|
|
||||||
...,
|
|
||||||
mongocryptd_spawn_path='/path/to/mongocryptd')
|
|
||||||
|
|
||||||
To control the logging output of ``mongocryptd`` pass options using
|
|
||||||
``mongocryptd_spawn_args``::
|
|
||||||
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(
|
|
||||||
...,
|
|
||||||
mongocryptd_spawn_args=['--logpath=/path/to/mongocryptd.log', '--logappend'])
|
|
||||||
|
|
||||||
If your application wishes to manage the ``mongocryptd`` process manually,
|
|
||||||
it is possible to disable spawning ``mongocryptd``::
|
|
||||||
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(
|
|
||||||
...,
|
|
||||||
mongocryptd_bypass_spawn=True,
|
|
||||||
# URI of the local ``mongocryptd`` process.
|
|
||||||
mongocryptd_uri='mongodb://localhost:27020')
|
|
||||||
|
|
||||||
``mongocryptd`` is only responsible for supporting automatic client-side field
|
|
||||||
level encryption and does not itself perform any encryption or decryption.
|
|
||||||
|
|
||||||
.. _automatic-client-side-encryption:
|
|
||||||
|
|
||||||
Automatic Client-Side Field Level Encryption
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Automatic client-side field level encryption is enabled by creating a
|
|
||||||
:class:`~pymongo.mongo_client.MongoClient` with the ``auto_encryption_opts``
|
|
||||||
option set to an instance of
|
|
||||||
:class:`~pymongo.encryption_options.AutoEncryptionOpts`. The following
|
|
||||||
examples show how to setup automatic client-side field level encryption
|
|
||||||
using :class:`~pymongo.encryption.ClientEncryption` to create a new
|
|
||||||
encryption data key.
|
|
||||||
|
|
||||||
.. note:: Automatic client-side field level encryption requires MongoDB >=4.2
|
|
||||||
enterprise or a MongoDB >=4.2 Atlas cluster. The community version of the
|
|
||||||
server supports automatic decryption as well as
|
|
||||||
:ref:`explicit-client-side-encryption`.
|
|
||||||
|
|
||||||
Providing Local Automatic Encryption Rules
|
|
||||||
``````````````````````````````````````````
|
|
||||||
|
|
||||||
The following example shows how to specify automatic encryption rules via the
|
|
||||||
``schema_map`` option. The automatic encryption rules are expressed using a
|
|
||||||
`strict subset of the JSON Schema syntax
|
|
||||||
<https://dochub.mongodb.org/core/client-side-field-level-encryption-automatic-encryption-rules>`_.
|
|
||||||
|
|
||||||
Supplying a ``schema_map`` provides more security than relying on
|
|
||||||
JSON Schemas obtained from the server. It protects against a
|
|
||||||
malicious server advertising a false JSON Schema, which could trick
|
|
||||||
the client into sending unencrypted data that should be encrypted.
|
|
||||||
|
|
||||||
JSON Schemas supplied in the ``schema_map`` only apply to configuring
|
|
||||||
automatic client-side field level encryption. Other validation
|
|
||||||
rules in the JSON schema will not be enforced by the driver and
|
|
||||||
will result in an error.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
from bson.codec_options import CodecOptions
|
|
||||||
from bson import json_util
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import Algorithm, ClientEncryption
|
|
||||||
from pymongo.encryption_options import AutoEncryptionOpts
|
|
||||||
|
|
||||||
|
|
||||||
def create_json_schema_file(kms_providers, key_vault_namespace, key_vault_client):
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
kms_providers,
|
|
||||||
key_vault_namespace,
|
|
||||||
key_vault_client,
|
|
||||||
# The CodecOptions class used for encrypting and decrypting.
|
|
||||||
# This should be the same CodecOptions instance you have configured
|
|
||||||
# on MongoClient, Database, or Collection. We will not be calling
|
|
||||||
# encrypt() or decrypt() in this example so we can use any
|
|
||||||
# CodecOptions.
|
|
||||||
CodecOptions(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new data key and json schema for the encryptedField.
|
|
||||||
# https://dochub.mongodb.org/core/client-side-field-level-encryption-automatic-encryption-rules
|
|
||||||
data_key_id = client_encryption.create_data_key(
|
|
||||||
"local", key_alt_names=["pymongo_encryption_example_1"]
|
|
||||||
)
|
|
||||||
schema = {
|
|
||||||
"properties": {
|
|
||||||
"encryptedField": {
|
|
||||||
"encrypt": {
|
|
||||||
"keyId": [data_key_id],
|
|
||||||
"bsonType": "string",
|
|
||||||
"algorithm": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bsonType": "object",
|
|
||||||
}
|
|
||||||
# Use CANONICAL_JSON_OPTIONS so that other drivers and tools will be
|
|
||||||
# able to parse the MongoDB extended JSON file.
|
|
||||||
json_schema_string = json_util.dumps(
|
|
||||||
schema, json_options=json_util.CANONICAL_JSON_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
with open("jsonSchema.json", "w") as file:
|
|
||||||
file.write(json_schema_string)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# The MongoDB namespace (db.collection) used to store the
|
|
||||||
# encrypted documents in this example.
|
|
||||||
encrypted_namespace = "test.coll"
|
|
||||||
|
|
||||||
# This must be the same master key that was used to create
|
|
||||||
# the encryption key.
|
|
||||||
local_master_key = os.urandom(96)
|
|
||||||
kms_providers = {"local": {"key": local_master_key}}
|
|
||||||
|
|
||||||
# The MongoDB namespace (db.collection) used to store
|
|
||||||
# the encryption data keys.
|
|
||||||
key_vault_namespace = "encryption.__pymongoTestKeyVault"
|
|
||||||
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
|
|
||||||
|
|
||||||
# The MongoClient used to access the key vault (key_vault_namespace).
|
|
||||||
key_vault_client = MongoClient()
|
|
||||||
key_vault = key_vault_client[key_vault_db_name][key_vault_coll_name]
|
|
||||||
# Ensure that two data keys cannot share the same keyAltName.
|
|
||||||
key_vault.drop()
|
|
||||||
key_vault.create_index(
|
|
||||||
"keyAltNames",
|
|
||||||
unique=True,
|
|
||||||
partialFilterExpression={"keyAltNames": {"$exists": True}},
|
|
||||||
)
|
|
||||||
|
|
||||||
create_json_schema_file(kms_providers, key_vault_namespace, key_vault_client)
|
|
||||||
|
|
||||||
# Load the JSON Schema and construct the local schema_map option.
|
|
||||||
with open("jsonSchema.json", "r") as file:
|
|
||||||
json_schema_string = file.read()
|
|
||||||
json_schema = json_util.loads(json_schema_string)
|
|
||||||
schema_map = {encrypted_namespace: json_schema}
|
|
||||||
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(
|
|
||||||
kms_providers, key_vault_namespace, schema_map=schema_map
|
|
||||||
)
|
|
||||||
|
|
||||||
client = MongoClient(auto_encryption_opts=auto_encryption_opts)
|
|
||||||
db_name, coll_name = encrypted_namespace.split(".", 1)
|
|
||||||
coll = client[db_name][coll_name]
|
|
||||||
# Clear old data
|
|
||||||
coll.drop()
|
|
||||||
|
|
||||||
coll.insert_one({"encryptedField": "123456789"})
|
|
||||||
print("Decrypted document: %s" % (coll.find_one(),))
|
|
||||||
unencrypted_coll = MongoClient()[db_name][coll_name]
|
|
||||||
print("Encrypted document: %s" % (unencrypted_coll.find_one(),))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
Server-Side Field Level Encryption Enforcement
|
|
||||||
``````````````````````````````````````````````
|
|
||||||
|
|
||||||
MongoDB >=4.2 servers supports using schema validation to enforce encryption
|
|
||||||
of specific fields in a collection. This schema validation will prevent an
|
|
||||||
application from inserting unencrypted values for any fields marked with the
|
|
||||||
``"encrypt"`` JSON schema keyword.
|
|
||||||
|
|
||||||
The following example shows how to setup automatic client-side field level
|
|
||||||
encryption using
|
|
||||||
:class:`~pymongo.encryption.ClientEncryption` to create a new encryption
|
|
||||||
data key and create a collection with the
|
|
||||||
`Automatic Encryption JSON Schema Syntax
|
|
||||||
<https://dochub.mongodb.org/core/client-side-field-level-encryption-automatic-encryption-rules>`_:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from bson.codec_options import CodecOptions
|
|
||||||
from bson.binary import STANDARD
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import Algorithm, ClientEncryption
|
|
||||||
from pymongo.encryption_options import AutoEncryptionOpts
|
|
||||||
from pymongo.errors import OperationFailure
|
|
||||||
from pymongo.write_concern import WriteConcern
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# The MongoDB namespace (db.collection) used to store the
|
|
||||||
# encrypted documents in this example.
|
|
||||||
encrypted_namespace = "test.coll"
|
|
||||||
|
|
||||||
# This must be the same master key that was used to create
|
|
||||||
# the encryption key.
|
|
||||||
local_master_key = os.urandom(96)
|
|
||||||
kms_providers = {"local": {"key": local_master_key}}
|
|
||||||
|
|
||||||
# The MongoDB namespace (db.collection) used to store
|
|
||||||
# the encryption data keys.
|
|
||||||
key_vault_namespace = "encryption.__pymongoTestKeyVault"
|
|
||||||
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
|
|
||||||
|
|
||||||
# The MongoClient used to access the key vault (key_vault_namespace).
|
|
||||||
key_vault_client = MongoClient()
|
|
||||||
key_vault = key_vault_client[key_vault_db_name][key_vault_coll_name]
|
|
||||||
# Ensure that two data keys cannot share the same keyAltName.
|
|
||||||
key_vault.drop()
|
|
||||||
key_vault.create_index(
|
|
||||||
"keyAltNames",
|
|
||||||
unique=True,
|
|
||||||
partialFilterExpression={"keyAltNames": {"$exists": True}},
|
|
||||||
)
|
|
||||||
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
kms_providers,
|
|
||||||
key_vault_namespace,
|
|
||||||
key_vault_client,
|
|
||||||
# The CodecOptions class used for encrypting and decrypting.
|
|
||||||
# This should be the same CodecOptions instance you have configured
|
|
||||||
# on MongoClient, Database, or Collection. We will not be calling
|
|
||||||
# encrypt() or decrypt() in this example so we can use any
|
|
||||||
# CodecOptions.
|
|
||||||
CodecOptions(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new data key and json schema for the encryptedField.
|
|
||||||
data_key_id = client_encryption.create_data_key(
|
|
||||||
"local", key_alt_names=["pymongo_encryption_example_2"]
|
|
||||||
)
|
|
||||||
json_schema = {
|
|
||||||
"properties": {
|
|
||||||
"encryptedField": {
|
|
||||||
"encrypt": {
|
|
||||||
"keyId": [data_key_id],
|
|
||||||
"bsonType": "string",
|
|
||||||
"algorithm": Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bsonType": "object",
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(kms_providers, key_vault_namespace)
|
|
||||||
client = MongoClient(auto_encryption_opts=auto_encryption_opts)
|
|
||||||
db_name, coll_name = encrypted_namespace.split(".", 1)
|
|
||||||
db = client[db_name]
|
|
||||||
# Clear old data
|
|
||||||
db.drop_collection(coll_name)
|
|
||||||
# Create the collection with the encryption JSON Schema.
|
|
||||||
db.create_collection(
|
|
||||||
coll_name,
|
|
||||||
# uuid_representation=STANDARD is required to ensure that any
|
|
||||||
# UUIDs in the $jsonSchema document are encoded to BSON Binary
|
|
||||||
# with the standard UUID subtype 4. This is only needed when
|
|
||||||
# running the "create" collection command with an encryption
|
|
||||||
# JSON Schema.
|
|
||||||
codec_options=CodecOptions(uuid_representation=STANDARD),
|
|
||||||
write_concern=WriteConcern(w="majority"),
|
|
||||||
validator={"$jsonSchema": json_schema},
|
|
||||||
)
|
|
||||||
coll = client[db_name][coll_name]
|
|
||||||
|
|
||||||
coll.insert_one({"encryptedField": "123456789"})
|
|
||||||
print("Decrypted document: %s" % (coll.find_one(),))
|
|
||||||
unencrypted_coll = MongoClient()[db_name][coll_name]
|
|
||||||
print("Encrypted document: %s" % (unencrypted_coll.find_one(),))
|
|
||||||
try:
|
|
||||||
unencrypted_coll.insert_one({"encryptedField": "123456789"})
|
|
||||||
except OperationFailure as exc:
|
|
||||||
print("Unencrypted insert failed: %s" % (exc.details,))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
.. _explicit-client-side-encryption:
|
|
||||||
|
|
||||||
Explicit Encryption
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Explicit encryption is a MongoDB community feature and does not use the
|
|
||||||
``mongocryptd`` process. Explicit encryption is provided by the
|
|
||||||
:class:`~pymongo.encryption.ClientEncryption` class, for example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import Algorithm, ClientEncryption
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# This must be the same master key that was used to create
|
|
||||||
# the encryption key.
|
|
||||||
local_master_key = os.urandom(96)
|
|
||||||
kms_providers = {"local": {"key": local_master_key}}
|
|
||||||
|
|
||||||
# The MongoDB namespace (db.collection) used to store
|
|
||||||
# the encryption data keys.
|
|
||||||
key_vault_namespace = "encryption.__pymongoTestKeyVault"
|
|
||||||
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
|
|
||||||
|
|
||||||
# The MongoClient used to read/write application data.
|
|
||||||
client = MongoClient()
|
|
||||||
coll = client.test.coll
|
|
||||||
# Clear old data
|
|
||||||
coll.drop()
|
|
||||||
|
|
||||||
# Set up the key vault (key_vault_namespace) for this example.
|
|
||||||
key_vault = client[key_vault_db_name][key_vault_coll_name]
|
|
||||||
# Ensure that two data keys cannot share the same keyAltName.
|
|
||||||
key_vault.drop()
|
|
||||||
key_vault.create_index(
|
|
||||||
"keyAltNames",
|
|
||||||
unique=True,
|
|
||||||
partialFilterExpression={"keyAltNames": {"$exists": True}},
|
|
||||||
)
|
|
||||||
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
kms_providers,
|
|
||||||
key_vault_namespace,
|
|
||||||
# The MongoClient to use for reading/writing to the key vault.
|
|
||||||
# This can be the same MongoClient used by the main application.
|
|
||||||
client,
|
|
||||||
# The CodecOptions class used for encrypting and decrypting.
|
|
||||||
# This should be the same CodecOptions instance you have configured
|
|
||||||
# on MongoClient, Database, or Collection.
|
|
||||||
coll.codec_options,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new data key for the encryptedField.
|
|
||||||
data_key_id = client_encryption.create_data_key(
|
|
||||||
"local", key_alt_names=["pymongo_encryption_example_3"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Explicitly encrypt a field:
|
|
||||||
encrypted_field = client_encryption.encrypt(
|
|
||||||
"123456789",
|
|
||||||
Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
|
|
||||||
key_id=data_key_id,
|
|
||||||
)
|
|
||||||
coll.insert_one({"encryptedField": encrypted_field})
|
|
||||||
doc = coll.find_one()
|
|
||||||
print("Encrypted document: %s" % (doc,))
|
|
||||||
|
|
||||||
# Explicitly decrypt the field:
|
|
||||||
doc["encryptedField"] = client_encryption.decrypt(doc["encryptedField"])
|
|
||||||
print("Decrypted document: %s" % (doc,))
|
|
||||||
|
|
||||||
# Cleanup resources.
|
|
||||||
client_encryption.close()
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
Explicit Encryption with Automatic Decryption
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Although automatic encryption requires MongoDB >=4.2 enterprise or a
|
|
||||||
MongoDB >=4.2 Atlas cluster, automatic *decryption* is supported for all users.
|
|
||||||
To configure automatic *decryption* without automatic *encryption* set
|
|
||||||
``bypass_auto_encryption=True`` in
|
|
||||||
:class:`~pymongo.encryption_options.AutoEncryptionOpts`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import Algorithm, ClientEncryption
|
|
||||||
from pymongo.encryption_options import AutoEncryptionOpts
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# This must be the same master key that was used to create
|
|
||||||
# the encryption key.
|
|
||||||
local_master_key = os.urandom(96)
|
|
||||||
kms_providers = {"local": {"key": local_master_key}}
|
|
||||||
|
|
||||||
# The MongoDB namespace (db.collection) used to store
|
|
||||||
# the encryption data keys.
|
|
||||||
key_vault_namespace = "encryption.__pymongoTestKeyVault"
|
|
||||||
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
|
|
||||||
|
|
||||||
# bypass_auto_encryption=True disable automatic encryption but keeps
|
|
||||||
# the automatic _decryption_ behavior. bypass_auto_encryption will
|
|
||||||
# also disable spawning mongocryptd.
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(
|
|
||||||
kms_providers, key_vault_namespace, bypass_auto_encryption=True
|
|
||||||
)
|
|
||||||
|
|
||||||
client = MongoClient(auto_encryption_opts=auto_encryption_opts)
|
|
||||||
coll = client.test.coll
|
|
||||||
# Clear old data
|
|
||||||
coll.drop()
|
|
||||||
|
|
||||||
# Set up the key vault (key_vault_namespace) for this example.
|
|
||||||
key_vault = client[key_vault_db_name][key_vault_coll_name]
|
|
||||||
# Ensure that two data keys cannot share the same keyAltName.
|
|
||||||
key_vault.drop()
|
|
||||||
key_vault.create_index(
|
|
||||||
"keyAltNames",
|
|
||||||
unique=True,
|
|
||||||
partialFilterExpression={"keyAltNames": {"$exists": True}},
|
|
||||||
)
|
|
||||||
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
kms_providers,
|
|
||||||
key_vault_namespace,
|
|
||||||
# The MongoClient to use for reading/writing to the key vault.
|
|
||||||
# This can be the same MongoClient used by the main application.
|
|
||||||
client,
|
|
||||||
# The CodecOptions class used for encrypting and decrypting.
|
|
||||||
# This should be the same CodecOptions instance you have configured
|
|
||||||
# on MongoClient, Database, or Collection.
|
|
||||||
coll.codec_options,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new data key for the encryptedField.
|
|
||||||
data_key_id = client_encryption.create_data_key(
|
|
||||||
"local", key_alt_names=["pymongo_encryption_example_4"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Explicitly encrypt a field:
|
|
||||||
encrypted_field = client_encryption.encrypt(
|
|
||||||
"123456789",
|
|
||||||
Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
|
|
||||||
key_alt_name="pymongo_encryption_example_4",
|
|
||||||
)
|
|
||||||
coll.insert_one({"encryptedField": encrypted_field})
|
|
||||||
# Automatically decrypts any encrypted fields.
|
|
||||||
doc = coll.find_one()
|
|
||||||
print("Decrypted document: %s" % (doc,))
|
|
||||||
unencrypted_coll = MongoClient().test.coll
|
|
||||||
print("Encrypted document: %s" % (unencrypted_coll.find_one(),))
|
|
||||||
|
|
||||||
# Cleanup resources.
|
|
||||||
client_encryption.close()
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
.. _CSFLE on-demand credentials:
|
|
||||||
|
|
||||||
|
|
||||||
CSFLE on-demand credentials
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
``pymongocrypt`` 1.4 adds support for fetching on-demand KMS credentials for
|
|
||||||
AWS, GCP, and Azure cloud environments.
|
|
||||||
|
|
||||||
To enable the driver's behavior to obtain credentials from the environment, add the appropriate key ("aws", "gcp", or "azure") with an empty map to
|
|
||||||
"kms_providers" in either :class:`~pymongo.encryption_options.AutoEncryptionOpts` or :class:`~pymongo.encryption.ClientEncryption` options.
|
|
||||||
|
|
||||||
An application using AWS credentials would look like:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import ClientEncryption
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
# The empty dictionary enables on-demand credentials.
|
|
||||||
kms_providers={"aws": {}},
|
|
||||||
key_vault_namespace="keyvault.datakeys",
|
|
||||||
key_vault_client=client,
|
|
||||||
codec_options=client.codec_options,
|
|
||||||
)
|
|
||||||
master_key = {
|
|
||||||
"region": "us-east-1",
|
|
||||||
"key": ("arn:aws:kms:us-east-1:123456789:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0"),
|
|
||||||
}
|
|
||||||
client_encryption.create_data_key("aws", master_key)
|
|
||||||
|
|
||||||
The above will enable the same behavior of obtaining AWS credentials from the environment as is used for :ref:`MONGODB-AWS` authentication, including the
|
|
||||||
caching to avoid rate limiting.
|
|
||||||
|
|
||||||
An application using GCP credentials would look like:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import ClientEncryption
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
# The empty dictionary enables on-demand credentials.
|
|
||||||
kms_providers={"gcp": {}},
|
|
||||||
key_vault_namespace="keyvault.datakeys",
|
|
||||||
key_vault_client=client,
|
|
||||||
codec_options=client.codec_options,
|
|
||||||
)
|
|
||||||
master_key = {
|
|
||||||
"projectId": "my-project",
|
|
||||||
"location": "global",
|
|
||||||
"keyRing": "key-ring-csfle",
|
|
||||||
"keyName": "key-name-csfle",
|
|
||||||
}
|
|
||||||
client_encryption.create_data_key("gcp", master_key)
|
|
||||||
|
|
||||||
The driver will query the `VM instance metadata <https://cloud.google.com/compute/docs/metadata/querying-metadata>`_ to obtain credentials.
|
|
||||||
|
|
||||||
An application using Azure credentials would look like, this time using
|
|
||||||
:class:`~pymongo.encryption_options.AutoEncryptionOpts`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption_options import AutoEncryptionOpts
|
|
||||||
|
|
||||||
# The empty dictionary enables on-demand credentials.
|
|
||||||
kms_providers = ({"azure": {}},)
|
|
||||||
key_vault_namespace = "keyvault.datakeys"
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(kms_providers, key_vault_namespace)
|
|
||||||
client = MongoClient(auto_encryption_opts=auto_encryption_opts)
|
|
||||||
coll = client.test.coll
|
|
||||||
coll.insert_one({"encryptedField": "123456789"})
|
|
||||||
|
|
||||||
The driver will `acquire an access token <https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token>`_ from the Azure VM.
|
|
||||||
|
|
||||||
.. _Queryable Encryption:
|
|
||||||
|
|
||||||
Queryable Encryption
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. _automatic-queryable-client-side-encryption:
|
|
||||||
|
|
||||||
Automatic Queryable Encryption
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Automatic Queryable Encryption requires MongoDB 7.0+ Enterprise or a MongoDB 7.0+ Atlas cluster.
|
|
||||||
|
|
||||||
Queryable Encryption is the second version of Client-Side Field Level Encryption.
|
|
||||||
Data is encrypted client-side. Queryable Encryption supports indexed encrypted fields,
|
|
||||||
which are further processed server-side.
|
|
||||||
|
|
||||||
Automatic encryption in Queryable Encryption is configured with an ``encrypted_fields`` mapping,
|
|
||||||
as demonstrated by the following example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
from bson.codec_options import CodecOptions
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import Algorithm, ClientEncryption, QueryType
|
|
||||||
from pymongo.encryption_options import AutoEncryptionOpts
|
|
||||||
|
|
||||||
local_master_key = os.urandom(96)
|
|
||||||
kms_providers = {"local": {"key": local_master_key}}
|
|
||||||
key_vault_namespace = "keyvault.datakeys"
|
|
||||||
key_vault_client = MongoClient()
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
kms_providers, key_vault_namespace, key_vault_client, CodecOptions()
|
|
||||||
)
|
|
||||||
key_vault = key_vault_client["keyvault"]["datakeys"]
|
|
||||||
key_vault.drop()
|
|
||||||
# Ensure that two data keys cannot share the same keyAltName.
|
|
||||||
key_vault.create_index(
|
|
||||||
"keyAltNames",
|
|
||||||
unique=True,
|
|
||||||
partialFilterExpression={"keyAltNames": {"$exists": True}},
|
|
||||||
)
|
|
||||||
key1_id = client_encryption.create_data_key("local", key_alt_names=["firstName"])
|
|
||||||
key2_id = client_encryption.create_data_key("local", key_alt_names=["lastName"])
|
|
||||||
|
|
||||||
encrypted_fields_map = {
|
|
||||||
"default.encryptedCollection": {
|
|
||||||
"escCollection": "encryptedCollection.esc",
|
|
||||||
"ecocCollection": "encryptedCollection.ecoc",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"path": "firstName",
|
|
||||||
"bsonType": "string",
|
|
||||||
"keyId": key1_id,
|
|
||||||
"queries": [{"queryType": "equality"}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "lastName",
|
|
||||||
"bsonType": "string",
|
|
||||||
"keyId": key2_id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_encryption_opts = AutoEncryptionOpts(
|
|
||||||
kms_providers,
|
|
||||||
key_vault_namespace,
|
|
||||||
encrypted_fields_map=encrypted_fields_map,
|
|
||||||
)
|
|
||||||
client = MongoClient(auto_encryption_opts=auto_encryption_opts)
|
|
||||||
client.default.drop_collection("encryptedCollection")
|
|
||||||
coll = client.default.create_collection("encryptedCollection")
|
|
||||||
coll.insert_one({"_id": 1, "firstName": "Jane", "lastName": "Doe"})
|
|
||||||
docs = list(coll.find({"firstName": "Jane"}))
|
|
||||||
print(docs)
|
|
||||||
|
|
||||||
In the above example, the ``firstName`` and ``lastName`` fields are
|
|
||||||
automatically encrypted and decrypted.
|
|
||||||
|
|
||||||
Explicit Queryable Encryption
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Explicit Queryable Encryption requires MongoDB 7.0+.
|
|
||||||
|
|
||||||
Queryable Encryption is the second version of Client-Side Field Level Encryption.
|
|
||||||
Data is encrypted client-side. Queryable Encryption supports indexed encrypted fields,
|
|
||||||
which are further processed server-side.
|
|
||||||
|
|
||||||
Explicit encryption in Queryable Encryption is performed using the ``encrypt`` and ``decrypt``
|
|
||||||
methods. Automatic encryption (to allow the ``find_one`` to automatically decrypt) is configured
|
|
||||||
using an ``encrypted_fields`` mapping, as demonstrated by the following example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pymongo import MongoClient
|
|
||||||
from pymongo.encryption import (
|
|
||||||
Algorithm,
|
|
||||||
AutoEncryptionOpts,
|
|
||||||
ClientEncryption,
|
|
||||||
QueryType,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# This must be the same master key that was used to create
|
|
||||||
# the encryption key.
|
|
||||||
local_master_key = os.urandom(96)
|
|
||||||
kms_providers = {"local": {"key": local_master_key}}
|
|
||||||
|
|
||||||
# The MongoDB namespace (db.collection) used to store
|
|
||||||
# the encryption data keys.
|
|
||||||
key_vault_namespace = "encryption.__pymongoTestKeyVault"
|
|
||||||
key_vault_db_name, key_vault_coll_name = key_vault_namespace.split(".", 1)
|
|
||||||
|
|
||||||
# Set up the key vault (key_vault_namespace) for this example.
|
|
||||||
client = MongoClient()
|
|
||||||
key_vault = client[key_vault_db_name][key_vault_coll_name]
|
|
||||||
|
|
||||||
# Ensure that two data keys cannot share the same keyAltName.
|
|
||||||
key_vault.drop()
|
|
||||||
key_vault.create_index(
|
|
||||||
"keyAltNames",
|
|
||||||
unique=True,
|
|
||||||
partialFilterExpression={"keyAltNames": {"$exists": True}},
|
|
||||||
)
|
|
||||||
|
|
||||||
client_encryption = ClientEncryption(
|
|
||||||
kms_providers,
|
|
||||||
key_vault_namespace,
|
|
||||||
# The MongoClient to use for reading/writing to the key vault.
|
|
||||||
# This can be the same MongoClient used by the main application.
|
|
||||||
client,
|
|
||||||
# The CodecOptions class used for encrypting and decrypting.
|
|
||||||
# This should be the same CodecOptions instance you have configured
|
|
||||||
# on MongoClient, Database, or Collection.
|
|
||||||
client.codec_options,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new data key for the encryptedField.
|
|
||||||
indexed_key_id = client_encryption.create_data_key("local")
|
|
||||||
unindexed_key_id = client_encryption.create_data_key("local")
|
|
||||||
|
|
||||||
encrypted_fields = {
|
|
||||||
"escCollection": "enxcol_.default.esc",
|
|
||||||
"ecocCollection": "enxcol_.default.ecoc",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"keyId": indexed_key_id,
|
|
||||||
"path": "encryptedIndexed",
|
|
||||||
"bsonType": "string",
|
|
||||||
"queries": {"queryType": "equality"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"keyId": unindexed_key_id,
|
|
||||||
"path": "encryptedUnindexed",
|
|
||||||
"bsonType": "string",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = AutoEncryptionOpts(
|
|
||||||
{"local": {"key": local_master_key}},
|
|
||||||
key_vault.full_name,
|
|
||||||
bypass_query_analysis=True,
|
|
||||||
key_vault_client=client,
|
|
||||||
)
|
|
||||||
|
|
||||||
# The MongoClient used to read/write application data.
|
|
||||||
encrypted_client = MongoClient(auto_encryption_opts=opts)
|
|
||||||
encrypted_client.drop_database("test")
|
|
||||||
db = encrypted_client.test
|
|
||||||
|
|
||||||
# Create the collection with encrypted fields.
|
|
||||||
coll = db.create_collection("coll", encryptedFields=encrypted_fields)
|
|
||||||
|
|
||||||
# Create and encrypt an indexed and unindexed value.
|
|
||||||
val = "encrypted indexed value"
|
|
||||||
unindexed_val = "encrypted unindexed value"
|
|
||||||
insert_payload_indexed = client_encryption.encrypt(
|
|
||||||
val, Algorithm.INDEXED, indexed_key_id, contention_factor=1
|
|
||||||
)
|
|
||||||
insert_payload_unindexed = client_encryption.encrypt(
|
|
||||||
unindexed_val, Algorithm.UNINDEXED, unindexed_key_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Insert the payloads.
|
|
||||||
coll.insert_one(
|
|
||||||
{
|
|
||||||
"encryptedIndexed": insert_payload_indexed,
|
|
||||||
"encryptedUnindexed": insert_payload_unindexed,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Encrypt our find payload using QueryType.EQUALITY.
|
|
||||||
# The value of "indexed_key_id" must be the same as used to encrypt
|
|
||||||
# the values above.
|
|
||||||
find_payload = client_encryption.encrypt(
|
|
||||||
val,
|
|
||||||
Algorithm.INDEXED,
|
|
||||||
indexed_key_id,
|
|
||||||
query_type=QueryType.EQUALITY,
|
|
||||||
contention_factor=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Find the document we inserted using the encrypted payload.
|
|
||||||
# The returned document is automatically decrypted.
|
|
||||||
doc = coll.find_one({"encryptedIndexed": find_payload})
|
|
||||||
print("Returned document: %s" % (doc,))
|
|
||||||
|
|
||||||
# Cleanup resources.
|
|
||||||
client_encryption.close()
|
|
||||||
encrypted_client.close()
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
Geospatial Indexing Example
|
|
||||||
===========================
|
|
||||||
|
|
||||||
.. testsetup::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client.drop_database("geo_example")
|
|
||||||
|
|
||||||
This example shows how to create and use a :data:`~pymongo.GEO2D`
|
|
||||||
index in PyMongo. To create a spherical (earth-like) geospatial index use :data:`~pymongo.GEOSPHERE` instead.
|
|
||||||
|
|
||||||
.. seealso:: The MongoDB documentation on `Geospatial Indexes <https://dochub.mongodb.org/core/geo>`_.
|
|
||||||
|
|
||||||
Creating a Geospatial Index
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
Creating a geospatial index in pymongo is easy:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient, GEO2D
|
|
||||||
>>> db = MongoClient().geo_example
|
|
||||||
>>> db.places.create_index([("loc", GEO2D)])
|
|
||||||
'loc_2d'
|
|
||||||
|
|
||||||
Inserting Places
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Locations in MongoDB are represented using either embedded documents
|
|
||||||
or lists where the first two elements are coordinates. Here, we'll
|
|
||||||
insert a couple of example locations:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> result = db.places.insert_many(
|
|
||||||
... [{"loc": [2, 5]}, {"loc": [30, 5]}, {"loc": [1, 2]}, {"loc": [4, 4]}]
|
|
||||||
... )
|
|
||||||
>>> result.inserted_ids
|
|
||||||
[ObjectId('...'), ObjectId('...'), ObjectId('...'), ObjectId('...')]
|
|
||||||
|
|
||||||
.. note:: If specifying latitude and longitude coordinates in :data:`~pymongo.GEOSPHERE`, list the **longitude** first and then **latitude**.
|
|
||||||
|
|
||||||
Querying
|
|
||||||
--------
|
|
||||||
|
|
||||||
Using the geospatial index we can find documents near another point:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> import pprint
|
|
||||||
>>> for doc in db.places.find({"loc": {"$near": [3, 6]}}).limit(3):
|
|
||||||
... pprint.pprint(doc)
|
|
||||||
...
|
|
||||||
{'_id': ObjectId('...'), 'loc': [2, 5]}
|
|
||||||
{'_id': ObjectId('...'), 'loc': [4, 4]}
|
|
||||||
{'_id': ObjectId('...'), 'loc': [1, 2]}
|
|
||||||
|
|
||||||
.. note:: If using :data:`pymongo.GEOSPHERE`, using $nearSphere is recommended.
|
|
||||||
|
|
||||||
The $maxDistance operator requires the use of :class:`~bson.son.SON`:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from bson.son import SON
|
|
||||||
>>> query = {"loc": SON([("$near", [3, 6]), ("$maxDistance", 100)])}
|
|
||||||
>>> for doc in db.places.find(query).limit(3):
|
|
||||||
... pprint.pprint(doc)
|
|
||||||
...
|
|
||||||
{'_id': ObjectId('...'), 'loc': [2, 5]}
|
|
||||||
{'_id': ObjectId('...'), 'loc': [4, 4]}
|
|
||||||
{'_id': ObjectId('...'), 'loc': [1, 2]}
|
|
||||||
|
|
||||||
It's also possible to query for all items within a given rectangle
|
|
||||||
(specified by lower-left and upper-right coordinates):
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> query = {"loc": {"$within": {"$box": [[2, 2], [5, 6]]}}}
|
|
||||||
>>> for doc in db.places.find(query).sort("_id"):
|
|
||||||
... pprint.pprint(doc)
|
|
||||||
...
|
|
||||||
{'_id': ObjectId('...'), 'loc': [2, 5]}
|
|
||||||
{'_id': ObjectId('...'), 'loc': [4, 4]}
|
|
||||||
|
|
||||||
Or circle (specified by center point and radius):
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> query = {"loc": {"$within": {"$center": [[0, 0], 6]}}}
|
|
||||||
>>> for doc in db.places.find(query).sort("_id"):
|
|
||||||
... pprint.pprint(doc)
|
|
||||||
...
|
|
||||||
{'_id': ObjectId('...'), 'loc': [2, 5]}
|
|
||||||
{'_id': ObjectId('...'), 'loc': [1, 2]}
|
|
||||||
{'_id': ObjectId('...'), 'loc': [4, 4]}
|
|
||||||
|
|
||||||
geoNear queries are also supported using :class:`~bson.son.SON`::
|
|
||||||
|
|
||||||
>>> from bson.son import SON
|
|
||||||
>>> db.command(SON([('geoNear', 'places'), ('near', [1, 2])]))
|
|
||||||
{'ok': 1.0, 'stats': ...}
|
|
||||||
|
|
||||||
.. warning:: Starting in MongoDB version 4.0, MongoDB deprecates the **geoNear** command. Use one of the following operations instead.
|
|
||||||
|
|
||||||
* $geoNear - aggregation stage.
|
|
||||||
* $near - query operator.
|
|
||||||
* $nearSphere - query operator.
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
Gevent
|
|
||||||
======
|
|
||||||
|
|
||||||
PyMongo supports `Gevent <https://www.gevent.org/>`_. Simply call Gevent's
|
|
||||||
``monkey.patch_all()`` before loading any other modules:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
|
||||||
|
|
||||||
>>> # You must call patch_all() *before* importing any other modules
|
|
||||||
>>> from gevent import monkey
|
|
||||||
>>> _ = monkey.patch_all()
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> client = MongoClient()
|
|
||||||
|
|
||||||
PyMongo uses thread and socket functions from the Python standard library.
|
|
||||||
Gevent's monkey-patching replaces those standard functions so that PyMongo
|
|
||||||
does asynchronous I/O with non-blocking sockets, and schedules operations
|
|
||||||
on greenlets instead of threads.
|
|
||||||
|
|
||||||
Avoid blocking in Hub.join
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
By default, PyMongo uses threads to discover and monitor your servers' topology
|
|
||||||
(see :ref:`health-monitoring`). If you execute ``monkey.patch_all()`` when
|
|
||||||
your application first begins, PyMongo automatically uses greenlets instead
|
|
||||||
of threads.
|
|
||||||
|
|
||||||
When shutting down, if your application calls :meth:`~gevent.hub.Hub.join` on
|
|
||||||
Gevent's :class:`~gevent.hub.Hub` without first terminating these background
|
|
||||||
greenlets, the call to :meth:`~gevent.hub.Hub.join` blocks indefinitely. You
|
|
||||||
therefore **must close or dereference** any active
|
|
||||||
:class:`~pymongo.mongo_client.MongoClient` before exiting.
|
|
||||||
|
|
||||||
An example solution to this issue in some application frameworks is a signal
|
|
||||||
handler to end background greenlets when your application receives SIGHUP:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import signal
|
|
||||||
|
|
||||||
|
|
||||||
def graceful_reload(signum, traceback):
|
|
||||||
"""Explicitly close some global MongoClient object."""
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
|
|
||||||
signal.signal(signal.SIGHUP, graceful_reload)
|
|
||||||
|
|
||||||
Applications using uWSGI prior to 1.9.16 are affected by this issue,
|
|
||||||
or newer uWSGI versions with the ``-gevent-wait-for-hub`` option.
|
|
||||||
See `the uWSGI changelog for details
|
|
||||||
<https://uwsgi-docs.readthedocs.io/en/latest/Changelog-1.9.16.html#important-change-in-the-gevent-plugin-shutdown-reload-procedure>`_.
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
GridFS Example
|
|
||||||
==============
|
|
||||||
|
|
||||||
.. testsetup::
|
|
||||||
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
client = MongoClient()
|
|
||||||
client.drop_database("gridfs_example")
|
|
||||||
|
|
||||||
This example shows how to use :mod:`gridfs` to store large binary
|
|
||||||
objects (e.g. files) in MongoDB.
|
|
||||||
|
|
||||||
.. seealso:: The API docs for :mod:`gridfs`.
|
|
||||||
|
|
||||||
.. seealso:: `This blog post
|
|
||||||
<https://dirolf.com/2010/03/29/new-gridfs-implementation-for-pymongo.html>`_
|
|
||||||
for some motivation behind this API.
|
|
||||||
|
|
||||||
Setup
|
|
||||||
-----
|
|
||||||
|
|
||||||
We start by creating a :class:`~gridfs.GridFS` instance to use:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> import gridfs
|
|
||||||
>>>
|
|
||||||
>>> db = MongoClient().gridfs_example
|
|
||||||
>>> fs = gridfs.GridFS(db)
|
|
||||||
|
|
||||||
Every :class:`~gridfs.GridFS` instance is created with and will
|
|
||||||
operate on a specific :class:`~pymongo.database.Database` instance.
|
|
||||||
|
|
||||||
Saving and Retrieving Data
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
The simplest way to work with :mod:`gridfs` is to use its key/value
|
|
||||||
interface (the :meth:`~gridfs.GridFS.put` and
|
|
||||||
:meth:`~gridfs.GridFS.get` methods). To write data to GridFS, use
|
|
||||||
:meth:`~gridfs.GridFS.put`:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> a = fs.put(b"hello world")
|
|
||||||
|
|
||||||
:meth:`~gridfs.GridFS.put` creates a new file in GridFS, and returns
|
|
||||||
the value of the file document's ``"_id"`` key. Given that ``"_id"``
|
|
||||||
we can use :meth:`~gridfs.GridFS.get` to get back the contents of the
|
|
||||||
file:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> fs.get(a).read()
|
|
||||||
b'hello world'
|
|
||||||
|
|
||||||
:meth:`~gridfs.GridFS.get` returns a file-like object, so we get the
|
|
||||||
file's contents by calling :meth:`~gridfs.grid_file.GridOut.read`.
|
|
||||||
|
|
||||||
In addition to putting a :class:`str` as a GridFS file, we can also
|
|
||||||
put any file-like object (an object with a :meth:`read`
|
|
||||||
method). GridFS will handle reading the file in chunk-sized segments
|
|
||||||
automatically. We can also add additional attributes to the file as
|
|
||||||
keyword arguments:
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> b = fs.put(fs.get(a), filename="foo", bar="baz")
|
|
||||||
>>> out = fs.get(b)
|
|
||||||
>>> out.read()
|
|
||||||
b'hello world'
|
|
||||||
>>> out.filename
|
|
||||||
'foo'
|
|
||||||
>>> out.bar
|
|
||||||
'baz'
|
|
||||||
>>> out.upload_date
|
|
||||||
datetime.datetime(...)
|
|
||||||
|
|
||||||
The attributes we set in :meth:`~gridfs.GridFS.put` are stored in the
|
|
||||||
file document, and retrievable after calling
|
|
||||||
:meth:`~gridfs.GridFS.get`. Some attributes (like ``"filename"``) are
|
|
||||||
special and are defined in the GridFS specification - see that
|
|
||||||
document for more details.
|
|
||||||
@ -1,367 +0,0 @@
|
|||||||
High Availability and PyMongo
|
|
||||||
=============================
|
|
||||||
|
|
||||||
PyMongo makes it easy to write highly available applications whether
|
|
||||||
you use a `single replica set <https://dochub.mongodb.org/core/rs>`_
|
|
||||||
or a `large sharded cluster
|
|
||||||
<https://www.mongodb.com/docs/manual/sharding/>`_.
|
|
||||||
|
|
||||||
Connecting to a Replica Set
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
PyMongo makes working with `replica sets
|
|
||||||
<https://dochub.mongodb.org/core/rs>`_ easy. Here we'll launch a new
|
|
||||||
replica set and show how to handle both initialization and normal
|
|
||||||
connections with PyMongo.
|
|
||||||
|
|
||||||
.. seealso:: The MongoDB documentation on `replication <https://dochub.mongodb.org/core/rs>`_.
|
|
||||||
|
|
||||||
Starting a Replica Set
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The main `replica set documentation
|
|
||||||
<https://dochub.mongodb.org/core/rs>`_ contains extensive information
|
|
||||||
about setting up a new replica set or migrating an existing MongoDB
|
|
||||||
setup, be sure to check that out. Here, we'll just do the bare minimum
|
|
||||||
to get a three node replica set setup locally.
|
|
||||||
|
|
||||||
.. warning:: Replica sets should always use multiple nodes in
|
|
||||||
production - putting all set members on the same physical node is
|
|
||||||
only recommended for testing and development.
|
|
||||||
|
|
||||||
We start three ``mongod`` processes, each on a different port and with
|
|
||||||
a different dbpath, but all using the same replica set name "foo".
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ mkdir -p /data/db0 /data/db1 /data/db2
|
|
||||||
$ mongod --port 27017 --dbpath /data/db0 --replSet foo
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ mongod --port 27018 --dbpath /data/db1 --replSet foo
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ mongod --port 27019 --dbpath /data/db2 --replSet foo
|
|
||||||
|
|
||||||
Initializing the Set
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
At this point all of our nodes are up and running, but the set has yet
|
|
||||||
to be initialized. Until the set is initialized no node will become
|
|
||||||
the primary, and things are essentially "offline".
|
|
||||||
|
|
||||||
To initialize the set we need to connect directly to a single node and run the
|
|
||||||
initiate command using the ``directConnection`` option::
|
|
||||||
|
|
||||||
>>> from pymongo import MongoClient
|
|
||||||
>>> c = MongoClient('localhost', 27017, directConnection=True)
|
|
||||||
|
|
||||||
.. note:: We could have connected to any of the other nodes instead,
|
|
||||||
but only the node we initiate from is allowed to contain any
|
|
||||||
initial data.
|
|
||||||
|
|
||||||
After connecting, we run the initiate command to get things started::
|
|
||||||
|
|
||||||
>>> config = {'_id': 'foo', 'members': [
|
|
||||||
... {'_id': 0, 'host': 'localhost:27017'},
|
|
||||||
... {'_id': 1, 'host': 'localhost:27018'},
|
|
||||||
... {'_id': 2, 'host': 'localhost:27019'}]}
|
|
||||||
>>> c.admin.command("replSetInitiate", config)
|
|
||||||
{'ok': 1.0, ...}
|
|
||||||
|
|
||||||
The three ``mongod`` servers we started earlier will now coordinate
|
|
||||||
and come online as a replica set.
|
|
||||||
|
|
||||||
Connecting to a Replica Set
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The initial connection as made above is a special case for an
|
|
||||||
uninitialized replica set. Normally we'll want to connect
|
|
||||||
differently. A connection to a replica set can be made using the
|
|
||||||
:meth:`~pymongo.mongo_client.MongoClient` constructor, specifying
|
|
||||||
one or more members of the set and optionally the replica set name.
|
|
||||||
Any of the following connects to the replica set we just created::
|
|
||||||
|
|
||||||
>>> MongoClient('localhost')
|
|
||||||
MongoClient(host=['localhost:27017'], ...)
|
|
||||||
>>> MongoClient('localhost', replicaset='foo')
|
|
||||||
MongoClient(host=['localhost:27017'], replicaset='foo', ...)
|
|
||||||
>>> MongoClient('localhost:27018', replicaset='foo')
|
|
||||||
MongoClient(['localhost:27018'], replicaset='foo', ...)
|
|
||||||
>>> MongoClient('localhost', 27019, replicaset='foo')
|
|
||||||
MongoClient(['localhost:27019'], replicaset='foo', ...)
|
|
||||||
>>> MongoClient('mongodb://localhost:27017,localhost:27018/')
|
|
||||||
MongoClient(['localhost:27017', 'localhost:27018'], ...)
|
|
||||||
>>> MongoClient('mongodb://localhost:27017,localhost:27018/?replicaSet=foo')
|
|
||||||
MongoClient(['localhost:27017', 'localhost:27018'], replicaset='foo', ...)
|
|
||||||
|
|
||||||
The addresses passed to :meth:`~pymongo.mongo_client.MongoClient` are called
|
|
||||||
the *seeds*. As long as at least one of the seeds is online, MongoClient
|
|
||||||
discovers all the members in the replica set, and determines which is the
|
|
||||||
current primary and which are secondaries or arbiters. Each seed must be the
|
|
||||||
address of a single mongod. Multihomed and round robin DNS addresses are
|
|
||||||
**not** supported.
|
|
||||||
|
|
||||||
The :class:`~pymongo.mongo_client.MongoClient` constructor is non-blocking:
|
|
||||||
the constructor returns immediately while the client connects to the replica
|
|
||||||
set using background threads. Note how, if you create a client and immediately
|
|
||||||
print the string representation of its
|
|
||||||
:attr:`~pymongo.mongo_client.MongoClient.nodes` attribute, the list may be
|
|
||||||
empty initially. If you wait a moment, MongoClient discovers the whole replica
|
|
||||||
set::
|
|
||||||
|
|
||||||
>>> from time import sleep
|
|
||||||
>>> c = MongoClient(replicaset='foo'); print(c.nodes); sleep(0.1); print(c.nodes)
|
|
||||||
frozenset([])
|
|
||||||
frozenset([('localhost', 27019), ('localhost', 27017), ('localhost', 27018)])
|
|
||||||
|
|
||||||
You need not wait for replica set discovery in your application, however.
|
|
||||||
If you need to do any operation with a MongoClient, such as a
|
|
||||||
:meth:`~pymongo.collection.Collection.find` or an
|
|
||||||
:meth:`~pymongo.collection.Collection.insert_one`, the client waits to discover
|
|
||||||
a suitable member before it attempts the operation.
|
|
||||||
|
|
||||||
Handling Failover
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
When a failover occurs, PyMongo will automatically attempt to find the
|
|
||||||
new primary node and perform subsequent operations on that node. This
|
|
||||||
can't happen completely transparently, however. Here we'll perform an
|
|
||||||
example failover to illustrate how everything behaves. First, we'll
|
|
||||||
connect to the replica set and perform a couple of basic operations::
|
|
||||||
|
|
||||||
>>> db = MongoClient("localhost", replicaSet='foo').test
|
|
||||||
>>> db.test.insert_one({"x": 1}).inserted_id
|
|
||||||
ObjectId('...')
|
|
||||||
>>> db.test.find_one()
|
|
||||||
{'x': 1, '_id': ObjectId('...')}
|
|
||||||
|
|
||||||
By checking the host and port, we can see that we're connected to
|
|
||||||
*localhost:27017*, which is the current primary::
|
|
||||||
|
|
||||||
>>> db.client.address
|
|
||||||
('localhost', 27017)
|
|
||||||
|
|
||||||
Now let's bring down that node and see what happens when we run our
|
|
||||||
query again::
|
|
||||||
|
|
||||||
>>> db.test.find_one()
|
|
||||||
Traceback (most recent call last):
|
|
||||||
pymongo.errors.AutoReconnect: ...
|
|
||||||
|
|
||||||
We get an :class:`~pymongo.errors.AutoReconnect` exception. This means
|
|
||||||
that the driver was not able to connect to the old primary (which
|
|
||||||
makes sense, as we killed the server), but that it will attempt to
|
|
||||||
automatically reconnect on subsequent operations. When this exception
|
|
||||||
is raised our application code needs to decide whether to retry the
|
|
||||||
operation or to simply continue, accepting the fact that the operation
|
|
||||||
might have failed.
|
|
||||||
|
|
||||||
On subsequent attempts to run the query we might continue to see this
|
|
||||||
exception. Eventually, however, the replica set will failover and
|
|
||||||
elect a new primary (this should take no more than a couple of seconds in
|
|
||||||
general). At that point the driver will connect to the new primary and
|
|
||||||
the operation will succeed::
|
|
||||||
|
|
||||||
>>> db.test.find_one()
|
|
||||||
{'x': 1, '_id': ObjectId('...')}
|
|
||||||
>>> db.client.address
|
|
||||||
('localhost', 27018)
|
|
||||||
|
|
||||||
Bring the former primary back up. It will rejoin the set as a secondary.
|
|
||||||
Now we can move to the next section: distributing reads to secondaries.
|
|
||||||
|
|
||||||
.. _secondary-reads:
|
|
||||||
|
|
||||||
Secondary Reads
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
By default an instance of MongoClient sends queries to
|
|
||||||
the primary member of the replica set. To use secondaries for queries
|
|
||||||
we have to change the read preference::
|
|
||||||
|
|
||||||
>>> client = MongoClient(
|
|
||||||
... 'localhost:27017',
|
|
||||||
... replicaSet='foo',
|
|
||||||
... readPreference='secondaryPreferred')
|
|
||||||
>>> client.read_preference
|
|
||||||
SecondaryPreferred(tag_sets=None)
|
|
||||||
|
|
||||||
Now all queries will be sent to the secondary members of the set. If there are
|
|
||||||
no secondary members the primary will be used as a fallback. If you have
|
|
||||||
queries you would prefer to never send to the primary you can specify that
|
|
||||||
using the ``secondary`` read preference.
|
|
||||||
|
|
||||||
By default the read preference of a :class:`~pymongo.database.Database` is
|
|
||||||
inherited from its MongoClient, and the read preference of a
|
|
||||||
:class:`~pymongo.collection.Collection` is inherited from its Database. To use
|
|
||||||
a different read preference use the
|
|
||||||
:meth:`~pymongo.mongo_client.MongoClient.get_database` method, or the
|
|
||||||
:meth:`~pymongo.database.Database.get_collection` method::
|
|
||||||
|
|
||||||
>>> from pymongo import ReadPreference
|
|
||||||
>>> client.read_preference
|
|
||||||
SecondaryPreferred(tag_sets=None)
|
|
||||||
>>> db = client.get_database('test', read_preference=ReadPreference.SECONDARY)
|
|
||||||
>>> db.read_preference
|
|
||||||
Secondary(tag_sets=None)
|
|
||||||
>>> coll = db.get_collection('test', read_preference=ReadPreference.PRIMARY)
|
|
||||||
>>> coll.read_preference
|
|
||||||
Primary()
|
|
||||||
|
|
||||||
You can also change the read preference of an existing
|
|
||||||
:class:`~pymongo.collection.Collection` with the
|
|
||||||
:meth:`~pymongo.collection.Collection.with_options` method::
|
|
||||||
|
|
||||||
>>> coll2 = coll.with_options(read_preference=ReadPreference.NEAREST)
|
|
||||||
>>> coll.read_preference
|
|
||||||
Primary()
|
|
||||||
>>> coll2.read_preference
|
|
||||||
Nearest(tag_sets=None)
|
|
||||||
|
|
||||||
Note that since most database commands can only be sent to the primary of a
|
|
||||||
replica set, the :meth:`~pymongo.database.Database.command` method does not obey
|
|
||||||
the Database's :attr:`~pymongo.database.Database.read_preference`, but you can
|
|
||||||
pass an explicit read preference to the method::
|
|
||||||
|
|
||||||
>>> db.command('dbstats', read_preference=ReadPreference.NEAREST)
|
|
||||||
{...}
|
|
||||||
|
|
||||||
Reads are configured using three options: **read preference**, **tag sets**,
|
|
||||||
and **local threshold**.
|
|
||||||
|
|
||||||
**Read preference**:
|
|
||||||
|
|
||||||
Read preference is configured using one of the classes from
|
|
||||||
:mod:`~pymongo.read_preferences` (:class:`~pymongo.read_preferences.Primary`,
|
|
||||||
:class:`~pymongo.read_preferences.PrimaryPreferred`,
|
|
||||||
:class:`~pymongo.read_preferences.Secondary`,
|
|
||||||
:class:`~pymongo.read_preferences.SecondaryPreferred`, or
|
|
||||||
:class:`~pymongo.read_preferences.Nearest`). For convenience, we also provide
|
|
||||||
:class:`~pymongo.read_preferences.ReadPreference` with the following
|
|
||||||
attributes:
|
|
||||||
|
|
||||||
- ``PRIMARY``: Read from the primary. This is the default read preference,
|
|
||||||
and provides the strongest consistency. If no primary is available, raise
|
|
||||||
:class:`~pymongo.errors.AutoReconnect`.
|
|
||||||
|
|
||||||
- ``PRIMARY_PREFERRED``: Read from the primary if available, otherwise read
|
|
||||||
from a secondary.
|
|
||||||
|
|
||||||
- ``SECONDARY``: Read from a secondary. If no matching secondary is available,
|
|
||||||
raise :class:`~pymongo.errors.AutoReconnect`.
|
|
||||||
|
|
||||||
- ``SECONDARY_PREFERRED``: Read from a secondary if available, otherwise
|
|
||||||
from the primary.
|
|
||||||
|
|
||||||
- ``NEAREST``: Read from any available member.
|
|
||||||
|
|
||||||
**Tag sets**:
|
|
||||||
|
|
||||||
Replica-set members can be `tagged
|
|
||||||
<https://www.mongodb.com/docs/manual/data-center-awareness/>`_ according to any
|
|
||||||
criteria you choose. By default, PyMongo ignores tags when
|
|
||||||
choosing a member to read from, but your read preference can be configured with
|
|
||||||
a ``tag_sets`` parameter. ``tag_sets`` must be a list of dictionaries, each
|
|
||||||
dict providing tag values that the replica set member must match.
|
|
||||||
PyMongo tries each set of tags in turn until it finds a set of
|
|
||||||
tags with at least one matching member. For example, to prefer reads from the
|
|
||||||
New York data center, but fall back to the San Francisco data center, tag your
|
|
||||||
replica set members according to their location and create a
|
|
||||||
MongoClient like so::
|
|
||||||
|
|
||||||
>>> from pymongo.read_preferences import Secondary
|
|
||||||
>>> db = client.get_database(
|
|
||||||
... 'test', read_preference=Secondary([{'dc': 'ny'}, {'dc': 'sf'}]))
|
|
||||||
>>> db.read_preference
|
|
||||||
Secondary(tag_sets=[{'dc': 'ny'}, {'dc': 'sf'}])
|
|
||||||
|
|
||||||
MongoClient tries to find secondaries in New York, then San Francisco,
|
|
||||||
and raises :class:`~pymongo.errors.AutoReconnect` if none are available. As an
|
|
||||||
additional fallback, specify a final, empty tag set, ``{}``, which means "read
|
|
||||||
from any member that matches the mode, ignoring tags."
|
|
||||||
|
|
||||||
See :mod:`~pymongo.read_preferences` for more information.
|
|
||||||
|
|
||||||
.. _distributes reads to secondaries:
|
|
||||||
|
|
||||||
**Local threshold**:
|
|
||||||
|
|
||||||
If multiple members match the read preference and tag sets, PyMongo reads
|
|
||||||
from among the nearest members, chosen according to ping time. By default,
|
|
||||||
only members whose ping times are within 15 milliseconds of the nearest
|
|
||||||
are used for queries. You can choose to distribute reads among members with
|
|
||||||
higher latencies by setting ``localThresholdMS`` to a larger
|
|
||||||
number::
|
|
||||||
|
|
||||||
>>> client = pymongo.MongoClient(
|
|
||||||
... replicaSet='repl0',
|
|
||||||
... readPreference='secondaryPreferred',
|
|
||||||
... localThresholdMS=35)
|
|
||||||
|
|
||||||
In this case, PyMongo distributes reads among matching members within 35
|
|
||||||
milliseconds of the closest member's ping time.
|
|
||||||
|
|
||||||
.. note:: ``localThresholdMS`` is ignored when talking to a
|
|
||||||
replica set *through* a mongos. The equivalent is the localThreshold_ command
|
|
||||||
line option.
|
|
||||||
|
|
||||||
.. _localThreshold: https://mongodb.com/docs/manual/reference/program/mongos/#std-option-mongos.--localThreshold
|
|
||||||
|
|
||||||
.. _health-monitoring:
|
|
||||||
|
|
||||||
Health Monitoring
|
|
||||||
'''''''''''''''''
|
|
||||||
|
|
||||||
When MongoClient is initialized it launches background threads to
|
|
||||||
monitor the replica set for changes in:
|
|
||||||
|
|
||||||
* Health: detect when a member goes down or comes up, or if a different member
|
|
||||||
becomes primary
|
|
||||||
* Configuration: detect when members are added or removed, and detect changes
|
|
||||||
in members' tags
|
|
||||||
* Latency: track a moving average of each member's ping time
|
|
||||||
|
|
||||||
Replica-set monitoring ensures queries are continually routed to the proper
|
|
||||||
members as the state of the replica set changes.
|
|
||||||
|
|
||||||
.. _mongos-load-balancing:
|
|
||||||
|
|
||||||
mongos Load Balancing
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
An instance of :class:`~pymongo.mongo_client.MongoClient` can be configured
|
|
||||||
with a list of addresses of mongos servers:
|
|
||||||
|
|
||||||
>>> client = MongoClient('mongodb://host1,host2,host3')
|
|
||||||
|
|
||||||
Each member of the list must be a single mongos server. Multihomed and round
|
|
||||||
robin DNS addresses are **not** supported. The client continuously
|
|
||||||
monitors all the mongoses' availability, and its network latency to each.
|
|
||||||
|
|
||||||
PyMongo distributes operations evenly among the set of mongoses within its
|
|
||||||
``localThresholdMS`` (similar to how it `distributes reads to secondaries`_
|
|
||||||
in a replica set). By default the threshold is 15 ms.
|
|
||||||
|
|
||||||
The lowest-latency server, and all servers with latencies no more than
|
|
||||||
``localThresholdMS`` beyond the lowest-latency server's, receive
|
|
||||||
operations equally. For example, if we have three mongoses:
|
|
||||||
|
|
||||||
- host1: 20 ms
|
|
||||||
- host2: 35 ms
|
|
||||||
- host3: 40 ms
|
|
||||||
|
|
||||||
By default the ``localThresholdMS`` is 15 ms, so PyMongo uses host1 and host2
|
|
||||||
evenly. It uses host1 because its network latency to the driver is shortest. It
|
|
||||||
uses host2 because its latency is within 15 ms of the lowest-latency server's.
|
|
||||||
But it excuses host3: host3 is 20ms beyond the lowest-latency server.
|
|
||||||
|
|
||||||
If we set ``localThresholdMS`` to 30 ms all servers are within the threshold:
|
|
||||||
|
|
||||||
>>> client = MongoClient('mongodb://host1,host2,host3/?localThresholdMS=30')
|
|
||||||
|
|
||||||
.. warning:: Do **not** connect PyMongo to a pool of mongos instances through a
|
|
||||||
load balancer. A single socket connection must always be routed to the same
|
|
||||||
mongos instance for proper cursor support.
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
Examples
|
|
||||||
========
|
|
||||||
|
|
||||||
The examples in this section are intended to give in depth overviews
|
|
||||||
of how to accomplish specific tasks with MongoDB and PyMongo.
|
|
||||||
|
|
||||||
Unless otherwise noted, all examples assume that a MongoDB instance is
|
|
||||||
running on the default host and port. Assuming you have `downloaded
|
|
||||||
and installed <https://www.mongodb.org/display/DOCS/Getting+Started>`_
|
|
||||||
MongoDB, you can start it like so:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ mongod
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
aggregation
|
|
||||||
authentication
|
|
||||||
collations
|
|
||||||
copydb
|
|
||||||
custom_type
|
|
||||||
bulk
|
|
||||||
client_bulk
|
|
||||||
datetimes
|
|
||||||
geo
|
|
||||||
gevent
|
|
||||||
gridfs
|
|
||||||
high_availability
|
|
||||||
logging
|
|
||||||
mod_wsgi
|
|
||||||
network_compression
|
|
||||||
server_selection
|
|
||||||
tailable
|
|
||||||
timeouts
|
|
||||||
tls
|
|
||||||
type_hints
|
|
||||||
encryption
|
|
||||||
uuid
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
Logging
|
|
||||||
========
|
|
||||||
|
|
||||||
Starting in 4.8, **PyMongo** supports `Python's native logging library <https://docs.python.org/3/howto/logging.html>`_,
|
|
||||||
enabling developers to customize the verbosity of log messages for their applications.
|
|
||||||
|
|
||||||
Components
|
|
||||||
-------------
|
|
||||||
There are currently three different **PyMongo** components with logging support: ``pymongo.command``, ``pymongo.connection``, and ``pymongo.serverSelection``.
|
|
||||||
These components deal with command operations, connection management, and server selection, respectively.
|
|
||||||
Each can be configured separately or they can all be configured together.
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
Currently, the above components each support ``DEBUG`` logging. To enable a single component, do the following::
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.getLogger('pymongo.<componentName>').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
For example, to enable command logging::
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.getLogger('pymongo.command').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
You can also enable all ``DEBUG`` logs at once::
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.getLogger('pymongo').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
Truncation
|
|
||||||
-------------
|
|
||||||
When ``pymongo.command`` debug logs are enabled, every command sent to the server and every response sent back will be included as part of the logs.
|
|
||||||
By default, these command and response documents are truncated after 1000 bytes.
|
|
||||||
|
|
||||||
You can configure a higher truncation limit by setting the ``MONGOB_LOG_MAX_DOCUMENT_LENGTH`` environment variable to your desired length.
|
|
||||||
|
|
||||||
Note that by default, only sensitive authentication command contents are redacted.
|
|
||||||
All commands containing user data will be logged, including the actual contents of your queries.
|
|
||||||
To prevent this behavior, set ``MONGOB_LOG_MAX_DOCUMENT_LENGTH`` to 0. This will omit the command and response bodies from the logs.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------------
|
|
||||||
Here's a simple example that enables ``pymongo.command`` debug logs and performs two database operations::
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import pymongo
|
|
||||||
|
|
||||||
# Automatically writes all logs to stdout
|
|
||||||
logging.basicConfig()
|
|
||||||
logging.getLogger('pymongo.command').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
client = pymongo.MongoClient()
|
|
||||||
client.db.test.insert_one({"x": 1})
|
|
||||||
client.db.test.find_one({"x": 1})
|
|
||||||
---------------------------------
|
|
||||||
DEBUG:pymongo.command:{"clientId": {"$oid": "65cbe82614be1fc2beb4e4a9"}, "message": "Command started", "command": "{\"insert\": \"test\", \"ordered\": true, \"lsid\": {\"id\": {\"$binary\": {\"base64\": \"GI7ubVhPSsWd7+OwHEFx6Q==\", \"subType\": \"04\"}}}, \"$db\": \"db\", \"documents\": [{\"x\": 1, \"_id\": {\"$oid\": \"65cbe82614be1fc2beb4e4aa\"}}]}", "commandName": "insert", "databaseName": "db", "requestId": 1144108930, "operationId": 1144108930, "driverConnectionId": 1, "serverConnectionId": 3554, "serverHost": "localhost", "serverPort": 27017}
|
|
||||||
DEBUG:pymongo.command:{"clientId": {"$oid": "65cbe82614be1fc2beb4e4a9"}, "message": "Command succeeded", "durationMS": 0.515, "reply": "{\"n\": 1, \"ok\": 1.0}", "commandName": "insert", "databaseName": "db", "requestId": 1144108930, "operationId": 1144108930, "driverConnectionId": 1, "serverConnectionId": 3554, "serverHost": "localhost", "serverPort": 27017}
|
|
||||||
DEBUG:pymongo.command:{"clientId": {"$oid": "65cbe82614be1fc2beb4e4a9"}, "message": "Command started", "command": "{\"find\": \"test\", \"filter\": {\"x\": 1}, \"limit\": 1, \"singleBatch\": true, \"lsid\": {\"id\": {\"$binary\": {\"base64\": \"GI7ubVhPSsWd7+OwHEFx6Q==\", \"subType\": \"04\"}}}, \"$db\": \"db\"}", "commandName": "find", "databaseName": "db", "requestId": 470211272, "operationId": 470211272, "driverConnectionId": 1, "serverConnectionId": 3554, "serverHost": "localhost", "serverPort": 27017}
|
|
||||||
DEBUG:pymongo.command:{"clientId": {"$oid": "65cbe82614be1fc2beb4e4a9"}, "message": "Command succeeded", "durationMS": 0.621, "reply": "{\"cursor\": {\"firstBatch\": [{\"_id\": {\"$oid\": \"65cbdf391a957ed280001417\"}, \"x\": 1}], \"ns\": \"db.test\"}, \"ok\": 1.0}", "commandName": "find", "databaseName": "db", "requestId": 470211272, "operationId": 470211272, "driverConnectionId": 1, "serverConnectionId": 3554, "serverHost": "localhost", "serverPort": 27017}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
.. _pymongo-and-mod_wsgi:
|
|
||||||
|
|
||||||
PyMongo and mod_wsgi
|
|
||||||
====================
|
|
||||||
|
|
||||||
To run your application under `mod_wsgi <https://github.com/GrahamDumpleton/mod_wsgi/>`_,
|
|
||||||
follow these guidelines:
|
|
||||||
|
|
||||||
* Run ``mod_wsgi`` in daemon mode with the ``WSGIDaemonProcess`` directive.
|
|
||||||
* Assign each application to a separate daemon with ``WSGIProcessGroup``.
|
|
||||||
* Use ``WSGIApplicationGroup %{GLOBAL}`` to ensure your application is running
|
|
||||||
in the daemon's main Python interpreter, not a sub interpreter.
|
|
||||||
|
|
||||||
For example, this ``mod_wsgi`` configuration ensures an application runs in the
|
|
||||||
main interpreter::
|
|
||||||
|
|
||||||
<VirtualHost *>
|
|
||||||
WSGIDaemonProcess my_process
|
|
||||||
WSGIScriptAlias /my_app /path/to/app.wsgi
|
|
||||||
WSGIProcessGroup my_process
|
|
||||||
WSGIApplicationGroup %{GLOBAL}
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
If you have multiple applications that use PyMongo, put each in a separate
|
|
||||||
daemon, still in the global application group::
|
|
||||||
|
|
||||||
<VirtualHost *>
|
|
||||||
WSGIDaemonProcess my_process
|
|
||||||
WSGIScriptAlias /my_app /path/to/app.wsgi
|
|
||||||
<Location /my_app>
|
|
||||||
WSGIProcessGroup my_process
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
WSGIDaemonProcess my_other_process
|
|
||||||
WSGIScriptAlias /my_other_app /path/to/other_app.wsgi
|
|
||||||
<Location /my_other_app>
|
|
||||||
WSGIProcessGroup my_other_process
|
|
||||||
</Location>
|
|
||||||
|
|
||||||
WSGIApplicationGroup %{GLOBAL}
|
|
||||||
</VirtualHost>
|
|
||||||
|
|
||||||
Background: ``mod_wsgi`` can run in "embedded" mode when only WSGIScriptAlias
|
|
||||||
is set, or "daemon" mode with WSGIDaemonProcess. In daemon mode, ``mod_wsgi``
|
|
||||||
can run your application in the Python main interpreter, or in sub interpreters.
|
|
||||||
The correct way to run a PyMongo application is in daemon mode, using the main
|
|
||||||
interpreter.
|
|
||||||
|
|
||||||
Python C extensions in general have issues running in multiple
|
|
||||||
Python sub interpreters. These difficulties are explained in the documentation for
|
|
||||||
`Py_NewInterpreter <https://docs.python.org/3/c-api/init.html#c.Py_NewInterpreter>`_
|
|
||||||
and in the `Multiple Python Sub Interpreters
|
|
||||||
<https://modwsgi.readthedocs.io/en/master/user-guides/application-issues.html#multiple-python-sub-interpreters>`_
|
|
||||||
section of the ``mod_wsgi`` documentation.
|
|
||||||
|
|
||||||
Beginning with PyMongo 2.7, the C extension for BSON detects when it is running
|
|
||||||
in a sub interpreter and activates a workaround, which adds a small cost to
|
|
||||||
BSON decoding. To avoid this cost, use ``WSGIApplicationGroup %{GLOBAL}`` to
|
|
||||||
ensure your application runs in the main interpreter.
|
|
||||||
|
|
||||||
Since your program runs in the main interpreter it should not share its
|
|
||||||
process with any other applications, lest they interfere with each other's
|
|
||||||
state. Each application should have its own daemon process, as shown in the
|
|
||||||
example above.
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
.. _network-compression-example:
|
|
||||||
|
|
||||||
Network Compression
|
|
||||||
===================
|
|
||||||
|
|
||||||
PyMongo supports network compression where network traffic between the client
|
|
||||||
and MongoDB server are compressed which reduces the amount of data passed
|
|
||||||
over the network. By default no compression is used.
|
|
||||||
|
|
||||||
The driver supports the following algorithms:
|
|
||||||
|
|
||||||
- `snappy <https://pypi.org/project/python-snappy>`_ available in MongoDB 3.4 and later.
|
|
||||||
- :mod:`zlib` available in MongoDB 3.6 and later.
|
|
||||||
- `zstandard <https://pypi.org/project/zstandard/>`_ available in MongoDB 4.2 and later.
|
|
||||||
|
|
||||||
.. note:: snappy and zstandard compression require additional dependencies. See :ref:`optional-deps`.
|
|
||||||
|
|
||||||
Applications can enable wire protocol compression via the ``compressors`` URI and
|
|
||||||
keyword argument to :meth:`~pymongo.mongo_client.MongoClient`. For example::
|
|
||||||
|
|
||||||
>>> client = MongoClient(compressors='zlib')
|
|
||||||
|
|
||||||
When multiple compression algorithms are given, the driver selects the first one in the
|
|
||||||
list supported by the MongoDB instance to which it is connected. For example::
|
|
||||||
|
|
||||||
>>> client = MongoClient(compressors='snappy,zstandard,zlib')
|
|
||||||
|
|
||||||
The ``compressors`` option can also be set via the URI::
|
|
||||||
|
|
||||||
>>> client = MongoClient('mongodb://example.com/?compressors=snappy,zstandard,zlib')
|
|
||||||
|
|
||||||
Additionally, zlib compression allows specifying a compression level with supported values from -1 to 9::
|
|
||||||
|
|
||||||
>>> client = MongoClient(compressors='zlib', zlibCompressionLevel=-1)
|
|
||||||
|
|
||||||
The ``zlibCompressionLevel`` is passed as the ``level`` argument to :func:`zlib.compress`.
|
|
||||||
|
|
||||||
.. seealso:: The MongoDB documentation on `network compression URI options <https://dochub.mongodb.org/core/compression-options>`_.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user