Skip to content

Commit f218620

Browse files
aclark4lifetimgraham
authored andcommitted
INTPYTHON-424 add django_mongodb.parse_uri() to configure DATABASES
1 parent c242744 commit f218620

File tree

5 files changed

+150
-2
lines changed

5 files changed

+150
-2
lines changed

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,59 @@ to this:
110110
DATABASES = {
111111
"default": {
112112
"ENGINE": "django_mongodb",
113+
"HOST": "mongodb+srv://cluster0.example.mongodb.net",
113114
"NAME": "my_database",
114115
"USER": "my_user",
115116
"PASSWORD": "my_password",
116-
"OPTIONS": {...},
117+
"PORT": 27017,
118+
"OPTIONS": {
119+
# Example:
120+
"retryWrites": "true",
121+
"w": "majority",
122+
"tls": "false",
123+
},
117124
},
118125
}
119126
```
120127

128+
For a localhost configuration, you can omit `HOST` or specify
129+
`"HOST": "localhost"`.
130+
131+
`HOST` only needs a scheme prefix for SRV connections (`mongodb+srv://`). A
132+
`mongodb://` prefix is never required.
133+
121134
`OPTIONS` is an optional dictionary of parameters that will be passed to
122135
[`MongoClient`](https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html).
123136

137+
`USER`, `PASSWORD`, and `PORT` (if 27017) may also be optional.
138+
139+
For a replica set or sharded cluster where you have multiple hosts, include
140+
all of them in `HOST`, e.g.
141+
`"mongodb://mongos0.example.com:27017,mongos1.example.com:27017"`.
142+
143+
Alternatively, if you prefer to simply paste in a MongoDB URI rather than parse
144+
it into the format above, you can use:
145+
146+
```python
147+
import django_mongodb
148+
149+
MONGODB_URI = "mongodb+srv://my_user:[email protected]/myDatabase?retryWrites=true&w=majority&tls=false"
150+
DATABASES["default"] = django_mongodb.parse_uri(MONGODB_URI)
151+
```
152+
153+
This constructs a `DATABASES` setting equivalent to the first example.
154+
155+
#### `django_mongodb.parse_uri(uri, conn_max_age=0, test=None)`
156+
157+
`parse_uri()` provides a few options to customize the resulting `DATABASES`
158+
setting, but for maximum flexibility, construct `DATABASES` manually as
159+
described above.
160+
161+
- Use `conn_max_age` to configure [persistent database connections](
162+
https://docs.djangoproject.com/en/stable/ref/databases/#persistent-database-connections).
163+
- Use `test` to provide a dictionary of [settings for test databases](
164+
https://docs.djangoproject.com/en/stable/ref/settings/#test).
165+
124166
Congratulations, your project is ready to go!
125167

126168
## Notes on Django QuerySets

django_mongodb/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Check Django compatibility before other imports which may fail if the
44
# wrong version of Django is installed.
5-
from .utils import check_django_compatability
5+
from .utils import check_django_compatability, parse_uri
66

77
check_django_compatability()
88

@@ -14,6 +14,8 @@
1414
from .lookups import register_lookups # noqa: E402
1515
from .query import register_nodes # noqa: E402
1616

17+
__all__ = ["parse_uri"]
18+
1719
register_aggregates()
1820
register_expressions()
1921
register_fields()

django_mongodb/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.core.exceptions import ImproperlyConfigured
77
from django.db.backends.utils import logger
88
from django.utils.version import get_version_tuple
9+
from pymongo.uri_parser import parse_uri as pymongo_parse_uri
910

1011

1112
def check_django_compatability():
@@ -25,6 +26,38 @@ def check_django_compatability():
2526
)
2627

2728

29+
def parse_uri(uri, conn_max_age=0, test=None):
30+
"""
31+
Convert the given uri into a dictionary suitable for Django's DATABASES
32+
setting.
33+
"""
34+
uri = pymongo_parse_uri(uri)
35+
host = None
36+
port = None
37+
if uri["fqdn"]:
38+
# This is a SRV URI and the host is the fqdn.
39+
host = f"mongodb+srv://{uri['fqdn']}"
40+
else:
41+
nodelist = uri.get("nodelist")
42+
if len(nodelist) == 1:
43+
host, port = nodelist[0]
44+
elif len(nodelist) > 1:
45+
host = ",".join([f"{host}:{port}" for host, port in nodelist])
46+
settings_dict = {
47+
"ENGINE": "django_mongodb",
48+
"NAME": uri["database"],
49+
"HOST": host,
50+
"PORT": port,
51+
"USER": uri.get("username"),
52+
"PASSWORD": uri.get("password"),
53+
"OPTIONS": uri.get("options"),
54+
"CONN_MAX_AGE": conn_max_age,
55+
}
56+
if test:
57+
settings_dict["TEST"] = test
58+
return settings_dict
59+
60+
2861
def set_wrapped_methods(cls):
2962
"""Initialize the wrapped methods on cls."""
3063
if hasattr(cls, "logging_wrapper"):

tests/backend_/utils/__init__.py

Whitespace-only changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from unittest.mock import patch
2+
3+
import pymongo
4+
from django.test import SimpleTestCase
5+
6+
from django_mongodb import parse_uri
7+
8+
9+
class ParseURITests(SimpleTestCase):
10+
def test_simple_uri(self):
11+
settings_dict = parse_uri("mongodb://cluster0.example.mongodb.net/myDatabase")
12+
self.assertEqual(settings_dict["ENGINE"], "django_mongodb")
13+
self.assertEqual(settings_dict["NAME"], "myDatabase")
14+
self.assertEqual(settings_dict["HOST"], "cluster0.example.mongodb.net")
15+
16+
def test_no_database(self):
17+
settings_dict = parse_uri("mongodb://cluster0.example.mongodb.net")
18+
self.assertIsNone(settings_dict["NAME"])
19+
self.assertEqual(settings_dict["HOST"], "cluster0.example.mongodb.net")
20+
21+
def test_srv_uri_with_options(self):
22+
uri = "mongodb+srv://my_user:[email protected]/my_database?retryWrites=true&w=majority"
23+
# patch() prevents a crash when PyMongo attempts to resolve the
24+
# nonexistent SRV record.
25+
with patch("dns.resolver.resolve"):
26+
settings_dict = parse_uri(uri)
27+
self.assertEqual(settings_dict["NAME"], "my_database")
28+
self.assertEqual(settings_dict["HOST"], "mongodb+srv://cluster0.example.mongodb.net")
29+
self.assertEqual(settings_dict["USER"], "my_user")
30+
self.assertEqual(settings_dict["PASSWORD"], "my_password")
31+
self.assertIsNone(settings_dict["PORT"])
32+
self.assertEqual(
33+
settings_dict["OPTIONS"], {"retryWrites": True, "w": "majority", "tls": True}
34+
)
35+
36+
def test_localhost(self):
37+
settings_dict = parse_uri("mongodb://localhost")
38+
self.assertEqual(settings_dict["HOST"], "localhost")
39+
self.assertEqual(settings_dict["PORT"], 27017)
40+
41+
def test_localhost_with_port(self):
42+
settings_dict = parse_uri("mongodb://localhost:27018")
43+
self.assertEqual(settings_dict["HOST"], "localhost")
44+
self.assertEqual(settings_dict["PORT"], 27018)
45+
46+
def test_hosts_with_ports(self):
47+
settings_dict = parse_uri("mongodb://localhost:27017,localhost:27018")
48+
self.assertEqual(settings_dict["HOST"], "localhost:27017,localhost:27018")
49+
self.assertEqual(settings_dict["PORT"], None)
50+
51+
def test_hosts_without_ports(self):
52+
settings_dict = parse_uri("mongodb://host1.net,host2.net")
53+
self.assertEqual(settings_dict["HOST"], "host1.net:27017,host2.net:27017")
54+
self.assertEqual(settings_dict["PORT"], None)
55+
56+
def test_conn_max_age(self):
57+
settings_dict = parse_uri("mongodb://localhost", conn_max_age=600)
58+
self.assertEqual(settings_dict["CONN_MAX_AGE"], 600)
59+
60+
def test_test_kwarg(self):
61+
settings_dict = parse_uri("mongodb://localhost", test={"NAME": "test_db"})
62+
self.assertEqual(settings_dict["TEST"], {"NAME": "test_db"})
63+
64+
def test_invalid_credentials(self):
65+
msg = "The empty string is not valid username."
66+
with self.assertRaisesMessage(pymongo.errors.InvalidURI, msg):
67+
parse_uri("mongodb://:@localhost")
68+
69+
def test_no_scheme(self):
70+
with self.assertRaisesMessage(pymongo.errors.InvalidURI, "Invalid URI scheme"):
71+
parse_uri("cluster0.example.mongodb.net")

0 commit comments

Comments
 (0)