Compare commits

...

373 Commits

Author SHA1 Message Date
Noah Stapp
9a8e34c726
PYTHON-5366 - test_pool_reset waits until Pool.reset() increments gen… (#2797) 2026-05-18 10:29:44 -04:00
Noah Stapp
552b7bf47b
PYTHON-5631 - test_direct_client_maintains_pool_to_arbiter waits inst… (#2798) 2026-05-13 12:20:15 -04:00
Qi Deng
a50550535d
URL-encode client_id in Azure IMDS token request (#2787)
Co-authored-by: Qi Deng <qdeng@aurascape.ai>
2026-05-13 09:33:42 -04:00
Noah Stapp
0adf6df131
PYTHON-5708 - Unskip large encryption tests on mongocryptd (#2793) 2026-05-07 15:23:07 -04:00
Noah Stapp
f145c7db94
PYTHON-5756 - Fix BSON Binary type length bug (#2790) 2026-05-07 15:23:00 -04:00
Noah Stapp
b6bac45c7e
PYTHON-5032 - Use PyErr_GetRaisedException instead of deprecated PyEr… (#2795) 2026-05-07 14:52:19 -04:00
Noah Stapp
8dc7efade2
PYTHON-5821 - Fix ordering issue between event publish and logging for Pool monitoring tests (#2796) 2026-05-07 12:28:15 -04:00
Noah Stapp
f4219bdca2
PYTHON-5817 - Add "Project Structure and Asyncio Considerations" section to CONTRIBUTING.md (#2788)
Co-authored-by: Jib <Jibzade@gmail.com>
2026-05-06 13:28:36 -04:00
Noah Stapp
900d9c7910
PYTHON-5436 - Always include session on getMores if the initial curso… (#2794) 2026-05-06 13:10:13 -04:00
Noah Stapp
575d75f4d3
PYTHON-5813 - Skip QE prefixPreview and suffixPreview tests on server… (#2792) 2026-05-05 13:41:10 -04:00
Noah Stapp
c30eff1291
PYTHON-5811 - Change stream events are not emitted for timeseries as … (#2791) 2026-05-05 11:40:19 -04:00
Jeffrey 'Alex' Clark
e67931dff7
PYTHON-5776 Add documentation comments to justfile recipes (#2784) 2026-04-27 19:45:36 -04:00
mongodb-drivers-pr-bot[bot]
64edd22d73
[Spec Resync] 04-20-2026 (#2766)
Co-authored-by: Cloud User <ec2-user@ip-10-128-20-182.ec2.internal>
Co-authored-by: Jeffrey 'Alex' Clark <aclark@aclark.net>
2026-04-27 15:56:10 -04:00
Jeffrey 'Alex' Clark
b3f1c4befb
[Spec Resync] Remove stale spec patches for closed tickets (#2782) 2026-04-27 15:55:18 -04:00
Jeffrey 'Alex' Clark
ab44a21b46
PYTHON-5780 Increase code coverage for pyopenssl_context.py (#2773) 2026-04-24 09:04:02 -04:00
Jeffrey 'Alex' Clark
a13842f351
PYTHON-5778 Add 100% unit test coverage for event_loggers.py (#2769) 2026-04-21 12:36:48 -04:00
Jeffrey 'Alex' Clark
8363bf60ad
PYTHON-5774 Increase daemon.py coverage to 63% (#2759) 2026-04-20 16:52:36 -04:00
Jeffrey 'Alex' Clark
5406febcd9
Bump version to 4.18.0.dev0 (#2768) 2026-04-20 16:51:01 -04:00
Noah Stapp
3491c08ef6
PYTHON-5801 - Update changelog for 4.17 release (#2762) 2026-04-17 14:17:53 -04:00
Noah Stapp
912ef337f9
PYTHON-5798 - Overload retargeting prose tests do not ensure that sec… (#2760)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 13:32:50 -04:00
Noah Stapp
b4e2c03a92
PYTHON-5800 - Simple collation is included in index information (#2761) 2026-04-16 12:25:23 -04:00
Noah Stapp
f31ba09713
PYTHON-5797 - Add IWM and Overload Error links to changelog (#2757)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-15 14:42:29 -04:00
Noah Stapp
5da91837d4
PYTHON-5794 - Add prose tests to verify correct retry behavior when a… (#2755)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jib <Jibzade@gmail.com>
2026-04-15 14:18:34 -04:00
Copilot
35e51a50f3
Revert "PYTHON-5768 Add AGENTS.md w/copilot instructions" (#2744) (#2754)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aclark4life <72164+aclark4life@users.noreply.github.com>
Co-authored-by: Jib <jib.adegunloye@mongodb.com>
2026-04-15 12:59:12 -04:00
Jeffrey 'Alex' Clark
f41dd5c08b
PYTHON-5772 Increase _gcp_helpers.py coverage (#2749)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 16:53:35 -04:00
Jeffrey 'Alex' Clark
49e7a052e2
PYTHON-5760 Increase _azure_helpers.py coverage (#2747) 2026-04-14 16:24:51 -04:00
Jeffrey 'Alex' Clark
a2b0cd85e3
PYTHON-5795 Fix absolute link to CONTRIBUTING.md in README.md (#2756) 2026-04-14 15:48:00 -04:00
Noah Stapp
e1751ff253
PYTHON-5668 - Merge backpressure branch into mainline (#2729)
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
Co-authored-by: Shane Harvey <shnhrv@gmail.com>
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Albertson <kevin.albertson@mongodb.com>
Co-authored-by: Casey Clements <caseyclements@users.noreply.github.com>
Co-authored-by: Sergey Zelenov <mail@zelenov.su>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 12:25:29 -04:00
Noah Stapp
ee20ef52ec
PYTHON-5791 - test_list_database_names should not check ordering (#2751) 2026-04-13 14:01:14 -04:00
Jeffrey 'Alex' Clark
08b806fd87
PYTHON-5768 Add AGENTS.md w/copilot instructions (#2744)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 12:20:27 -04:00
Jib
db4db928d3
PYTHON-5401: Add AI Generated Contributions Policy (#2696)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-01 11:51:53 -04:00
dependabot[bot]
ee851ba974
Bump astral-sh/setup-uv from 7.3.0 to 7.6.0 in the actions group (#2740)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:50:25 -07:00
mongodb-drivers-pr-bot[bot]
ce416a0944
[Spec Resync] 03-30-2026 (#2741)
Co-authored-by: Cloud User <ec2-user@ip-10-128-20-15.ec2.internal>
Co-authored-by: Iris Ho <iris.ho@mongodb.com>
2026-03-31 11:41:46 -07:00
dependabot[bot]
daba50c797
Bump the actions group across 1 directory with 4 updates (#2736)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 14:56:12 -04:00
Jeffrey 'Alex' Clark
c3428789fb
PYTHON-5766 Add codecov badge to readme (#2737)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 10:55:50 -04:00
Jeffrey 'Alex' Clark
ec9d95413c
PYTHON-5757 Deprecate Python 2 methods in SON (#2732) 2026-03-18 17:46:23 -04:00
Jeffrey 'Alex' Clark
13085ff679
PYTHON-5758 Remove unused validation functions (#2733) 2026-03-18 13:19:18 -04:00
Jeffrey 'Alex' Clark
80c3ff2aee
PYTHON-5753 Add just recipes for running coverage tests locally (#2727) 2026-03-12 12:42:15 -04:00
Jeffrey 'Alex' Clark
3d89d9faca
PYTHON-5754 Fix USE_ACTIVE_VENV support (#2728) 2026-03-11 14:09:11 -04:00
Shane Harvey
b6cc22ffdd
PYTHON-5748 Remove unused SpecRunner class (#2725) 2026-03-09 12:37:32 -07:00
Shane Harvey
f303125cee
PYTHON-5114 Test suite reduce killAllSessions calls (#2721) 2026-03-09 11:53:40 -07:00
Iris
38da6c3f9a
PYTHON-5747 Add jira link to spec resync PR (#2723) 2026-03-09 12:24:59 -04:00
Noah Stapp
926541fa4d
PYTHON-5742 - Add Copilot instructions (#2717) 2026-03-09 10:29:00 -04:00
Noah Stapp
f533157981
Python 4542 - Improved sessions API (#2712) 2026-03-05 09:04:37 -07:00
mongodb-drivers-pr-bot[bot]
e028fe2a38
[Spec Resync] 03-02-2026 (#2716)
Co-authored-by: Cloud User <ec2-user@ip-10-128-55-188.ec2.internal>
Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com>
2026-03-02 18:24:06 -08:00
Noah Stapp
469a32a9dd
PYTHON-5737 - BSON encoding/decoding performance improvements (#2715) 2026-03-02 10:06:47 -08:00
Noah Stapp
84814b2a72
PYTHON-5731 - Server selection deprioritization only for overload errors on replica sets (#2710) 2026-02-23 13:18:24 -05:00
Steven Silvester
908102d776
PYTHON-5732 Use mongodb-runner in Evergreen Tests (#2703) 2026-02-20 13:02:52 -06:00
Steven Silvester
edd0e0698f
PYTHON-5708 Temporarily skip some BSON encryption tests (#2709) 2026-02-20 11:56:30 -06:00
dependabot[bot]
cbd82e75e7
Bump the actions group with 2 updates (#2711)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 06:26:49 -06:00
Steven Silvester
6923641626
PYTHON-5729 Pin setuptools when using older gevent (#2708) 2026-02-18 14:42:00 -06:00
Steven Silvester
b60d266ad7
PYTHON-3898 Add coverage to all variants (#2705) 2026-02-17 12:23:34 -06:00
Steven Silvester
36676384bd
PYTHON-5705 Improve fallback for PyOpenSSL windows system certs loading (#2688) 2026-02-09 19:39:05 -06:00
Steven Silvester
0441761872
PYTHON-5715 Add appName to OIDC test failpoints (#2697) 2026-02-09 14:51:30 -06:00
Steven Silvester
fdb6a3291f
PYTHON-5467 Fix codecov upload on Evergreen (#2702) 2026-02-09 13:55:08 -06:00
Steven Silvester
b1a0a1f104
PYTHON-5467 Fix codecov upload (#2701) 2026-02-06 10:29:37 -06:00
Casey Clements
f28ab12db0
PYTHON-XXXX Fixed typo in Running Tests Locally section. (#2698) 2026-02-06 09:08:00 -05:00
dependabot[bot]
d5e1777732
Bump astral-sh/setup-uv from 7.2.0 to 7.2.1 in the actions group (#2700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-06 09:01:06 -05:00
Steven Silvester
afc884d786
PYTHON-5467 Add codecov integration (#2690) 2026-02-05 13:52:10 -06:00
mongodb-drivers-pr-bot[bot]
e077ebd926
[Spec Resync] 02-02-2026 (#2694)
Co-authored-by: Cloud User <ec2-user@ip-10-128-37-208.ec2.internal>
2026-02-03 14:44:16 -05:00
Noah Stapp
543c4e532c
PYTHON-1357 - Refactor Cursor and CommandCursor (#2691) 2026-02-02 08:47:26 -05:00
dependabot[bot]
182d8e2ea0
Bump peter-evans/create-pull-request from 8.0.0 to 8.1.0 in the actions group (#2692)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2026-01-30 08:35:46 -06:00
dependabot[bot]
4c86d86bf1
Bump astral-sh/setup-uv from 7.1.6 to 7.2.0 in the actions group across 1 directory (#2684)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-28 13:36:28 -06:00
Steven Silvester
fa56b563dd
PYTHON-5704 Skip free-threading for enterprise auth builds (#2687) 2026-01-27 12:04:51 -06:00
Steven Silvester
896f139ddc
PYTHON-5703 Use Ubuntu24 for AWS Auth tests (#2686) 2026-01-27 10:49:44 -06:00
mongodb-drivers-pr-bot[bot]
a89c5e3a89
PYTHON-5699 & PYTHON-5698 [Spec Resync] 01-26-2026 (#2685)
Co-authored-by: Cloud User <ec2-user@ip-10-128-52-19.ec2.internal>
2026-01-26 13:36:51 -06:00
Noah Stapp
db6dad95be
PYTHON-5605 - Drop usage of Ubuntu 20 (#2683) 2026-01-26 07:51:26 -05:00
Noah Stapp
a426ad91d7
PYTHON-5692 - [Infrastructure] Improve dependabot version updates (#2682) 2026-01-23 14:53:30 -05:00
dependabot[bot]
1e7477b9df
Bump pyright from 1.1.407 to 1.1.408 (#2675)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Casey Clements <casey.clements@mongodb.com>
Co-authored-by: Casey Clements <caseyclements@users.noreply.github.com>
2026-01-22 10:17:15 -05:00
mongodb-drivers-pr-bot[bot]
db28d14b6d
[Spec Resync] 01-19-2026 (#2680)
Co-authored-by: Cloud User <ec2-user@ip-10-128-52-183.ec2.internal>
2026-01-20 13:21:36 -05:00
Noah Stapp
12b3859903
PYTHON-5697 - Migrate 8.0+ tests to Windows 2022 (#2681) 2026-01-20 12:24:55 -05:00
Rin
b88415b8e8
refactor(ci): replace shell=True and awk pipes with native Python (#2671) 2026-01-09 09:23:00 -05:00
mongodb-dbx-release-bot[bot]
cb01da6a50
BUMP 4.17.0.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2026-01-07 18:10:24 +00:00
Jeffrey A. Clark
32901018ca
Prepare 4.16.0 release (#2672) 2026-01-07 12:03:02 -05:00
Steven Silvester
1be94d262d
PYTHON-5685 Fix unified spec sync metadata for csot and sessions tests (#2669) 2026-01-05 18:04:05 -05:00
Rin
6585d9cb51
PYTHON-2442: Refactor: use _asdict() in _options_dict() (#2670)
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-12-30 10:41:37 -06:00
Jeffrey A. Clark
fdb1f7ea4a
PYTHON-5677 Prevent ClientEncryption from loading crypt shared library (#2659)
Co-authored-by: Kevin Albertson <kevin.albertson@mongodb.com>
2025-12-29 17:16:34 -05:00
dependabot[bot]
0cd9763423
Bump zizmorcore/zizmor-action from cb3d8e846e148d1111d90b03375b9c03deceda37 to 706c51b5bce7adb027de71ab36d865f5d3fcc7b7 in the actions group (#2667)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 09:16:13 -06:00
Steven Silvester
2f263d4d3f
PYTHON-5680 Fix handling of expectedDocuments in Unified Test Runner (#2665) 2025-12-29 09:09:56 -06:00
Tim Graham
e9658b2406
Add 4.15.5 release date to changelog (#2666) 2025-12-26 16:46:28 -05:00
dependabot[bot]
10dd20405b
Update coverage[toml] requirement from <=7.10.6,>=5 to >=5,<=7.10.7 (#2662)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
Co-authored-by: Casey Clements <caseyclements@users.noreply.github.com>
2025-12-23 14:20:52 -05:00
mongodb-drivers-pr-bot[bot]
130067799c
[Spec Resync] 12-22-2025 (#2663)
Co-authored-by: Cloud User <ec2-user@ip-10-128-23-103.ec2.internal>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-12-23 09:59:06 -06:00
Steven Silvester
18c1f142b5
PYTHON-5529 Introduce optin setting to await for MinPoolSize population (#2664) 2025-12-23 06:43:32 -06:00
dependabot[bot]
6ccaae5772
Bump furo from 2025.9.25 to 2025.12.19 (#2661)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-12-22 10:23:11 -05:00
dependabot[bot]
5b13ae006a
Bump github/codeql-action from 4.31.8 to 4.31.9 in the actions group (#2660)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 07:41:08 -06:00
Steven Silvester
c930c69776
PYTHON-5566 & PYTHON-3132 Add minimum version checks for remaining test variants (#2650) 2025-12-19 13:14:52 -06:00
Adam Johnson
b1ea391842
PYTHON-5679 Optimize ObjectId (#2656)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-12-18 06:16:29 -06:00
Adam Johnson
e5070789cc
PYTHON-5679 Optimize ObjectId.__str__() (#2657)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-12-18 06:16:02 -06:00
Jib
60289f0398
PYTHON-5433 (hotfix): Fix typing check for sbom requirements file (#2655) 2025-12-17 20:37:58 -06:00
dependabot[bot]
1e78bd4d46
Bump mypy from 1.19.0 to 1.19.1 (#2652)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
Co-authored-by: Jib <jib.adegunloye@mongodb.com>
2025-12-16 13:33:40 -06:00
Steven Silvester
029c74cb3a
PYTHON-5670 Restore minimal support for Python 3.9 (#2640) 2025-12-16 13:32:40 -06:00
Steven Silvester
0ce7686c64
PYTHON-5563 Fix unified test discovery (#2644) 2025-12-16 13:30:30 -06:00
Jib
f9f48bab95
PYTHON-5433: Create an sbom-requirements.txt file to capture optional dependencies (#2649) 2025-12-16 14:29:15 -05:00
Noah Stapp
0cfba4994d
PYTHON-5662 - Add support for server selection's deprioritized servers to all topologies (#2639) 2025-12-16 12:21:45 -05:00
dependabot[bot]
f813437154
Bump the actions group with 6 updates (#2651)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 06:17:06 -06:00
Steven Silvester
27ac7bd717
PYTHON-2517 Remove any Jython specific code or workarounds (#2641) 2025-12-12 12:36:11 -06:00
Steven Silvester
2f7946f523
PYTHON-4099 Add contributing docs for memory profiling (#2646) 2025-12-11 09:58:53 -06:00
Steven Silvester
da6d3d9e62
PYTHON-5673 Only update sbom when core dependencies change (#2647) 2025-12-11 06:18:38 -06:00
Jeffrey A. Clark
37632e70d6
PYTHON-5669 setup-tests.sh should support --active (#2648) 2025-12-10 22:29:00 -05:00
mongodb-dbx-release-bot[bot]
a9923507c5
BUMP 4.16.0.dev1
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-12-11 00:32:47 +00:00
dependabot[bot]
1496b8d2ff
Bump the actions group with 3 updates (#2637)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com>
2025-12-10 12:59:49 -08:00
mongodb-drivers-pr-bot[bot]
ab8b99a005
[Spec Resync] 12-01-2025 (#2632)
Co-authored-by: Cloud User <ec2-user@ip-10-128-26-154.ec2.internal>
Co-authored-by: Jeffrey A. Clark <aclark@aclark.net>
Co-authored-by: Iris Ho <iris.ho@mongodb.com>
2025-12-10 11:49:27 -08:00
Steven Silvester
ae88b5a08f
PYTHON-5530 Reduce usage of legacy test runner (#2642) 2025-12-10 13:40:24 -06:00
dependabot[bot]
49e59d41b2
PYTHON-5661 Bump mypy from 1.18.2 to 1.19.0 (#2629)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-12-10 10:37:29 -06:00
Steven Silvester
e7aab567bf
PYTHON-4783 Remove reference to RHEL7 in tests (#2643) 2025-12-10 09:06:49 -06:00
Casey Clements
2195866ba7
PYTHON-5355 Addition of API to move to and from NumPy ndarrays and BSON BinaryVectors (#2590)
Co-authored-by: Jib <Jibzade@gmail.com>
Co-authored-by: Noah Stapp <noah.stapp@mongodb.com>
2025-12-05 11:39:22 -05:00
Kevin Albertson
3093a7c7cb
PYTHON-5664 extract using tar command (#2636) 2025-12-04 11:58:10 -05:00
Jib
44baec9e9c
PYTHON-5401: Revise pull request template for better structure (#2626) 2025-12-04 10:49:30 -05:00
dependabot[bot]
bd6decb8c0
Bump zizmorcore/zizmor-action from b0e5c0b2b3785bc67b9b6c743fdbd495cda1b4c4 to c0e2b1c877e25a91d1d747c438d49199cad29698 in the actions group (#2630)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jib <jib.adegunloye@mongodb.com>
2025-12-02 10:45:03 -05:00
Casey Clements
6011df9e37
PYTHON-5643 Add contributor docs for the test.utils_shared.delay function (#2628) 2025-12-01 15:17:35 -05:00
Casey Clements
8bf8263391
PYTHON-5656: Fixes broken link to aggregation pipeline docs. (#2627) 2025-12-01 15:15:09 -05:00
Cal Jacobson
222a55f8cd
PYTHON-5653: fix - correct return type annotation for find_one_and_* methods to include None (#2615)
Co-authored-by: Jib <jib.adegunloye@mongodb.com>
Co-authored-by: Casey Clements <caseyclements@users.noreply.github.com>
2025-11-25 15:36:33 -05:00
Kevin Albertson
3d76c84b2a
PYTHON-5647 remove redundant entry for *.mongodbgov.net (#2625) 2025-11-25 14:27:28 -06:00
dependabot[bot]
881094015b
Bump the actions group with 7 updates (#2620)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 13:58:34 -06:00
Noah Stapp
42cf3407c8
PYTHON-5642 - getMore operations should do server selection if the server is unknown (#2621) 2025-11-24 11:43:48 -05:00
github-actions[bot]
1a434c7c59
chore: Update SBOM (#2623)
Co-authored-by: blink1073 <2096628+blink1073@users.noreply.github.com>
2025-11-24 10:34:44 -06:00
thanhnguyen-mdb
cef27b18d9
PYTHON-5433 - Fix Silkbomb issues (#2622) 2025-11-24 10:21:00 -06:00
Kevin Albertson
a9c034426b
PYTHON-5647 extend ALLOWED_HOSTS (#2618) 2025-11-21 10:33:18 -06:00
mongodb-drivers-pr-bot[bot]
0c5eec790b
[Spec Resync] 11-10-2025 (#2609)
Co-authored-by: Cloud User <ec2-user@ip-10-128-24-49.ec2.internal>
Co-authored-by: Noah Stapp <noah.stapp@mongodb.com>
Co-authored-by: Jib <jib.adegunloye@mongodb.com>
2025-11-21 11:13:29 -05:00
github-actions[bot]
47da699a87
chore: Update SBOM (#2619)
Co-authored-by: blink1073 <2096628+blink1073@users.noreply.github.com>
2025-11-20 18:41:46 -06:00
thanhnguyen-mdb
71e0c950e1
PYTHON-5433 - Added SBOM update automation (#2617) 2025-11-20 15:02:46 -06:00
dependabot[bot]
44a58f1650
Bump pyright from 1.1.406 to 1.1.407 (#2603)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jib <jib.adegunloye@mongodb.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-11-13 12:22:00 -06:00
dependabot[bot]
63acab96cf
Bump the actions group with 2 updates (#2608)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-11 09:23:32 -06:00
dependabot[bot]
eb25ce420e
Bump the actions group across 1 directory with 4 updates (#2604) 2025-11-05 12:20:45 -06:00
Rogdham
f278e471d1
PYTHON-5522: Support std lib zstandard in 3.14 (#2592) 2025-10-31 16:14:14 -05:00
Noah Stapp
5f00966f9c
[TASK]-[PYTHON-5623]: Change with_transaction callback return type to Awaitable (#2594)
Co-authored-by: Logan Pulley <logan@pulley.host>
2025-10-29 14:31:25 -04:00
Noah Stapp
b607ef144c
PYTHON-5214 - Improve BSON decoding InvalidBSON error message (#2605) 2025-10-29 14:30:18 -04:00
Noah Stapp
fd02550349
PYTHON-5628 - Update the link for help in the documentation (#2602) 2025-10-27 11:41:14 -04:00
Noah Stapp
0c8a22b87d
PYTHON-5627 - Update feedback link (#2601) 2025-10-24 15:26:46 -04:00
Steven Silvester
a5f6d638b9
PYTHON-5615 Use uv python when python toolchain is not available (#2597) 2025-10-22 17:22:22 -05:00
Noah Stapp
ad1167d01e
[Task]-PYTHON-5626: Remove project.license toml table (#2595) 2025-10-21 15:57:36 -04:00
Noah Stapp
faa77eab43
[Task] PYTHON-5561: Add support for PyPy 3.11 (#2596) 2025-10-21 13:06:41 -04:00
dependabot[bot]
6a796c8668
Bump furo from 2025.7.19 to 2025.9.25 (#2565)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-10-10 11:27:01 -05:00
Steven Silvester
6d91859659
PYTHON-5611 Fix python binary usage for Other Hosts (#2586) 2025-10-08 12:26:16 -05:00
Steven Silvester
5eb1edf315
PYTHON-5609 Add 4.15.3 release to changelog (#2585) 2025-10-08 07:36:44 -05:00
Casey Clements
d595913117
PYTHON-5598 Add generate_config method to ensure auth is tested on free-threaded python 3.14t (#2580) 2025-10-07 15:43:07 -04:00
Iris
89a4eaa36c
PYTHON-5576: add PR template to mongo-python-driver (#2567) 2025-10-07 12:34:56 -07:00
Steven Silvester
491f5ba77f
PYTHON-5588 Fix python binary used in FIPS tests (#2581) 2025-10-07 12:30:06 -05:00
Steven Silvester
84772bd8a9
PYTHON-5604 Skip ECS tests until we can test on Ubuntu 22 (#2582) 2025-10-07 11:07:44 -05:00
Steven Silvester
a2e39ada00
PYTHON-5596 Fix return type for distinct methods (#2576) 2025-10-07 11:04:16 -05:00
dependabot[bot]
46974363b4
PYTHON-5538 Fix lock file handling and bump pyright from 1.1.405 to 1.1.406 (#2575)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-10-06 13:02:53 -05:00
Jeffrey A. Clark
406bed0418
PYTHON-5597 Upgrade to macos-latest (#2578) 2025-10-06 13:10:31 -04:00
dependabot[bot]
16a2fea219
Bump the actions group with 3 updates (#2574)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-10-06 10:31:00 -05:00
Noah Stapp
52400e11a1
PYTHON-5571 - Fix memory leak when raising InvalidDocument with C extensions (#2573) 2025-10-06 09:25:57 -04:00
Noah Stapp
d47bd9cf95
PYTHON-5024 - Add 3.14t as a standard Python matrix version (#2563) 2025-10-03 13:03:07 -04:00
Casey Clements
6bdf07e726
PYTHON-5585 Add jira.mongodb.org/secure/ReleaseNote links to linkcheck_ignore (#2572) 2025-10-02 17:48:22 -04:00
Casey Clements
e3910f868b
PYTHON-5593 Adds v4.15.2 notes to changelog (#2570) 2025-10-02 13:43:31 -04:00
dependabot[bot]
215b3b1938
Bump github/codeql-action from 3.30.3 to 3.30.5 in the actions group (#2564)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:51:14 -04:00
Steven Silvester
67384f0f08
PYTHON-5550 Add a test that uses uvloop as the event loop (#2543) 2025-09-30 12:30:00 -05:00
Steven Silvester
b291807106
PYTHON-5587 Remove check for dnspython version (#2566) 2025-09-30 11:39:51 -05:00
dependabot[bot]
8d4518287c
Bump mypy from 1.18.1 to 1.18.2 (#2551)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
Co-authored-by: Iris Ho <iris.ho@mongodb.com>
2025-09-29 11:29:57 -07:00
Iris
4839e523c8
PYTHON-5569: [Build Failure] Spec Resync job is failing silently (#2553) 2025-09-29 10:29:08 -07:00
Steven Silvester
e0767cf5a1
PYTHON-5479 Drop support for Python 3.9 (#2562)
Co-authored-by: Noah Stapp <noah@noahstapp.com>
2025-09-26 09:54:19 -05:00
Steven Silvester
0d93ec48a5
PYTHON-5573 Require dnspython 2.6.1+ (#2559) 2025-09-25 13:09:33 -05:00
Steven Silvester
1f308c841f
PYTHON-5480 Update Python 3.9-specific tests to use Python 3.10 (#2560) 2025-09-25 12:52:30 -05:00
Noah Stapp
eb0cedd969
PYTHON-5577 - Drop support for OpenSSL 1.0.2 (#2561) 2025-09-25 11:16:17 -04:00
Steven Silvester
fad2ccb0e7
PYTHON-5565 Add minimum version test for Encryption (#2547) 2025-09-25 09:28:39 -05:00
Steven Silvester
448a4944ff
PYTHON-5574 Allow uv lockfile to update from justfile lint (#2558) 2025-09-24 19:48:03 -05:00
Iris
4849eacc10
PYTHON-5563: Change most tasks to run daily instead of weekly (#2556) 2025-09-24 11:42:14 -07:00
Noah Stapp
9e64ed1bd8
PYTHON-4755 - Stop supporting and testing against Eventlet (#2557) 2025-09-24 14:07:51 -04:00
Noah Stapp
0049dc8896
PYTHON-2390 - Retryable reads use the same implicit session (#2544) 2025-09-24 13:23:28 -04:00
Jib
51f7b408f3
PYTHON-5572: Add team members to contributors.rst (#2554) 2025-09-24 10:27:45 -04:00
Steven Silvester
29c4c2cc0f
PYTHON-5570 Do not freeze the lockfile (#2555) 2025-09-23 14:08:13 -05:00
Noah Stapp
266caf02c4
PYTHON-5449 - Do not attach invalid document in exception message (#2539) 2025-09-23 14:31:35 -04:00
Steven Silvester
6fe85436ae
PYTHON-3414 Improve error message when using incompatible dependencies (#2549) 2025-09-22 17:15:02 -05:00
dependabot[bot]
9603a85f21
Bump the actions group with 2 updates (#2550)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-09-22 13:05:14 -05:00
Steven Silvester
ef59602e39
PYTHON-5491 Update test for dropIndex behavior change (#2546) 2025-09-18 20:08:22 -05:00
Noah Stapp
668bd8232a
PYTHON-2391 - Ensure retries do not use duplicate command payloads (#2545) 2025-09-17 16:52:00 -04:00
Steven Silvester
4936fe90bf
PYTHON-5539 Fix installation of pymongocrypt from source (#2541) 2025-09-17 13:05:52 -05:00
Steven Silvester
dba0aa94ad
PYTHON-5472 Remove driver tests for Atlas Data Lake (#2542) 2025-09-17 08:15:32 -05:00
Steven Silvester
a7a645f85f
PYTHON-5555 Fix AWS Lambda build (#2540) 2025-09-17 06:39:37 -05:00
Steven Silvester
5787acc271
PYTHON-5556 Keep uv lock file up to date (#2534) 2025-09-17 06:38:47 -05:00
Steven Silvester
4b4d74971c
PYTHON-5500 Account for extra flakiness in test_dns_failures_logging (#2533) 2025-09-17 06:38:24 -05:00
mongodb-dbx-release-bot[bot]
4b4c949997
BUMP 4.16.0.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-09-16 16:43:29 +00:00
Steven Silvester
8cf65796da
PYTHON-5542 Prepare for 4.15.1 Release (#2537) 2025-09-16 11:01:47 -05:00
Steven Silvester
7a07c02814
PYTHON-5544 Revert changes to base protocol layer (#2535) 2025-09-16 09:16:31 -05:00
dependabot[bot]
eca38b730b
Bump mypy from 1.17.1 to 1.18.1 (#2532)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 07:45:15 -05:00
dependabot[bot]
32e183baa7
Bump the actions group with 3 updates (#2531)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 07:44:32 -05:00
Jeffrey A. Clark
3da6e858d5
PYTHON-5543 PyMongoBaseProtocol should inherit from asyncio.BaseProtocol (#2528)
Co-authored-by: Noah Stapp <noah@noahstapp.com>
2025-09-11 16:37:22 -04:00
Steven Silvester
2b148867e7
PYTHON-5540 Fix usage of text_opts for older versions of pymongocrypt (#2525) 2025-09-10 16:38:55 -05:00
Steven Silvester
527cbdd18a
PYTHON-5537 Update typing dependencies (#2524) 2025-09-10 13:28:02 -05:00
dependabot[bot]
8879f2b951
Bump the actions group with 5 updates (#2519)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-09-10 13:27:36 -05:00
mongodb-dbx-release-bot[bot]
d2653eecc6
BUMP 4.16.0.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-09-10 16:50:43 +00:00
Jeffrey A. Clark
1514e9b784
Prepare 4.15 release (#2523) 2025-09-10 12:03:54 -04:00
Steven Silvester
98e9f5ecc1
PYTHON-5538 Clean up uv lock file handling (#2522) 2025-09-10 10:36:14 -05:00
Steven Silvester
d7316afb63
PYTHON-5328 CRUD Support in Driver for Prefix/Suffix/Substring Indexes (#2521) 2025-09-10 10:35:35 -05:00
Steven Silvester
7580309e99
PYTHON-4928 Convert CSFLE spec tests to unified test format (#2520) 2025-09-08 16:01:12 -05:00
dependabot[bot]
47c5460d2e
Bump pyright from 1.1.404 to 1.1.405 (#2518)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 09:10:29 -05:00
Noah Stapp
b84e1a7ce4
PYTHON-5527 - Unified test typo in 'Expected error' (#2517) 2025-09-03 15:00:04 -04:00
Noah Stapp
c0e0554a3b
PYTHON-5521 - Update TestBsonSizeBatches.test_06_insert_fails_over_16MiB error codes (#2515) 2025-09-03 14:18:51 -04:00
Noah Stapp
d63edf7aea
PYTHON-5524 - Fix CSFLE spec test min version checks (#2516) 2025-09-03 13:35:43 -04:00
dependabot[bot]
b756bbd2a3
Bump the actions group with 2 updates (#2513)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-09-02 08:37:19 -05:00
dependabot[bot]
b2bba67b61
Update coverage requirement from <=7.10.5,>=5 to >=5,<=7.10.6 (#2512)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-02 07:30:34 -05:00
Steven Silvester
6656767850
PYTHON-5486 Test Gevent with Auth and SSL (#2508) 2025-08-27 11:24:47 -05:00
Finn Womack
cffb9069fd
PYTHON-5520 Add windows arm64 wheel support (#2511) 2025-08-27 07:30:56 -05:00
Steven Silvester
0d4c84e86f
PYTHON-5519 Clean up uv handling (#2510) 2025-08-26 09:52:09 -05:00
dependabot[bot]
8c361be219
Bump the actions group with 5 updates (#2505)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-08-26 08:24:30 -05:00
dependabot[bot]
9892e1bbe9
Update coverage requirement from <=7.10.3,>=5 to >=5,<=7.10.5 (#2507)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 11:57:35 -05:00
dependabot[bot]
cd4e5db997
Bump pyright from 1.1.403 to 1.1.404 (#2506)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 11:57:02 -05:00
Iris
3ebd93480a
PYTHON-5514 Specific assertions for "is" and "is not None" (#2502) 2025-08-25 08:54:10 -07:00
Shane Harvey
ddf9508e15
PYTHON-5510 Fix server selection log message for commitTransaction (#2503) 2025-08-22 14:51:39 -07:00
Steven Silvester
e08284bdca
PYTHON-5456 Support text indexes with auto encryption (#2500) 2025-08-21 10:55:48 -05:00
Noah Stapp
5e96353797
PYTHON-5508 - Add built-in DecimalEncoder and DecimalDecoder (#2499) 2025-08-21 06:51:00 -07:00
Steven Silvester
9a9a65c617
PYTHON-5496 Update CSOT tests for change in dropIndex behavior in 8.3 (#2498) 2025-08-20 18:42:06 -05:00
Steven Silvester
f7b94be0db
PYTHON-5143 Support auto encryption in unified tests (#2488) 2025-08-20 08:58:20 -05:00
Iris
db3d3c7022
Prep for 4.14.1 release (#2495) [master] (#2496) 2025-08-19 17:46:25 -07:00
Steven Silvester
3a26119eb3
PYTHON-5502 Fix c extensions on OIDC VMs (#2489) 2025-08-19 11:26:11 -05:00
Steven Silvester
d24b4a5697
PYTHON-5503 Use uv to install just in GitHub Actions (#2490) 2025-08-19 11:23:51 -05:00
Steven Silvester
e4b7eb52e6
PYTHON-5215 Add an asyncio.Protocol implementation for KMS (#2460) 2025-08-19 08:45:24 -05:00
Steven Silvester
37d327fbd8
PYTHON-5502 Fix handling of c extensions in Azure and GCP VMs (#2486) 2025-08-19 08:37:54 -05:00
Steven Silvester
b32da4b409
PYTHON-5492 Fix handling of MaxTimeMS message (#2484) 2025-08-18 18:52:46 -05:00
Iris
2a1523fa85
PYTHON-5488 append_metadata should not add duplicates (#2461) 2025-08-18 11:12:48 -07:00
Steven Silvester
de332c553c
PYTHON-5500 Mark test_dns_failures_logging as flaky (#2480) 2025-08-18 12:05:26 -05:00
Steven Silvester
bfa01c6a6c
PYTHON-5498 Disable C extensions for Remote KMS Tests (#2478) 2025-08-18 06:53:15 -05:00
Steven Silvester
9dbccbee2c
PYTHON-5492 Fix handling of MaxTimeMSExpired responses (#2477) 2025-08-15 19:13:51 -05:00
Steven Silvester
e44ece0b07
PYTHON-5493 Add a patch for the log order difference (#2473) 2025-08-15 11:58:39 -05:00
Steven Silvester
b83fcbb1a9
PYTHON-5349 Use drivers-evergreen-tools to start servers in GitHub Actions (#2474) 2025-08-15 09:15:37 -05:00
Steven Silvester
1ffdedc7a4
PYTHON-5492 Mark test as flaky (#2472) 2025-08-14 13:54:24 -05:00
Noah Stapp
3c786f5cd9
PYTHON-3606 - Document best practice for closing MongoClients and cursors (#2465) 2025-08-13 09:46:01 -04:00
Steven Silvester
f105789e12
Revert "Bump the actions group with 3 updates" (#2471) 2025-08-11 13:49:29 -05:00
dependabot[bot]
61e90473e6
Update coverage requirement from <=7.10.2,>=5 to >=5,<=7.10.3 (#2470)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 10:36:57 -05:00
dependabot[bot]
e79c19b4d2
Bump the actions group with 3 updates (#2469)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 10:35:58 -05:00
Steven Silvester
578a532395
PYTHON-5491 Skip non-idempotent dropIndex tests (#2467) 2025-08-08 19:52:17 -05:00
Jeffrey A. Clark
4e9b52b8d6
PYTHON-5487 Update 4.14 changelog to mention MongoDB 4.0 is no longer supported (#2462)
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
Co-authored-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-08-07 14:01:08 -04:00
Noah Stapp
d88596cef1
PYTHON-5218 - Add logging statement when SRV polling fails (#2463)
Co-authored-by: Jib <Jibzade@gmail.com>
2025-08-07 13:32:11 -04:00
Noah Stapp
ad16d6e880
PYTHON-4431 - Remove ReadTheDocs Documentation in Favor of Official Docs (#2459) 2025-08-07 12:06:38 -04:00
Noah Stapp
bbb6f88fae
PYTHON-5257 - Turn on mypy disallow_any_generics (#2456) 2025-08-06 14:21:53 -04:00
Steven Silvester
d7074ba9ee
PYTHON-5454 & PYTHON-5455 Add preliminary python 3.14 support (#2451) 2025-08-05 13:01:30 -05:00
Kevin Albertson
d11cf20452
Fix In-Use Encryption examples (#2457) 2025-08-05 10:05:22 -04:00
dependabot[bot]
baec1e05f7
Bump pyright from 1.1.392.post0 to 1.1.403 (#2455)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 07:47:28 -05:00
dependabot[bot]
003ff56cbc
Bump the actions group with 2 updates (#2454)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 06:37:55 -05:00
dependabot[bot]
cbe1b9e81b
Update coverage requirement from <=7.5,>=5 to >=5,<=7.10.2 (#2453)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 06:37:25 -05:00
dependabot[bot]
0249a08201
Bump mypy from 1.14.1 to 1.17.1 (#2452)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 06:36:28 -05:00
mongodb-drivers-pr-bot[bot]
bfaab82e26
[Spec Resync] 07-28-2025 (#2447)
Co-authored-by: Cloud User <ec2-user@ip-10-128-23-129.ec2.internal>
Co-authored-by: Noah Stapp <noah.stapp@mongodb.com>
2025-07-31 11:40:02 -04:00
Noah Stapp
9f64dad687
PYTHON-5473 - Better test assertions for booleans (#2450) 2025-07-31 08:57:00 -04:00
Noah Stapp
9514a67270
PYTHON-5441 - Unskip gridfs download chunk tests (#2449) 2025-07-31 08:54:12 -04:00
dependabot[bot]
59d94f397b
Bump the actions group with 3 updates (#2446)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 11:32:04 -05:00
Steven Silvester
ffb372aec7
PYTHON-5027 Test Windows with Python 3.14t (#2444) 2025-07-24 13:20:19 -05:00
Steven Silvester
06872f7f03
PYTHON-4780 Implement fast path for server selection with Primary (#2416) 2025-07-22 10:23:26 -05:00
dependabot[bot]
5a640daf92
Bump astral-sh/setup-uv from 6.3.1 to 6.4.1 in the actions group (#2441)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 07:52:15 -05:00
dependabot[bot]
f9b2f711c0
Bump furo from 2024.8.6 to 2025.7.19 (#2440)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 07:51:49 -05:00
Iris
31cca98656
PYTHON-5253 Automated Spec Resync Quick Followup/Fix (#2443) 2025-07-21 14:18:39 -07:00
Steven Silvester
cf2630148a
PYTHON-4677 Specify how maxTimeMS can be set for explain helpers (#2439) 2025-07-18 12:04:10 -05:00
Steven Silvester
55d399b75a
PYTHON-4019 Infinite loop in generic transactional provider due to dup keys (#2438) 2025-07-17 09:29:11 -05:00
Steven Silvester
fed738df42
PYTHON-5444 Update OIDC tests use camelCase options (#2436) 2025-07-16 16:27:59 -05:00
Steven Silvester
6ef91357b2
PYTHON-4884 Test encoding dates after year 9999 with Relaxed Extended JSON (#2437) 2025-07-16 16:27:26 -05:00
Steven Silvester
36bb704c76
PYTHON-5237 Relax requirement for optional fields for sessions unified tests (#2435) 2025-07-15 20:26:29 -05:00
Steven Silvester
71514b5989
PYTHON-5152 Sunset Astrolabe (#2434) 2025-07-15 20:25:13 -05:00
Steven Silvester
3be7f76763
PYTHON-4203 Update prose tests for mongos deprioritization during retryable ops (#2430) 2025-07-15 15:38:15 -05:00
Iris
83fcf7cd08
PYTHON-4931 Add spec tests for GridFS rename (#2431) 2025-07-15 12:15:05 -07:00
Casey Clements
4a29fbda69
PYTHON-5289 Fixes indentation in docstring of Binary.from_vector (#2432) 2025-07-15 11:27:38 -07:00
Casey Clements
1e67c5c02c
PYTHON-5289 Validate ignored bits are 0 on write for bson.BinaryVector (#2397) 2025-07-15 10:17:30 -07:00
Iris
ca3cbc3f31
PYTHON-5253 Automated Spec Test Sync (#2409)
Co-authored-by: Noah Stapp <noah@noahstapp.com>
2025-07-15 08:34:47 -07:00
Steven Silvester
84db915d91
PYTHON-5361 Fix timeouts in CSE custom endpoint test (#2426) 2025-07-14 12:27:22 -05:00
Steven Silvester
5ce53dc175
PYTHON-5374 Assert unset BulkWriteException.partialResult in CRUD prose tests (#2425)
Co-authored-by: Noah Stapp <noah@noahstapp.com>
2025-07-14 10:47:38 -05:00
Steven Silvester
e07a6b7e77
PYTHON-5439 Remove dead link in PyMongo 4 migration guide (#2428) 2025-07-14 10:24:09 -05:00
Steven Silvester
7b82b3582f
PYTHON-5440 Use dochub link for index-wildcard (#2427) 2025-07-14 10:03:33 -05:00
Steven Silvester
f29c7b1f15
PYTHON-5315 Mark test_recover_from_initially_erroring_seedlist as flaky (#2424) 2025-07-10 06:32:43 -05:00
Kevin Albertson
04f2cc0fa9
PYTHON-5373 test client auth on cloud-dev (#2423) 2025-07-09 11:56:42 -04:00
Iris
c77c15e369
PYTHON-5421 continued - update changelog, update docstring, and add testing (#2420) 2025-07-07 14:00:11 -07:00
Noah Stapp
11d3488026
PYTHON-5415 - Unskip tests that rely on server hostname (#2398) 2025-07-07 09:59:48 -04:00
dependabot[bot]
d6ab555b81
Bump the actions group with 2 updates (#2422)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 07:17:35 -05:00
Steven Silvester
c788c7e0c1
PYTHON-5431 Include assume role creds in backport task (#2418) 2025-07-03 13:36:22 -05:00
Steven Silvester
1d21d27dda
PYTHON-5430 Use the zizmor action (#2417) 2025-07-03 12:30:35 -05:00
rishitb-mongodb
dde8837fb2
DRIVERS-3105: Update README.md to add mention of SemVer adherence (#2391) 2025-07-02 17:04:40 -04:00
Iris
947fbe33ee
PYTHON-5421 Make parse_uri() return "options" as a dict rather than _CaseInsensitiveDictionary (#2413) 2025-07-02 09:51:50 -07:00
Steven Silvester
2eb18f18b2
PYTHON-5428 Mark test_connection_close_does_not_block_other_operations as flaky (#2415) 2025-07-02 11:22:12 -05:00
Steven Silvester
0b2900d162
PYTHON-5413 Handle flaky tests (#2395) 2025-07-01 15:42:58 -05:00
Iris
578c6c2ad2
PYTHON-5423 Always use subprocess.run instead of subprocess.check_call or subprocess.call (#2412) 2025-06-30 11:08:42 -07:00
dependabot[bot]
ed26975926
Bump the actions group across 1 directory with 3 updates (#2411)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 07:42:55 -05:00
Iris
0e407351a4
PYTHON-5392 Better test assertions for comparisons (#2350)
Co-authored-by: Noah Stapp <noah@noahstapp.com>
2025-06-27 14:06:00 -07:00
Noah Stapp
6a672d4dd3
PYTHON-5382 - Add a test with min dependencies (#2410) 2025-06-27 14:41:53 -04:00
Jib
0cb4b2f1a6
PYTHON-5287: create CODEOWNERS (#2408) 2025-06-27 12:58:11 -04:00
Noah Stapp
244f17d57b
PYTHON-5404 - Add docs + justfile target for profiling execution (#2402) 2025-06-26 16:37:03 -04:00
Iris
65f7c54208
PYTHON-5344 and PYTHON-5403 Allow Instantiated MongoClients to Send Client Metadata On-Demand (#2358) 2025-06-24 09:34:53 -07:00
Noah Stapp
e2bfa9a590
PYTHON-5248 - Drop support for MongoDB 4.0 (#2353) 2025-06-20 14:25:19 -04:00
Casey Clements
4ea0288eaa
PYTHON-5126 Updated changelog to reflect breaking change in bson.binary.BinaryVector (#2394)
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-06-20 09:40:05 -04:00
Casey Clements
336163aaa0
PYTHON-5126 - Implemented new test cases for Binary Vector (#2393) 2025-06-18 13:35:23 -04:00
Steven Silvester
8a94de1c1b
PYTHON-5343 Clean up contributing docs (#2390) 2025-06-16 11:51:46 -05:00
dependabot[bot]
87c015fbcf
Bump github/codeql-action from 3.28.19 to 3.29.0 in the actions group (#2388)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 06:31:12 -05:00
Noah Stapp
50ea82310d
PYTHON 5212 - Use asyncio.loop.sock_connect in _async_create_connection (#2383) 2025-06-13 15:30:10 -04:00
Shane Harvey
c16ef0a13e
PYTHON-5414 Add test for hostname verification error message regression (#2385) 2025-06-13 11:45:47 -07:00
Shane Harvey
e51ac1fd1c
PYTHON-5409 Fix test_implicit_sessions_checkout again (#2384) 2025-06-13 09:33:32 -07:00
Maarten Sijm
c2aefc2eda
PYTHON-5414 Fix "module service_identity has no attribute SICertificateError" when using pyopenssl (#2382) 2025-06-12 16:45:18 -07:00
Jeffrey A. Clark
54846cd110
PYTHON-5409 Update test_session.py comment (#2381) 2025-06-11 19:24:59 -04:00
Steven Silvester
a742aa22d4
PYTHON-5411 Add 4.13.1 changelog to master (#2380) 2025-06-11 16:42:18 -05:00
Jeffrey A. Clark
dfd5573c19
PYTHON-5002 Include test/ dir in synchro gaurd (#2379) 2025-06-11 15:54:32 -04:00
Noah Stapp
8a8cb6f0af
PYTHON-5406 - Use correct client for test (#2377) 2025-06-11 13:45:54 -04:00
Steven Silvester
7e19515d7b
PYTHON-5393 Make link checking more robust (#2374) 2025-06-11 10:44:46 -05:00
Steven Silvester
f645036d71
Fix release metadata (#2372) 2025-06-10 12:26:25 -05:00
Steven Silvester
9145521dfa
PYTHON-5410 Assume ec2 role in backport task (#2369) 2025-06-10 12:12:05 -05:00
Steven Silvester
673f821acb
[v4.13] PYTHON-5406 AsyncPeriodicExecutor must reset CSOT contextvars before executing (#2367)
Co-authored-by: Noah Stapp <noah.stapp@mongodb.com>
2025-06-10 06:22:28 -05:00
Steven Silvester
1bcb85f1c1
PYTHON-5321 Remove Serverless testing (#2359) 2025-06-09 19:46:10 -05:00
Steven Silvester
0dd5a5c794
PYTHON-5405 Use legacy wait_for_read cancellation approach on Windows (#2363) 2025-06-09 19:36:44 -05:00
Shane Harvey
f50ef65dd5
PYTHON-5409 Make test_implicit_sessions_checkout less flaky (#2366) 2025-06-09 11:26:07 -07:00
dependabot[bot]
24e9da6a09
Bump github/codeql-action from 3.28.18 to 3.28.19 in the actions group (#2362)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-09 08:29:27 -05:00
Noah Stapp
0f6647b49a
PYTHON-5305 - Fix Create Release Branch workflow (#2361) 2025-06-09 07:55:22 -04:00
Noah Stapp
536b1cb8ab
PYTHON-5406 - AsyncPeriodicExecutor must reset CSOT contextvars befor… (#2360) 2025-06-06 13:17:36 -04:00
Steven Silvester
6d33d4fb34
PYTHON-5399 Add a prose test for OIDC reauthentication when a session is involved (#2351) 2025-06-05 09:21:10 -05:00
dependabot[bot]
454c163788
Bump astral-sh/setup-uv from 6.0.1 to 6.1.0 in the actions group (#2357)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-02 06:29:28 -05:00
Steven Silvester
958b3d11dc
PYTHON-5400 Migrate away from Windows Server 2019 runner image on GitHub Actions (#2355) 2025-05-30 16:30:11 -05:00
Noah Stapp
1366b9132e
PYTHON-5394 - Add native async support for OIDC (#2352) 2025-05-28 15:44:54 -04:00
Michael Šimáček
27593796fb
PYTHON-5391 Skip C extension build on GraalPy (#2349)
Co-authored-by: Steven Silvester <steve.silvester@mongodb.com>
2025-05-23 12:01:30 -05:00
Iris
b8460b6001
PYTHON-5387 Better test assertions for membership (#2348) 2025-05-23 09:04:32 -07:00
Iris
65089ead4c
PYTHON-5386 Better test assertions for isinstance (#2347) 2025-05-22 16:15:44 -07:00
Noah Stapp
717fb47c17
PYTHON-5061 - Add an API to extend the bson TypeRegistry (#2345) 2025-05-21 13:45:36 -04:00
dependabot[bot]
106343a6a2
Bump github/codeql-action from 3.28.17 to 3.28.18 in the actions group (#2343)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 07:30:00 -05:00
Jeffrey A. Clark
726a6fa98d
PYTHON-5384 Describe MongoDB specifications (#2344) 2025-05-19 19:12:41 -04:00
mongodb-dbx-release-bot[bot]
a435a3e1c3
BUMP 4.14.0.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-05-14 19:14:32 +00:00
Noah Stapp
84411b9119
Bump version to 4.13.0 for release (#2342) 2025-05-14 14:51:36 -04:00
mongodb-dbx-release-bot[bot]
397c280217
BUMP 4.13.0.dev1
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-05-14 18:23:10 +00:00
Noah Stapp
60faca0253
Update changelog for v4.13 release (#2341) 2025-05-14 14:00:46 -04:00
Noah Stapp
92a5623886
PYTHON-5377 - Update assets to align with GA release of Async PyMongo (#2339) 2025-05-14 07:45:37 -04:00
Shane Harvey
4cc5e89ebf
PYTHON-5362 WriteConcern repr should be eval-able (#2338) 2025-05-13 13:37:18 -07:00
Steven Silvester
2374f3811a
PYTHON-5379 Run more variants on pull requests (#2340) 2025-05-13 11:40:00 -05:00
Steven Silvester
75f6a3718e
Revert "PYTHON-5126 & PYTHON-5280 Addresses issues raised in DRIVERS-3097 and DRIVERS-3123 " (#2337) 2025-05-12 09:35:08 -05:00
Noah Stapp
aa41e70523
PYTHON-5369 - Re-raise socket.timeout errors if the deadline has alre… (#2326) 2025-05-12 09:28:05 -04:00
Jeffrey A. Clark
2655bb4d86
PYTHON-5033 Use PyModule_Add on >= 3.13 (#2332) 2025-05-08 17:14:26 -04:00
Noah Stapp
98b030af94
PYTHON-5356 - Init unified test client SDAM for all unified tests (#2325) 2025-05-08 15:19:31 -04:00
Noah Stapp
775b683276
PYTHON-5371 - Pass repr(ServerDescription) to logging (#2329) 2025-05-08 14:20:11 -04:00
Steven Silvester
5914ea0ff4
PYTHON-5342 Fix test_dns_failures test (#2336) 2025-05-08 13:10:11 -05:00
Steven Silvester
d0b0dc3512
PYTHON-5339 Clean up GitHub PR definitions in Evergreen Project (#2331) 2025-05-06 13:40:12 -05:00
dependabot[bot]
12b4fe3644
Bump the actions group with 2 updates (#2333)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 08:32:53 -05:00
Noah Stapp
b0667b11d3
PYTHON-5358 - Switch to supported Perf usage in EVG (#2334) 2025-05-06 09:00:30 -04:00
Steven Silvester
cf9b68c6f4
Convert Enterprise Auth Variants to use common tasks (#2330) 2025-05-02 11:54:54 -05:00
Steven Silvester
2b4ab2a9ad
PYTHON-5365 Fix handing of remote tests (#2327) 2025-05-02 11:54:03 -05:00
Steven Silvester
000391c440
PYTHON-5333 Update encryption and pyopenssl variants (#2328) 2025-05-02 10:35:34 -05:00
Steven Silvester
0ec57781d1
PYTHON-5345 Streamline the standard tasks (#2312) 2025-05-01 09:08:48 -05:00
Jeffrey A. Clark
85c5ee45b5
PYTHON-5364 Update package description (#2324) 2025-04-30 08:45:46 -04:00
Jeffrey A. Clark
08e7f036a2
PYTHON-5357 Update changelog for 4.12.1 release (#2321) 2025-04-29 09:26:40 -04:00
Noah Stapp
e7db0e34aa
PYTHON-5342 - Skip async test_srv_polling tests on Windows (#2320) 2025-04-29 08:42:22 -04:00
Steven Silvester
02c3df6fc9
PYTHON-5298 Update lock file and clean up dependency installation (#2317) 2025-04-28 13:59:36 -05:00
Jeffrey A. Clark
9a2f5678de
PYTHON-5353 Pin github actions (#2318) 2025-04-28 11:48:32 -04:00
Noah Stapp
1dc45fddc1
PYTHON-5322 - Increase test_streaming_protocol.TestStreamingProtocol test_monitor_waits_after_server_check_error timeout (#2315) 2025-04-25 13:27:27 -04:00
Casey Clements
2ebd2aaecd
PYTHON-5336 Added VECTOR_SUBTYPE line to API docs (#2313) 2025-04-25 12:24:22 -04:00
Iris
c3e3373df2
PYTHON-5309 Ensure AsyncMongoClient doesn't use PyOpenSSL (#2286)
Co-authored-by: Noah Stapp <noah@noahstapp.com>
2025-04-24 16:19:09 -07:00
Steven Silvester
dae4f7f159
PYTHON-5348 Fix CodeQL Scanning for GitHub Actions (#2308) 2025-04-24 09:28:10 -05:00
Noah Stapp
34f7d7ee4c
PYTHON-5346 - test_init_disconnected_with_srv cannot run against shar… (#2304) 2025-04-23 16:32:39 -04:00
Noah Stapp
1bdf035802
PYTHON-5212 changelog update (#2306) 2025-04-23 16:32:08 -04:00
Steven Silvester
42cb70e9ab
PYTHON-5341 Fix handling of SSL tests with Stable API (#2305) 2025-04-23 14:43:49 -05:00
Noah Stapp
09897b698e
PYTHON-5212 - Do not hold Topology lock while resetting pool (#2301) 2025-04-23 15:13:38 -04:00
Shane Harvey
e2e673edeb
PYTHON-5314 Fix default imports for modules that worked in v4.8 (#2300) 2025-04-22 11:44:58 -07:00
Noah Stapp
412d0005b8
PYTHON-5306 - Fix use of public MongoClient attributes before connection (#2285) 2025-04-21 15:53:21 -04:00
Steven Silvester
d51c70b401
PYTHON-5337 Evergreen PyOpenSSL variants should use PyOpenSSL (#2299) 2025-04-21 10:48:26 -05:00
Noah Stapp
0ee8e585c2
PYTHON-5292 - Debug logs should only print on failed tests (#2296) 2025-04-21 09:43:58 -04:00
Steven Silvester
6ed3533b73
PYTHON-5313 Create Evergreen tests that do not run orchestration (#2284) 2025-04-18 13:03:53 -05:00
Steven Silvester
0f37bfd7a1
PYTHON-5331 Convert stable api and storage tests to new task pattern (#2295) 2025-04-18 10:02:50 -05:00
Steven Silvester
5f956210f8
PYTHON-5332 Update AWS, mod_wsgi, and green framework variants (#2297) 2025-04-18 09:29:20 -05:00
Noah Stapp
094a320817
PYTHON-5284 - Update changelog for Eventlet testing removal (#2293) 2025-04-17 13:01:27 -04:00
Steven Silvester
db1449b79f
PYTHON-5330 Convert no c extensions and doctests to use the standard test pattern (#2294) 2025-04-17 09:09:49 -05:00
Noah Stapp
448c8e8326
PYTHON-5325 - Decrease TestAsyncConcurrency.test_concurrency threshold (#2291) 2025-04-16 16:16:25 -04:00
Noah Stapp
4cac781530
PYTHON-5326 - Skip serverless tests with known issue (#2292) 2025-04-16 14:31:05 -04:00
Noah Stapp
aa6fa7a696
PYTHON-5284 - Remove eventlet tests for CPython > 3.9 (#2290) 2025-04-16 14:10:10 -04:00
Noah Stapp
f476d8bd97
PYTHON-5324 - Fix Windows encryption test secrets path (#2289) 2025-04-16 13:06:09 -04:00
Steven Silvester
149fe390d4
PYTHON-5188 Make version setting a part of the release process (#2288) 2025-04-16 10:40:25 -05:00
Steven Silvester
846b1fc25c
PYTHON-5316 Update tests for other hosts (#2287) 2025-04-16 08:50:51 -05:00
Steven Silvester
b83389d6bc
PYTHON-5311 Create standard linux evergreen tasks (#2282) 2025-04-15 14:44:09 -05:00
Noah Stapp
e6a4a7145e
PYTHON-5310 - Fix uri_parser AttributeError when used directly (#2283) 2025-04-15 08:05:20 -04:00
Steven Silvester
3723edc199
PYTHON-5277 Convert remaining Evergreen functions to generated config (#2281) 2025-04-14 09:35:37 -05:00
Steven Silvester
5d14b3458e
PYTHON-5304 Create standard non-linux tests (#2275) 2025-04-14 08:09:08 -05:00
Shane Harvey
a8197a792e
PYTHON-5308 Remove SON from doc examples (#2280) 2025-04-11 14:58:03 -07:00
Steven Silvester
7ec9c07081
PYTHON-5303 Add missing gridfs synchronous init file (#2279) 2025-04-11 16:20:08 -05:00
Noah Stapp
7a0afcf0b9
PYTHON-5302 - Run ruff before synchro in pre-commit hooks (#2274) 2025-04-10 11:08:23 -04:00
Steven Silvester
cce4a0d179
PYTHON-5295 Update lockfile for compat with older versions of uv (#2271) 2025-04-10 09:55:46 -05:00
Steven Silvester
5b42ed8cac
PYTHON-5286 Create server version variants (#2270) 2025-04-10 09:31:50 -05:00
Noah Stapp
5b0862e78e
PYTHON-5297 - AsyncMongoClient connection error causes UnboundLocalError (#2273) 2025-04-10 10:30:09 -04:00
Jeffrey A. Clark
86e221eb5c
PYTHON-5288: SRV hostname validation fails when resolver and resolved hostnames are identical with three domain levels (#2272) 2025-04-09 18:00:04 -04:00
Noah Stapp
3c2ce16ad8
PYTHON-5283 - Skip test.test_monitor.TestMonitor.test_cleanup_executo… (#2268) 2025-04-09 15:14:35 -04:00
Casey Clements
fafa00e9e3
PYTHON-5126 & PYTHON-5280 Addresses issues raised in DRIVERS-3097 and DRIVERS-3123 (#2261) 2025-04-09 09:09:42 -04:00
Steven Silvester
175481e35d
PYTHON-5282 Move config utility functions to separate file (#2267) 2025-04-08 11:31:13 -05:00
Steven Silvester
93886286a3
PYTHON-5275 Fix handlig of FIPS build (#2266) 2025-04-08 11:30:17 -05:00
Noah Stapp
bc2cc1ed58
PYTHON-4924 - PoolClearedError should have TransientTransactionError … (#2244) 2025-04-08 11:39:06 -04:00
mongodb-dbx-release-bot[bot]
2c077ba8a4
BUMP 4.13.0.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-04-08 13:12:35 +00:00
612 changed files with 99975 additions and 26309 deletions

4
.codecov.yml Normal file
View 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

View File

@ -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

View File

@ -29,114 +29,6 @@ include:
- filename: .evergreen/generated_configs/tasks.yml - filename: .evergreen/generated_configs/tasks.yml
- filename: .evergreen/generated_configs/variants.yml - filename: .evergreen/generated_configs/variants.yml
functions:
"fetch source":
# Executes clone and applies the submitted patch, if any
- command: git.get_project
params:
directory: "src"
# Applies the subitted patch, if any
# Deprecated. Should be removed. But still needed for certain agents (ZAP)
- command: git.apply_patch
"setup system":
# Make an evergreen expansion file with dynamic values
- command: subprocess.exec
params:
include_expansions_in_env: ["is_patch", "project", "version_id"]
binary: bash
working_dir: "src"
args:
- .evergreen/scripts/setup-system.sh
# Load the expansion file to make an evergreen variable with the current unique version
- command: expansions.update
params:
file: src/expansion.yml
"upload test results":
- command: attach.results
params:
file_location: "${DRIVERS_TOOLS}/results.json"
- command: attach.xunit_results
params:
file: "src/xunit-results/TEST-*.xml"
"run server":
- command: subprocess.exec
params:
binary: bash
working_dir: "src"
include_expansions_in_env: [VERSION, TOPOLOGY, AUTH, SSL, ORCHESTRATION_FILE, PYTHON_BINARY, PYTHON_VERSION,
STORAGE_ENGINE, REQUIRE_API_VERSION, DRIVERS_TOOLS, TEST_CRYPT_SHARED, AUTH_AWS, LOAD_BALANCER, LOCAL_ATLAS]
args: [.evergreen/just.sh, run-server, "${TEST_NAME}"]
- command: expansions.update
params:
file: ${DRIVERS_TOOLS}/mo-expansion.yml
"run just script":
- command: subprocess.exec
type: test
params:
include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN]
binary: bash
working_dir: "src"
args: [.evergreen/just.sh, "${JUSTFILE_TARGET}"]
"run tests":
- command: subprocess.exec
type: test
params:
include_expansions_in_env: [AUTH, SSL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
AWS_SESSION_TOKEN, COVERAGE, PYTHON_BINARY, LIBMONGOCRYPT_URL, MONGODB_URI, PYTHON_VERSION,
DISABLE_TEST_COMMANDS, GREEN_FRAMEWORK, NO_EXT, COMPRESSORS, MONGODB_API_VERSION, DEBUG_LOG,
ORCHESTRATION_FILE, OCSP_SERVER_TYPE, VERSION]
binary: bash
working_dir: "src"
args: [.evergreen/just.sh, setup-tests, "${TEST_NAME}", "${SUB_TEST_NAME}"]
- command: subprocess.exec
type: test
params:
working_dir: "src"
binary: bash
args: [.evergreen/just.sh, run-tests]
"cleanup":
- command: subprocess.exec
params:
binary: bash
working_dir: "src"
args:
- .evergreen/scripts/cleanup.sh
"teardown system":
- command: subprocess.exec
params:
binary: bash
working_dir: "src"
args: [.evergreen/just.sh, teardown-tests]
- command: subprocess.exec
params:
binary: bash
working_dir: "src"
args:
- ${DRIVERS_TOOLS}/.evergreen/teardown.sh
"assume ec2 role":
- command: ec2.assume_role
params:
role_arn: ${aws_test_secrets_role}
duration_seconds: 3600
"attach benchmark test results":
- command: attach.results
params:
file_location: src/report.json
"send dashboard data":
- command: perf.send
params:
file: src/results.json
pre: pre:
- func: "fetch source" - func: "fetch source"
- func: "setup system" - func: "setup system"
@ -146,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

View File

@ -1,4 +1,27 @@
functions: functions:
# Assume ec2 role
assume ec2 role:
- command: ec2.assume_role
params:
role_arn: ${aws_test_secrets_role}
duration_seconds: 3600
# Attach benchmark test results
attach benchmark test results:
- command: attach.results
params:
file_location: src/report.json
# Cleanup
cleanup:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/scripts/cleanup.sh
working_dir: src
type: test
# Download and merge coverage # Download and merge coverage
download and merge coverage: download and merge coverage:
- command: ec2.assume_role - command: ec2.assume_role
@ -56,6 +79,200 @@ functions:
optional: "true" optional: "true"
type: setup type: setup
# Fetch source
fetch source:
- command: git.get_project
params:
directory: src
# Run server
run server:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/just.sh
- run-server
- ${TEST_NAME}
working_dir: src
include_expansions_in_env:
- VERSION
- TOPOLOGY
- AUTH
- SSL
- ORCHESTRATION_FILE
- UV_PYTHON
- TOOLCHAIN_VERSION
- STORAGE_ENGINE
- REQUIRE_API_VERSION
- DRIVERS_TOOLS
- TEST_CRYPT_SHARED
- AUTH_AWS
- LOAD_BALANCER
- LOCAL_ATLAS
- NO_EXT
type: test
- command: expansions.update
params:
file: ${DRIVERS_TOOLS}/mo-expansion.yml
# Run tests
run tests:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/just.sh
- setup-tests
- ${TEST_NAME}
- ${SUB_TEST_NAME}
working_dir: src
include_expansions_in_env:
- AUTH
- SSL
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_SESSION_TOKEN
- COVERAGE
- UV_PYTHON
- LIBMONGOCRYPT_URL
- MONGODB_URI
- TOOLCHAIN_VERSION
- DISABLE_TEST_COMMANDS
- GREEN_FRAMEWORK
- NO_EXT
- COMPRESSORS
- MONGODB_API_VERSION
- REQUIRE_API_VERSION
- DEBUG_LOG
- DISABLE_FLAKY
- ORCHESTRATION_FILE
- OCSP_SERVER_TYPE
- VERSION
- IS_WIN32
- REQUIRE_FIPS
- TEST_MIN_DEPS
type: test
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/just.sh
- run-tests
working_dir: src
type: test
# Send dashboard data
send dashboard data:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/scripts/perf-submission-setup.sh
working_dir: src
include_expansions_in_env:
- requester
- revision_order_id
- project_id
- version_id
- build_variant
- parsed_order_id
- task_name
- task_id
- execution
- is_mainline
type: test
- command: expansions.update
params:
file: src/expansion.yml
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/scripts/perf-submission.sh
working_dir: src
include_expansions_in_env:
- requester
- revision_order_id
- project_id
- version_id
- build_variant
- parsed_order_id
- task_name
- task_id
- execution
- is_mainline
type: test
# Setup system
setup system:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/scripts/setup-system.sh
working_dir: src
include_expansions_in_env:
- is_patch
- project
- version_id
type: test
- command: expansions.update
params:
file: src/expansion.yml
# Teardown system
teardown system:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/just.sh
- teardown-tests
working_dir: src
type: test
- command: subprocess.exec
params:
binary: bash
args:
- ${DRIVERS_TOOLS}/.evergreen/teardown.sh
working_dir: src
type: test
# Test numpy
test numpy:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/just.sh
- test-numpy
working_dir: src
include_expansions_in_env:
- TOOLCHAIN_VERSION
- COVERAGE
type: test
# Upload coverage codecov
upload codecov:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/scripts/upload-codecov.sh
working_dir: src
include_expansions_in_env:
- CODECOV_TOKEN
- build_variant
- task_name
- github_commit
- github_pr_number
- github_pr_head_branch
- github_author
- requester
- branch_name
type: test
# Upload coverage # Upload coverage
upload coverage: upload coverage:
- command: ec2.assume_role - command: ec2.assume_role
@ -115,3 +332,12 @@ functions:
display_name: drivers-tools-logs.tar.gz display_name: drivers-tools-logs.tar.gz
optional: "true" optional: "true"
type: setup type: setup
# Upload test results
upload test results:
- command: attach.results
params:
file_location: ${DRIVERS_TOOLS}/results.json
- command: attach.xunit_results
params:
file: src/xunit-results/TEST-*.xml

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

View File

@ -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/

View File

@ -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

View File

@ -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}"

View File

@ -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

View File

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

View File

@ -96,7 +96,7 @@ EOT
_bin_path="" _bin_path=""
if [ "Windows_NT" == "${OS:-}" ]; then if [ "Windows_NT" == "${OS:-}" ]; then
_bin_path="/cygdrive/c/Python/Current/Scripts" _bin_path="/cygdrive/c/Python/Current/Scripts"
elif [ "$(uname -s)" != "Darwin" ]; then elif [ "$(uname -s)" == "Darwin" ]; then
_bin_path="/Library/Frameworks/Python.Framework/Versions/Current/bin" _bin_path="/Library/Frameworks/Python.Framework/Versions/Current/bin"
else else
_bin_path="/opt/python/Current/bin" _bin_path="/opt/python/Current/bin"
@ -106,6 +106,7 @@ if [ -d "${_bin_path}" ]; then
if [ "Windows_NT" == "${OS:-}" ]; then if [ "Windows_NT" == "${OS:-}" ]; then
_suffix=".exe" _suffix=".exe"
fi fi
echo "Symlinking binaries from toolchain"
mkdir -p $PYMONGO_BIN_DIR mkdir -p $PYMONGO_BIN_DIR
ln -s ${_bin_path}/just${_suffix} $PYMONGO_BIN_DIR/just${_suffix} ln -s ${_bin_path}/just${_suffix} $PYMONGO_BIN_DIR/just${_suffix}
ln -s ${_bin_path}/uv${_suffix} $PYMONGO_BIN_DIR/uv${_suffix} ln -s ${_bin_path}/uv${_suffix} $PYMONGO_BIN_DIR/uv${_suffix}

View 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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,357 @@
from __future__ import annotations
from dataclasses import dataclass
from inspect import getmembers, isfunction
from itertools import cycle, zip_longest
from pathlib import Path
from typing import Any
from shrub.v3.evg_build_variant import BuildVariant
from shrub.v3.evg_command import (
EvgCommandType,
ec2_assume_role,
s3_put,
subprocess_exec,
)
from shrub.v3.evg_project import EvgProject
from shrub.v3.evg_task import EvgTaskRef
from shrub.v3.shrub_service import ShrubService
##############
# Globals
##############
ALL_VERSIONS = ["4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
CPYTHONS = ["3.10", "3.11", "3.12", "3.13", "3.14t", "3.14"]
PYPYS = ["pypy3.11"]
MIN_SUPPORT_VERSIONS = ["3.9", "pypy3.9", "pypy3.10"]
ALL_PYTHONS = CPYTHONS + PYPYS
MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]]
BATCHTIME_WEEK = 10080
BATCHTIME_DAY = 1440
AUTH_SSLS = [("auth", "ssl"), ("noauth", "ssl"), ("noauth", "nossl")]
TOPOLOGIES = ["standalone", "replica_set", "sharded_cluster"]
C_EXTS = ["without_ext", "with_ext"]
SYNCS = ["sync", "async"]
DISPLAY_LOOKUP = dict(
ssl=dict(ssl="SSL", nossl="NoSSL"),
auth=dict(auth="Auth", noauth="NoAuth"),
topology=dict(
standalone="Standalone", replica_set="Replica Set", sharded_cluster="Sharded Cluster"
),
test_suites=dict(default="Sync", default_async="Async"),
sync={"sync": "Sync", "async": "Async"},
coverage={"1": "cov"},
no_ext={"1": "No C"},
test_min_deps={"1": "Min Deps"},
)
HOSTS = dict()
@dataclass
class Host:
name: str
run_on: str
display_name: str
variables: dict[str, str] | None
# Hosts with toolchains.
HOSTS["rhel8"] = Host("rhel8", "rhel87-small", "RHEL8", dict())
HOSTS["win64"] = Host("win64", "windows-64-vsMulti-small", "Win64", dict())
HOSTS["win-latest"] = Host("win-latest", "windows-2022-latest-small", "WinLatest", dict())
HOSTS["win32"] = Host("win32", "windows-64-vsMulti-small", "Win32", dict())
HOSTS["macos"] = Host("macos", "macos-14", "macOS", dict())
HOSTS["macos-arm64"] = Host("macos-arm64", "macos-14-arm64", "macOS Arm64", dict())
HOSTS["ubuntu22"] = Host("ubuntu22", "ubuntu2204-small", "Ubuntu-22", dict())
HOSTS["ubuntu24"] = Host("ubuntu24", "ubuntu2404-small", "Ubuntu-24", dict())
HOSTS["perf"] = Host("perf", "rhel90-dbx-perf-large", "", dict())
HOSTS["debian11"] = Host("debian11", "debian11-small", "Debian11", dict())
DEFAULT_HOST = HOSTS["rhel8"]
# Other hosts
OTHER_HOSTS = ["RHEL9-FIPS", "RHEL8-zseries", "RHEL8-POWER8", "RHEL8-arm64", "Amazon2023"]
for name, run_on in zip(
OTHER_HOSTS,
[
"rhel92-fips",
"rhel8-zseries-small",
"rhel8-power-small",
"rhel82-arm64-small",
"amazon2023-arm64-latest-large-m8g",
],
):
HOSTS[name] = Host(name, run_on, name, dict())
##############
# Helpers
##############
def create_variant_generic(
tasks: list[str | EvgTaskRef],
display_name: str,
*,
host: Host | str | None = None,
default_run_on="rhel87-small",
expansions: dict | None = None,
**kwargs: Any,
) -> BuildVariant:
"""Create a build variant for the given inputs."""
task_refs = []
if isinstance(host, str):
host = HOSTS[host]
for t in tasks:
if isinstance(t, EvgTaskRef):
task_refs.append(t)
else:
task_refs.append(EvgTaskRef(name=t))
expansions = expansions and expansions.copy() or dict()
if "run_on" in kwargs:
run_on = kwargs.pop("run_on")
elif host:
run_on = [host.run_on]
if host.variables:
expansions.update(host.variables)
else:
run_on = [default_run_on]
if isinstance(run_on, str):
run_on = [run_on]
name = display_name.replace(" ", "-").replace("*-", "").lower()
return BuildVariant(
name=name,
display_name=display_name,
tasks=task_refs,
expansions=expansions or None,
run_on=run_on,
**kwargs,
)
def create_variant(
tasks: list[str | EvgTaskRef],
display_name: str,
*,
version: str | None = None,
host: Host | str | None = None,
expansions: dict | None = None,
**kwargs: Any,
) -> BuildVariant:
expansions = expansions and expansions.copy() or dict()
if version:
expansions["VERSION"] = version
# 8.0+ Windows builds must run on win-latest
if (
"win64" in display_name.lower()
or "win32" in display_name.lower()
and version
and version >= "8.0"
):
kwargs["run_on"] = HOSTS["win-latest"].run_on
return create_variant_generic(
tasks, display_name, version=version, host=host, expansions=expansions, **kwargs
)
def get_versions_from(min_version: str) -> list[str]:
"""Get all server versions starting from a minimum version."""
min_version_float = float(min_version)
rapid_latest = ["rapid", "latest"]
versions = [v for v in ALL_VERSIONS if v not in rapid_latest]
return [v for v in versions if float(v) >= min_version_float] + rapid_latest
def get_versions_until(max_version: str) -> list[str]:
"""Get all server version up to a max version."""
max_version_float = float(max_version)
versions = [v for v in ALL_VERSIONS if v not in ["rapid", "latest"]]
versions = [v for v in versions if float(v) <= max_version_float]
if not len(versions):
raise ValueError(f"No server versions found less <= {max_version}")
return versions
def get_common_name(base: str, sep: str, **kwargs) -> str:
display_name = base
version = kwargs.pop("VERSION", None)
version = version or kwargs.pop("version", None)
if version:
if version not in ["rapid", "latest"]:
version = f"v{version}"
display_name = f"{display_name}{sep}{version}"
for key, value in kwargs.items():
name = value
if key.lower() in ["python", "toolchain_version"]:
if not value.startswith("pypy"):
name = f"Python{value}"
else:
name = f"PyPy{value.replace('pypy', '')}"
elif key.lower() in DISPLAY_LOOKUP and value in DISPLAY_LOOKUP[key.lower()]:
name = DISPLAY_LOOKUP[key.lower()][value]
else:
continue
display_name = f"{display_name}{sep}{name}"
return display_name
def get_variant_name(base: str, host: str | Host | None = None, **kwargs) -> str:
"""Get the display name of a variant."""
display_name = base
if isinstance(host, str):
host = HOSTS[host]
if host is not None:
display_name += f" {host.display_name}"
return get_common_name(display_name, " ", **kwargs)
def get_task_name(base: str, **kwargs):
return get_common_name(base, "-", **kwargs).replace(" ", "-").lower()
def zip_cycle(*iterables, empty_default=None):
"""Get all combinations of the inputs, cycling over the shorter list(s)."""
cycles = [cycle(i) for i in iterables]
for _ in zip_longest(*iterables):
yield tuple(next(i, empty_default) for i in cycles)
def handle_c_ext(c_ext, expansions) -> None:
"""Handle c extension option."""
if c_ext == C_EXTS[0]:
expansions["NO_EXT"] = "1"
def get_standard_auth_ssl(topology):
auth = "auth" if topology == "sharded_cluster" else "noauth"
ssl = "nossl" if topology == "standalone" else "ssl"
return auth, ssl
def get_assume_role(**kwargs):
kwargs.setdefault("command_type", EvgCommandType.SETUP)
kwargs.setdefault("role_arn", "${assume_role_arn}")
return ec2_assume_role(**kwargs)
def get_subprocess_exec(**kwargs):
kwargs.setdefault("binary", "bash")
kwargs.setdefault("working_dir", "src")
kwargs.setdefault("command_type", EvgCommandType.TEST)
return subprocess_exec(**kwargs)
def get_s3_put(**kwargs):
kwargs["aws_key"] = "${AWS_ACCESS_KEY_ID}"
kwargs["aws_secret"] = "${AWS_SECRET_ACCESS_KEY}" # noqa:S105
kwargs["aws_session_token"] = "${AWS_SESSION_TOKEN}" # noqa:S105
kwargs["bucket"] = "${bucket_name}"
kwargs.setdefault("optional", "true")
kwargs.setdefault("permissions", "public-read")
kwargs.setdefault("content_type", "${content_type|application/x-gzip}")
kwargs.setdefault("command_type", EvgCommandType.SETUP)
return s3_put(**kwargs)
def generate_yaml(tasks=None, variants=None):
"""Generate the yaml for a given set of tasks and variants."""
project = EvgProject(tasks=tasks, buildvariants=variants)
out = ShrubService.generate_yaml(project)
# Dedent by two spaces to match what we use in config.yml
lines = [line[2:] for line in out.splitlines()]
print("\n".join(lines))
##################
# Generate Config
##################
def write_variants_to_file(mod):
here = Path(__file__).absolute().parent
target = here.parent / "generated_configs" / "variants.yml"
if target.exists():
target.unlink()
with target.open("w") as fid:
fid.write("buildvariants:\n")
for name, func in sorted(getmembers(mod, isfunction)):
if not name.endswith("_variants"):
continue
if not name.startswith("create_"):
raise ValueError("Variant creators must start with create_")
title = name.replace("create_", "").replace("_variants", "").replace("_", " ").capitalize()
project = EvgProject(tasks=None, buildvariants=func())
out = ShrubService.generate_yaml(project).splitlines()
with target.open("a") as fid:
fid.write(f" # {title} tests\n")
for line in out[1:]:
fid.write(f"{line}\n")
fid.write("\n")
# Remove extra trailing newline:
data = target.read_text().splitlines()
with target.open("w") as fid:
for line in data[:-1]:
fid.write(f"{line}\n")
def write_tasks_to_file(mod):
here = Path(__file__).absolute().parent
target = here.parent / "generated_configs" / "tasks.yml"
if target.exists():
target.unlink()
with target.open("w") as fid:
fid.write("tasks:\n")
for name, func in sorted(getmembers(mod, isfunction)):
if name.startswith("_") or not name.endswith("_tasks"):
continue
if not name.startswith("create_"):
raise ValueError("Task creators must start with create_")
title = name.replace("create_", "").replace("_tasks", "").replace("_", " ").capitalize()
project = EvgProject(tasks=func(), buildvariants=None)
out = ShrubService.generate_yaml(project).splitlines()
with target.open("a") as fid:
fid.write(f" # {title} tests\n")
for line in out[1:]:
fid.write(f"{line}\n")
fid.write("\n")
# Remove extra trailing newline:
data = target.read_text().splitlines()
with target.open("w") as fid:
for line in data[:-1]:
fid.write(f"{line}\n")
def write_functions_to_file(mod):
here = Path(__file__).absolute().parent
target = here.parent / "generated_configs" / "functions.yml"
if target.exists():
target.unlink()
with target.open("w") as fid:
fid.write("functions:\n")
functions = dict()
for name, func in sorted(getmembers(mod, isfunction)):
if name.startswith("_") or not name.endswith("_func"):
continue
if not name.startswith("create_"):
raise ValueError("Function creators must start with create_")
title = name.replace("create_", "").replace("_func", "").replace("_", " ").capitalize()
func_name, cmds = func()
functions = dict()
functions[func_name] = cmds
project = EvgProject(functions=functions, tasks=None, buildvariants=None)
out = ShrubService.generate_yaml(project).splitlines()
with target.open("a") as fid:
fid.write(f" # {title}\n")
for line in out[1:]:
fid.write(f"{line}\n")
fid.write("\n")
# Remove extra trailing newline:
data = target.read_text().splitlines()
with target.open("w") as fid:
for line in data[:-1]:
fid.write(f"{line}\n")

View File

@ -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})
@ -10,60 +10,27 @@ if [ -f $HERE/env.sh ]; then
. $HERE/env.sh . $HERE/env.sh
fi fi
_BIN_DIR=${PYMONGO_BIN_DIR:-$HOME/.local/bin} # Set up the default bin directory.
export PATH="$PATH:${_BIN_DIR}" if [ -z "${PYMONGO_BIN_DIR:-}" ]; then
PYMONGO_BIN_DIR="$HOME/.local/bin"
# 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)" $_BIN_DIR/${2}${_suffix}
# uv also comes with a uvx binary.
if [ $2 == "uv" ]; then
ln -s "$(which uvx)" $_BIN_DIR/uvx${_suffix}
fi
echo "Installed to ${_BIN_DIR}"
echo "Installing $2 using pip... done."
}
# Ensure just is installed.
if ! command -v just >/dev/null 2>&1; then
# On most systems we can install directly.
_TARGET=""
if [ "Windows_NT" = "${OS:-}" ]; then
_TARGET="--target x86_64-pc-windows-msvc"
fi
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
# Install uv. # Ensure uv is installed.
if ! command -v uv >/dev/null 2>&1; then if ! command -v uv &>/dev/null; then
_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

View File

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

View File

@ -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}")

View File

@ -0,0 +1,15 @@
#!/bin/bash
# We use the requester expansion to determine whether the data is from a mainline evergreen run or not
set -eu
# shellcheck disable=SC2154
if [ "${requester}" == "commit" ]; then
echo "is_mainline: true" >> expansion.yml
else
echo "is_mainline: false" >> expansion.yml
fi
# We parse the username out of the order_id as patches append that in and SPS does not need that information
# shellcheck disable=SC2154
echo "parsed_order_id: $(echo "${revision_order_id}" | awk -F'_' '{print $NF}')" >> expansion.yml

View File

@ -0,0 +1,25 @@
#!/bin/bash
# We use the requester expansion to determine whether the data is from a mainline evergreen run or not
set -eu
# Submit the performance data to the SPS endpoint
# shellcheck disable=SC2154
response=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X 'POST' \
"https://performance-monitoring-api.corp.mongodb.com/raw_perf_results/cedar_report?project=${project_id}&version=${version_id}&variant=${build_variant}&order=${parsed_order_id}&task_name=${task_name}&task_id=${task_id}&execution=${execution}&mainline=${is_mainline}" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d @results.json)
http_status=$(echo "$response" | grep "HTTP_STATUS" | awk -F':' '{print $2}')
response_body=$(echo "$response" | sed '/HTTP_STATUS/d')
# We want to throw an error if the data was not successfully submitted
if [ "$http_status" -ne 200 ]; then
echo "Error: Received HTTP status $http_status"
echo "Response Body: $response_body"
exit 1
fi
echo "Response Body: $response_body"
echo "HTTP Status: $http_status"

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

View 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

View File

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

View File

@ -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()
@ -152,6 +173,13 @@ def run() -> None:
test_kms_send_to_remote(SUB_TEST_NAME) test_kms_send_to_remote(SUB_TEST_NAME)
return return
# Handle doctests.
if TEST_NAME == "doctest":
from sphinx.cmd.build import main
result = main("-E -b doctest doc ./doc/_build/doctest".split())
sys.exit(result)
# Send ecs tests to run remotely. # Send ecs tests to run remotely.
if TEST_NAME == "auth_aws" and SUB_TEST_NAME == "ecs": if TEST_NAME == "auth_aws" and SUB_TEST_NAME == "ecs":
run_command(f"{DRIVERS_TOOLS}/.evergreen/auth_aws/aws_setup.sh ecs") run_command(f"{DRIVERS_TOOLS}/.evergreen/auth_aws/aws_setup.sh ecs")
@ -174,7 +202,17 @@ def run() -> None:
return return
if os.environ.get("DEBUG_LOG"): if os.environ.get("DEBUG_LOG"):
TEST_ARGS.extend(f"-o log_cli_level={logging.DEBUG} -o log_cli=1".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:])

View File

@ -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

View File

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

View File

@ -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})
@ -20,4 +21,6 @@ if [ -f $SCRIPT_DIR/env.sh ]; then
source $SCRIPT_DIR/env.sh source $SCRIPT_DIR/env.sh
fi fi
uv run $SCRIPT_DIR/setup_tests.py "$@" echo "Setting up tests with args \"$*\"..."
uv run ${USE_ACTIVE_VENV:+--active} "$SCRIPT_DIR/setup_tests.py" "$@"
echo "Setting up tests with args \"$*\"... done."

View 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

View File

@ -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,9 @@ 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",
"IS_WIN32",
] ]
# Map the test name to test extra. # Map the test name to test extra.
@ -51,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:
@ -88,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"
@ -109,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")
@ -146,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}"
@ -158,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()}")
@ -180,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
@ -216,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":
@ -253,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":
@ -283,6 +274,9 @@ def handle_test_env() -> None:
write_env("GSSAPI_PORT", config["SASL_PORT"]) write_env("GSSAPI_PORT", config["SASL_PORT"])
write_env("GSSAPI_PRINCIPAL", config["PRINCIPAL"]) write_env("GSSAPI_PRINCIPAL", config["PRINCIPAL"])
if test_name == "doctest":
UV_ARGS.append("--extra docs")
if test_name == "load_balancer": if test_name == "load_balancer":
SINGLE_MONGOS_LB_URI = os.environ.get( SINGLE_MONGOS_LB_URI = os.environ.get(
"SINGLE_MONGOS_LB_URI", "mongodb://127.0.0.1:8000/?loadBalanced=true" "SINGLE_MONGOS_LB_URI", "mongodb://127.0.0.1:8000/?loadBalanced=true"
@ -334,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,
@ -362,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"
@ -385,14 +382,14 @@ def handle_test_env() -> None:
if not DRIVERS_TOOLS: if not DRIVERS_TOOLS:
raise RuntimeError("Missing DRIVERS_TOOLS") raise RuntimeError("Missing DRIVERS_TOOLS")
csfle_dir = Path(f"{DRIVERS_TOOLS}/.evergreen/csfle") csfle_dir = Path(f"{DRIVERS_TOOLS}/.evergreen/csfle")
run_command(f"bash {csfle_dir}/setup-secrets.sh", cwd=csfle_dir) run_command(f"bash {csfle_dir.as_posix()}/setup-secrets.sh", cwd=csfle_dir)
load_config_from_file(csfle_dir / "secrets-export.sh") load_config_from_file(csfle_dir / "secrets-export.sh")
run_command(f"bash {csfle_dir}/start-servers.sh") run_command(f"bash {csfle_dir.as_posix()}/start-servers.sh")
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)
@ -424,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():
@ -451,21 +462,21 @@ 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:
# Use --capture=tee-sys so pytest prints test output inline: TEST_ARGS = f"-v --durations=5 {TEST_ARGS}"
# https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html
TEST_ARGS = f"-v --capture=tee-sys --durations=5 {TEST_ARGS}"
TEST_SUITE = TEST_SUITE_MAP.get(test_name) TEST_SUITE = TEST_SUITE_MAP.get(test_name)
if TEST_SUITE: if TEST_SUITE:
TEST_ARGS = f"-m {TEST_SUITE} {TEST_ARGS}" TEST_ARGS = f"-m {TEST_SUITE} {TEST_ARGS}"

View File

@ -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

View File

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

View 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

View File

@ -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",
@ -43,19 +42,27 @@ TEST_SUITE_MAP = {
"kms": "kms", "kms": "kms",
"load_balancer": "load_balancer", "load_balancer": "load_balancer",
"mockupdb": "mockupdb", "mockupdb": "mockupdb",
"pyopenssl": "",
"ocsp": "ocsp", "ocsp": "ocsp",
"perf": "perf", "perf": "perf",
"serverless": "", "numpy": "",
} }
# Tests that require a sub test suite. # Tests that require a sub test suite.
SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi", "perf"] SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi", "perf"]
EXTRA_TESTS = ["mod_wsgi", "aws_lambda"] 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 = ["auth_oidc", "atlas_connect", "data_lake", "mockupdb", "serverless", "ocsp"] NO_RUN_ORCHESTRATION = [
"auth_oidc",
"atlas_connect",
"aws_lambda",
"mockupdb",
"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(
@ -78,7 +85,7 @@ def get_test_options(
else: else:
parser.add_argument( parser.add_argument(
"test_name", "test_name",
choices=set(TEST_SUITE_MAP) - set(NO_RUN_ORCHESTRATION), choices=set(list(TEST_SUITE_MAP) + EXTRA_TESTS) - set(NO_RUN_ORCHESTRATION),
nargs="?", nargs="?",
default="default", default="default",
help="The optional name of the test suite to be run, which informs the server configuration.", help="The optional name of the test suite to be run, which informs the server configuration.",
@ -91,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:
@ -101,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(
@ -124,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:
@ -186,6 +223,6 @@ def run_command(cmd: str | list[str], **kwargs: Any) -> None:
def create_archive() -> str: def create_archive() -> str:
run_command("git add .", cwd=ROOT) run_command("git add .", cwd=ROOT)
run_command('git commit -m "add files"', check=False, cwd=ROOT) run_command('git commit --no-verify -m "add files"', check=False, cwd=ROOT)
run_command(f"git archive -o {TMP_DRIVER_FILE} HEAD", cwd=ROOT) run_command(f"git archive -o {TMP_DRIVER_FILE} HEAD", cwd=ROOT)
return TMP_DRIVER_FILE return TMP_DRIVER_FILE

View File

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

View 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": [

View 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": []
+ }
+ ]
+ }
+ }
+ ]
+}

View 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",

View 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"
- ]
- }
- }
}
]
}

View 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"
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
}
]
}

View 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": []
+ }
+ ]
+}

View File

@ -1,136 +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')
PYTHON="C:/python/Python$version/python.exe"
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
View File

@ -0,0 +1 @@
* @mongodb/dbx-python

44
.github/copilot-instructions.md vendored Normal file
View 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)
```

View File

@ -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
View 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 didnt 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?

View File

@ -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@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 }}
@ -54,7 +54,6 @@ jobs:
queries: security-extended queries: security-extended
config: | config: |
paths-ignore: paths-ignore:
- '.github/**'
- 'doc/**' - 'doc/**'
- 'tools/**' - 'tools/**'
- 'test/**' - 'test/**'
@ -64,6 +63,6 @@ jobs:
pip install -e . pip install -e .
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@ -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 }}

View File

@ -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@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: "./*"

View File

@ -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
-->

View File

@ -3,12 +3,8 @@ name: Release
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version:
description: "The new version to set"
required: true
following_version: following_version:
description: "The post (dev) version to set" description: "The post (dev) version to set"
required: true
dry_run: dry_run:
description: "Dry Run?" description: "Dry Run?"
default: false default: false
@ -26,7 +22,6 @@ env:
# to 'false' when the input is set to 'false'. # to 'false' when the input is set to 'false'.
DRY_RUN: ${{ ! contains(inputs.dry_run, 'false') }} DRY_RUN: ${{ ! contains(inputs.dry_run, 'false') }}
FOLLOWING_VERSION: ${{ inputs.following_version || '' }} FOLLOWING_VERSION: ${{ inputs.following_version || '' }}
VERSION: ${{ inputs.version || '10.10.10.10' }}
defaults: defaults:
run: run:
@ -43,20 +38,18 @@ 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:
version: ${{ env.VERSION }}
dry_run: ${{ env.DRY_RUN }} dry_run: ${{ env.DRY_RUN }}
build-dist: build-dist:
@ -82,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@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@release/v1 uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
post-publish: post-publish:
needs: [publish] needs: [publish]
@ -106,19 +99,17 @@ 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:
version: ${{ env.VERSION }}
following_version: ${{ env.FOLLOWING_VERSION }} following_version: ${{ env.FOLLOWING_VERSION }}
product_name: ${{ env.PRODUCT_NAME }} product_name: ${{ env.PRODUCT_NAME }}
evergreen_project: ${{ env.EVERGREEN_PROJECT }} evergreen_project: ${{ env.EVERGREEN_PROJECT }}

104
.github/workflows/sbom.yml vendored Normal file
View 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

View File

@ -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@v3
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@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,71 +53,102 @@ 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@v3
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@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@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@v3
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@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@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
run: just docs-test run: |
just setup-tests doctest
just run-tests
docs: docs:
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@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@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
@ -124,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@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@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
@ -144,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@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@v3 run: uv tool install rust-just
- name: Install dependencies - name: Install dependencies
run: | run: |
just install just install
@ -163,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
@ -193,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: |
@ -202,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@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: |
@ -218,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

View File

@ -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@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@v3
with:
sarif_file: results.sarif
category: zizmor

7
.github/zizmor.yml vendored Normal file
View 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
View File

@ -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

View File

@ -18,6 +18,14 @@ repos:
exclude: .patch exclude: .patch
exclude_types: [json] exclude_types: [json]
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.3
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
- id: ruff-format
- repo: local - repo: local
hooks: hooks:
- id: synchro - id: synchro
@ -30,14 +38,6 @@ repos:
- ruff==0.1.3 - ruff==0.1.3
- unasync - unasync
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.3
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
- id: ruff-format
- repo: https://github.com/adamchainz/blacken-docs - repo: https://github.com/adamchainz/blacken-docs
rev: "1.16.0" rev: "1.16.0"
hooks: hooks:
@ -121,4 +121,15 @@ repos:
entry: .evergreen/scripts/generate-config.sh entry: .evergreen/scripts/generate-config.sh
language: python language: python
require_serial: true require_serial: true
additional_dependencies: ["shrub.py>=3.9.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"

View File

@ -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,24 +220,53 @@ 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.
- Run `just setup-tests encryption`. - Run `just setup-tests encryption`.
- Run the tests with `just run-tests`. - Run the tests with `just run-tests`.
To test with `encryption` and `PyOpenSSL`, use `just setup-tests encryption pyopenssl`.
### PyOpenSSL tests
- Run `just run-server` to start the server.
- Run `just setup-tests default_sync pyopenssl`.
- Run the tests with `just run-tests`.
Note: `PyOpenSSL` is not used in async tests, but you can use `just setup-tests default_async pyopenssl`
to verify that PyMongo falls back to the standard library `OpenSSL`.
### Load balancer tests ### Load balancer tests
- Install `haproxy` (available as `brew install haproxy` on macOS). - Install `haproxy` (available as `brew install haproxy` on macOS).
@ -294,7 +330,8 @@ Note: these tests can only be run from an Evergreen host.
The doc tests require a running server. The doc tests require a running server.
- Run `just run-server`. - Run `just run-server`.
- Run `just docs-test`. - Run `just setup-tests doctest`.
- Run `just run-tests`.
### Free-threaded Python Tests ### Free-threaded Python Tests
@ -303,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
@ -323,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`.
@ -351,17 +381,27 @@ If you are running one of the `no-responder` tests, omit the `run-server` step.
## Enable Debug Logs ## Enable Debug Logs
- Use `-o log_cli_level="DEBUG" -o log_cli=1` with `just test` or `pytest`. - Use `-o log_cli_level="DEBUG" -o log_cli=1` with `just test` or `pytest` to output all debug logs to the terminal. **Warning**: This will output a huge amount of logs.
- Add `log_cli_level = "DEBUG` and `log_cli = 1` to the `tool.pytest.ini_options` section in `pyproject.toml` for Evergreen patches or to enable debug logs by default on your machine. - Add `log_cli=1` and `log_cli_level="DEBUG"` to the `tool.pytest.ini_options` section in `pyproject.toml` to enable debug logs in this manner by default on your machine.
- You can also set `DEBUG_LOG=1` and run either `just setup-tests` or `just-test`. - Set `DEBUG_LOG=1` and run `just setup-tests`, `just-test`, or `pytest` to enable debug logs only for failed tests.
- 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 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`.
@ -369,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
@ -393,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.
@ -415,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.

View File

@ -4,17 +4,25 @@
[![Python Versions](https://img.shields.io/pypi/pyversions/pymongo)](https://pypi.org/project/pymongo) [![Python Versions](https://img.shields.io/pypi/pyversions/pymongo)](https://pypi.org/project/pymongo)
[![Monthly Downloads](https://static.pepy.tech/badge/pymongo/month)](https://pepy.tech/project/pymongo) [![Monthly Downloads](https://static.pepy.tech/badge/pymongo/month)](https://pepy.tech/project/pymongo)
[![API Documentation Status](https://readthedocs.org/projects/pymongo/badge/?version=stable)](http://pymongo.readthedocs.io/en/stable/api?badge=stable) [![API Documentation Status](https://readthedocs.org/projects/pymongo/badge/?version=stable)](http://pymongo.readthedocs.io/en/stable/api?badge=stable)
[![codecov](https://codecov.io/gh/mongodb/mongo-python-driver/graph/badge.svg?branch=master)](https://codecov.io/gh/mongodb/mongo-python-driver)
## About ## About
The PyMongo distribution contains tools for interacting with MongoDB The PyMongo distribution contains tools for interacting with MongoDB
database from Python. The `bson` package is an implementation of the database from Python. The `bson` package is an implementation of the
[BSON format](http://bsonspec.org) for Python. The `pymongo` package is [BSON format](http://bsonspec.org) for Python. The `pymongo` package is
a native Python driver for MongoDB. The `gridfs` package is a a native Python driver for MongoDB, offering both synchronous and asynchronous APIs. The `gridfs` package is 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).

View File

@ -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

View File

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

View File

@ -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;
@ -3227,11 +3417,18 @@ _cbson_exec(PyObject *m)
INITERROR; INITERROR;
} }
#if PY_VERSION_HEX >= 0x030D0000
if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
Py_DECREF(m);
INITERROR;
}
# else
if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) { if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) {
Py_DECREF(c_api_object); Py_DECREF(c_api_object);
Py_DECREF(m); Py_DECREF(m);
INITERROR; INITERROR;
} }
#endif
return 0; return 0;
} }

View File

@ -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 */

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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}')"

View File

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

View File

@ -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

View File

@ -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]

View File

@ -16,6 +16,7 @@
.. autodata:: MD5_SUBTYPE .. autodata:: MD5_SUBTYPE
.. autodata:: COLUMN_SUBTYPE .. autodata:: COLUMN_SUBTYPE
.. autodata:: SENSITIVE_SUBTYPE .. autodata:: SENSITIVE_SUBTYPE
.. autodata:: VECTOR_SUBTYPE
.. autodata:: USER_DEFINED_SUBTYPE .. autodata:: USER_DEFINED_SUBTYPE
.. autoclass:: UuidRepresentation .. autoclass:: UuidRepresentation

View File

@ -1,10 +1,6 @@
:mod:`gridfs async` -- Async tools for working with GridFS :mod:`gridfs async` -- Async tools for working with GridFS
========================================================== ==========================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: gridfs.asynchronous .. automodule:: gridfs.asynchronous
:synopsis: Async tools for working with GridFS :synopsis: Async tools for working with GridFS

View File

@ -1,10 +1,6 @@
:mod:`change_stream` -- Watch changes on a collection, database, or cluster :mod:`change_stream` -- Watch changes on a collection, database, or cluster
=========================================================================== ===========================================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: pymongo.asynchronous.change_stream .. automodule:: pymongo.asynchronous.change_stream
:members: :members:

View File

@ -1,10 +1,6 @@
:mod:`client_session` -- Logical sessions for sequential operations :mod:`client_session` -- Logical sessions for sequential operations
=================================================================== ===================================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: pymongo.asynchronous.client_session .. automodule:: pymongo.asynchronous.client_session
:members: :members:

View File

@ -1,10 +1,6 @@
:mod:`collection` -- Collection level operations :mod:`collection` -- Collection level operations
================================================ ================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: pymongo.asynchronous.collection .. automodule:: pymongo.asynchronous.collection
:synopsis: Collection level operations :synopsis: Collection level operations

View File

@ -1,11 +1,8 @@
:mod:`command_cursor` -- Tools for iterating over MongoDB command results :mod:`command_cursor` -- Tools for iterating over MongoDB command results
========================================================================= =========================================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. 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:

View File

@ -1,16 +1,14 @@
:mod:`cursor` -- Tools for iterating over MongoDB query results :mod:`cursor` -- Tools for iterating over MongoDB query results
=============================================================== ===============================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: pymongo.asynchronous.cursor .. automodule:: pymongo.asynchronous.cursor
:synopsis: Tools for iterating over MongoDB query results :synopsis: Tools for iterating over MongoDB query results
.. 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]

View File

@ -1,10 +1,6 @@
:mod:`database` -- Database level operations :mod:`database` -- Database level operations
============================================ ============================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: pymongo.asynchronous.database .. automodule:: pymongo.asynchronous.database
:synopsis: Database level operations :synopsis: Database level operations

View File

@ -1,10 +1,6 @@
:mod:`pymongo async` -- Async Python driver for MongoDB :mod:`pymongo async` -- Async Python driver for MongoDB
======================================================= =======================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: pymongo.asynchronous .. automodule:: pymongo.asynchronous
:synopsis: Asynchronous Python driver for MongoDB :synopsis: Asynchronous Python driver for MongoDB

View File

@ -1,10 +1,6 @@
:mod:`mongo_client` -- Tools for connecting to MongoDB :mod:`mongo_client` -- Tools for connecting to MongoDB
====================================================== ======================================================
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. automodule:: pymongo.asynchronous.mongo_client .. automodule:: pymongo.asynchronous.mongo_client
:synopsis: Tools for connecting to MongoDB :synopsis: Tools for connecting to MongoDB

View File

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

View File

@ -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]

View File

@ -1,429 +0,0 @@
Async Tutorial
==============
.. warning:: This API is currently in beta, meaning the classes, methods,
and behaviors described within may change before the full release.
If you come across any bugs during your use of this API,
please file a Jira ticket in the "Python Driver" project at https://jira.mongodb.org/browse/PYTHON.
.. 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.

View File

@ -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

View File

@ -1,6 +1,279 @@
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)
--------------------------------------
PyMongo 4.13 brings a number of changes including:
- The asynchronous API is now stable and no longer in beta.
See the :mod:`pymongo.asynchronous` docs
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
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
...............
See the `PyMongo 4.13 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _PyMongo 4.13 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=42509
Changes in Version 4.12.1 (2025/04/29)
--------------------------------------
Version 4.12.1 is a bug fix release.
- Fixed a bug that could raise ``UnboundLocalError`` when creating asynchronous connections over SSL.
- Fixed a bug causing SRV hostname validation to fail when resolver and resolved hostnames are identical with three domain levels.
- Fixed a bug that caused direct use of ``pymongo.uri_parser`` to raise an ``AttributeError``.
- Fixed a bug where clients created with connect=False and a "mongodb+srv://" connection string
could cause public ``pymongo.MongoClient`` and ``pymongo.AsyncMongoClient`` attributes (topology_description,
nodes, address, primary, secondaries, arbiters) to incorrectly return a Database, leading to type
errors such as: "NotImplementedError: Database objects do not implement truth value testing or bool()".
- Removed Eventlet testing against Python versions newer than 3.9 since
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
- Fixed a bug where MongoDB cluster topology changes could cause asynchronous operations to take much longer to complete
due to holding the Topology lock while closing stale connections.
- Fixed a bug that would cause AsyncMongoClient to attempt to use PyOpenSSL when available, resulting in errors such as
"pymongo.errors.ServerSelectionTimeoutError: 'SSLContext' object has no attribute 'wrap_bio'".
Issues Resolved
...............
See the `PyMongo 4.12.1 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _PyMongo 4.12.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43094
Changes in Version 4.12.0 (2025/04/08) Changes in Version 4.12.0 (2025/04/08)
-------------------------------------- --------------------------------------
@ -12,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`
@ -74,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`_).
@ -196,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`,
@ -206,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`,
@ -236,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
@ -343,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.
@ -505,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
@ -530,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
@ -591,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+.
@ -619,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
@ -656,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``
@ -665,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``
@ -675,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
@ -708,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.
@ -758,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`_).
@ -821,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
@ -910,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`.
@ -1249,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`,
@ -1306,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
@ -1404,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
@ -1435,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
@ -1519,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
@ -1545,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.
@ -1647,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>`_
@ -1674,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
@ -1704,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:
@ -1772,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`.
@ -1795,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
@ -1815,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
@ -2065,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.
@ -2094,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
@ -2432,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
.................................................. ..................................................
@ -2476,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
@ -2484,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
@ -2498,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.
@ -2836,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
............... ...............
@ -3075,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`
@ -3086,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.
@ -3166,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.
@ -3186,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
............... ...............
@ -3232,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.
@ -3339,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
@ -3393,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>`_.
@ -4002,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`.
@ -4069,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

View File

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

View File

@ -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

View File

@ -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"
@ -105,6 +113,7 @@ from pymongo.mongo_client import MongoClient
client = MongoClient() client = MongoClient()
client.drop_database("doctest_test") client.drop_database("doctest_test")
db = client.doctest_test db = client.doctest_test
server_major_version = int(client.server_info()['version'].split()[-1][0])
""" """
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
@ -179,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,

View File

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

View File

@ -1,9 +0,0 @@
Developer Guide
===============
Technical guide for contributors to PyMongo.
.. toctree::
:maxdepth: 1
periodic_executor

View File

@ -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

View File

@ -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>`_

View File

@ -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

View File

@ -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': []}

View File

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

View File

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

View File

@ -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

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