Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

INTPYTHON-424 add django_mongodb.parse_uri() to configure DATABASES #195

Merged
merged 1 commit into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,59 @@ to this:
DATABASES = {
"default": {
"ENGINE": "django_mongodb",
"HOST": "mongodb+srv://cluster0.example.mongodb.net",
timgraham marked this conversation as resolved.
Show resolved Hide resolved
"NAME": "my_database",
"USER": "my_user",
"PASSWORD": "my_password",
"OPTIONS": {...},
"PORT": 27017,
"OPTIONS": {
# Example:
"retryWrites": "true",
"w": "majority",
"tls": "false",
},
},
}
```

For a localhost configuration, you can omit `HOST` or specify
`"HOST": "localhost"`.

`HOST` only needs a scheme prefix for SRV connections (`mongodb+srv://`). A
`mongodb://` prefix is never required.

`OPTIONS` is an optional dictionary of parameters that will be passed to
[`MongoClient`](https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html).

`USER`, `PASSWORD`, and `PORT` (if 27017) may also be optional.

For a replica set or sharded cluster where you have multiple hosts, include
all of them in `HOST`, e.g.
`"mongodb://mongos0.example.com:27017,mongos1.example.com:27017"`.

Alternatively, if you prefer to simply paste in a MongoDB URI rather than parse
it into the format above, you can use:

```python
import django_mongodb

MONGODB_URI = "mongodb+srv://my_user:[email protected]/myDatabase?retryWrites=true&w=majority&tls=false"
DATABASES["default"] = django_mongodb.parse_uri(MONGODB_URI)
```

This constructs a `DATABASES` setting equivalent to the first example.

#### `django_mongodb.parse_uri(uri, conn_max_age=0, test=None)`

`parse_uri()` provides a few options to customize the resulting `DATABASES`
setting, but for maximum flexibility, construct `DATABASES` manually as
described above.

- Use `conn_max_age` to configure [persistent database connections](
https://docs.djangoproject.com/en/stable/ref/databases/#persistent-database-connections).
- Use `test` to provide a dictionary of [settings for test databases](
https://docs.djangoproject.com/en/stable/ref/settings/#test).

Congratulations, your project is ready to go!

## Notes on Django QuerySets
Expand Down
4 changes: 3 additions & 1 deletion django_mongodb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

check_django_compatability()

Expand All @@ -14,6 +14,8 @@
from .lookups import register_lookups # noqa: E402
from .query import register_nodes # noqa: E402

__all__ = ["parse_uri"]

register_aggregates()
register_expressions()
register_fields()
Expand Down
33 changes: 33 additions & 0 deletions django_mongodb/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.utils import logger
from django.utils.version import get_version_tuple
from pymongo.uri_parser import parse_uri as pymongo_parse_uri


def check_django_compatability():
Expand All @@ -25,6 +26,38 @@ def check_django_compatability():
)


def parse_uri(uri, conn_max_age=0, test=None):
"""
Convert the given uri into a dictionary suitable for Django's DATABASES
setting.
"""
uri = pymongo_parse_uri(uri)
host = None
port = None
if uri["fqdn"]:
# This is a SRV URI and the host is the fqdn.
host = f"mongodb+srv://{uri['fqdn']}"
else:
nodelist = uri.get("nodelist")
if len(nodelist) == 1:
host, port = nodelist[0]
elif len(nodelist) > 1:
host = ",".join([f"{host}:{port}" for host, port in nodelist])
aclark4life marked this conversation as resolved.
Show resolved Hide resolved
aclark4life marked this conversation as resolved.
Show resolved Hide resolved
settings_dict = {
"ENGINE": "django_mongodb",
"NAME": uri["database"],
aclark4life marked this conversation as resolved.
Show resolved Hide resolved
"HOST": host,
aclark4life marked this conversation as resolved.
Show resolved Hide resolved
"PORT": port,
"USER": uri.get("username"),
"PASSWORD": uri.get("password"),
"OPTIONS": uri.get("options"),
"CONN_MAX_AGE": conn_max_age,
}
if test:
settings_dict["TEST"] = test
return settings_dict


def set_wrapped_methods(cls):
"""Initialize the wrapped methods on cls."""
if hasattr(cls, "logging_wrapper"):
Expand Down
Empty file.
71 changes: 71 additions & 0 deletions tests/backend_/utils/test_parse_uri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from unittest.mock import patch

import pymongo
from django.test import SimpleTestCase

from django_mongodb import parse_uri


class ParseURITests(SimpleTestCase):
def test_simple_uri(self):
settings_dict = parse_uri("mongodb://cluster0.example.mongodb.net/myDatabase")
self.assertEqual(settings_dict["ENGINE"], "django_mongodb")
self.assertEqual(settings_dict["NAME"], "myDatabase")
self.assertEqual(settings_dict["HOST"], "cluster0.example.mongodb.net")

def test_no_database(self):
settings_dict = parse_uri("mongodb://cluster0.example.mongodb.net")
self.assertIsNone(settings_dict["NAME"])
self.assertEqual(settings_dict["HOST"], "cluster0.example.mongodb.net")

def test_srv_uri_with_options(self):
uri = "mongodb+srv://my_user:[email protected]/my_database?retryWrites=true&w=majority"
# patch() prevents a crash when PyMongo attempts to resolve the
# nonexistent SRV record.
with patch("dns.resolver.resolve"):
settings_dict = parse_uri(uri)
self.assertEqual(settings_dict["NAME"], "my_database")
self.assertEqual(settings_dict["HOST"], "mongodb+srv://cluster0.example.mongodb.net")
self.assertEqual(settings_dict["USER"], "my_user")
self.assertEqual(settings_dict["PASSWORD"], "my_password")
self.assertIsNone(settings_dict["PORT"])
self.assertEqual(
settings_dict["OPTIONS"], {"retryWrites": True, "w": "majority", "tls": True}
)

def test_localhost(self):
settings_dict = parse_uri("mongodb://localhost")
self.assertEqual(settings_dict["HOST"], "localhost")
self.assertEqual(settings_dict["PORT"], 27017)

def test_localhost_with_port(self):
settings_dict = parse_uri("mongodb://localhost:27018")
self.assertEqual(settings_dict["HOST"], "localhost")
self.assertEqual(settings_dict["PORT"], 27018)

def test_hosts_with_ports(self):
settings_dict = parse_uri("mongodb://localhost:27017,localhost:27018")
self.assertEqual(settings_dict["HOST"], "localhost:27017,localhost:27018")
self.assertEqual(settings_dict["PORT"], None)

def test_hosts_without_ports(self):
settings_dict = parse_uri("mongodb://host1.net,host2.net")
self.assertEqual(settings_dict["HOST"], "host1.net:27017,host2.net:27017")
self.assertEqual(settings_dict["PORT"], None)

def test_conn_max_age(self):
settings_dict = parse_uri("mongodb://localhost", conn_max_age=600)
self.assertEqual(settings_dict["CONN_MAX_AGE"], 600)

def test_test_kwarg(self):
settings_dict = parse_uri("mongodb://localhost", test={"NAME": "test_db"})
self.assertEqual(settings_dict["TEST"], {"NAME": "test_db"})

def test_invalid_credentials(self):
msg = "The empty string is not valid username."
with self.assertRaisesMessage(pymongo.errors.InvalidURI, msg):
parse_uri("mongodb://:@localhost")

def test_no_scheme(self):
with self.assertRaisesMessage(pymongo.errors.InvalidURI, "Invalid URI scheme"):
parse_uri("cluster0.example.mongodb.net")
Loading