Repository: pydantic/pydantic

Issue:
AnyUrl percent encoding (#3061) introduced v1.10.0 breaks database connection string
## Checks

- [x] I added a descriptive title to this issue
- [x] I have searched (google, github) for similar issues and couldn't find anything
- [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

## Bug
This bug-fix https://github.com/pydantic/pydantic/issues/3061 introduced percent encoding for the `query` attribute of `AnyUrl`. It lists a set of characters that get percent encoded and refers to the [url-spec](https://www.w3.org/Addressing/URL/url-spec.txt).

According to the url-spec, the `+` character is in the group of safe characters and **should not be encoded** (page 16 in the doc).

As a consequence of the introduced characters encoding, my database connection string to a MS SQL Server DB got unusable:


```
import pydantic


url = pydantic.AnyUrl.build(
    scheme="mssql+pyodbc",
    user="user",
    password="password",
    host="127.0.0.1",
    port="1433",
    path="/database",
    query="driver=ODBC+Driver+17+for+SQL+Server",
)
print(pydantic.VERSION)
print(url)


# 1.9.1
# mssql+pyodbc://user:password@127.0.0.1:1433/database?driver=ODBC+Driver+17+for+SQL+Server

# 1.10.1
# mssql+pyodbc://user:password@127.0.0.1:1433/database?driver=ODBC%2BDriver%2B17%2Bfor%2BSQL%2BServer
```

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:

```
pydantic version: 1.9.1
pydantic compiled: True
install path: C:\Users\xxx\PycharmProjects\xxx\venv\Lib\site-packages\pydantic
python version: 3.8.10 (tags/v3.8.10:3d8993a, May  3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)]
platform: Windows-10-10.0.19044-SP0
optional deps. installed: ['typing-extensions']
```

It would be cool to have a MSSQLServerDsn similar to Postgres, Redis, or MongoDB instead of the general AnyUrl (https://pydantic-docs.helpmanual.io/usage/types/#urls).


Discussion:
maybe related to https://github.com/pydantic/pydantic/issues/4458
duplicate of #4458
This is slightly different but should be easy to fix in #4469.

Resolving patch:
diff --git a/docs/examples/types_url_building.py b/docs/examples/types_url_building.py
deleted file mode 100644
--- a/docs/examples/types_url_building.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from pydantic import AnyUrl, stricturl
-
-url = AnyUrl.build(scheme='https', host='example.com', query='query=my query')
-print(url)
-
-my_url_builder = stricturl(quote_plus=True)
-url = my_url_builder.build(
-    scheme='http', host='example.com', query='query=my query'
-)
-print(url)
diff --git a/pydantic/networks.py b/pydantic/networks.py
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -26,7 +26,6 @@
     cast,
     no_type_check,
 )
-from urllib.parse import quote, quote_plus
 
 from . import errors
 from .utils import Representation, update_not_none
@@ -178,7 +177,6 @@ class AnyUrl(str):
     user_required: bool = False
     host_required: bool = True
     hidden_parts: Set[str] = set()
-    quote_plus: bool = False
 
     __slots__ = ('scheme', 'user', 'password', 'host', 'tld', 'host_type', 'port', 'path', 'query', 'fragment')
 
@@ -241,19 +239,18 @@ def build(
 
         url = scheme + '://'
         if user:
-            url += cls.quote(user)
+            url += user
         if password:
-            url += ':' + cls.quote(password)
+            url += ':' + password
         if user or password:
             url += '@'
         url += host
         if port and ('port' not in cls.hidden_parts or cls.get_default_parts(parts).get('port') != port):
             url += ':' + port
         if path:
-            url += '/'.join(map(cls.quote, path.split('/')))
+            url += path
         if query:
-            queries = query.split('&')
-            url += '?' + '&'.join(map(lambda s: '='.join(map(cls.quote, s.split('='))), queries))
+            url += '?' + query
         if fragment:
             url += '#' + fragment
         return url
@@ -394,10 +391,6 @@ def apply_default_parts(cls, parts: 'Parts') -> 'Parts':
                 parts[key] = value  # type: ignore[literal-required]
         return parts
 
-    @classmethod
-    def quote(cls, string: str, safe: str = '') -> str:
-        return quote_plus(string, safe) if cls.quote_plus else quote(string, safe)
-
     def __repr__(self) -> str:
         extra = ', '.join(f'{n}={getattr(self, n)!r}' for n in self.__slots__ if getattr(self, n) is not None)
         return f'{self.__class__.__name__}({super().__repr__()}, {extra})'
@@ -565,7 +558,6 @@ def stricturl(
     tld_required: bool = True,
     host_required: bool = True,
     allowed_schemes: Optional[Collection[str]] = None,
-    quote_plus: bool = False,
 ) -> Type[AnyUrl]:
     # use kwargs then define conf in a dict to aid with IDE type hinting
     namespace = dict(
@@ -575,7 +567,6 @@ def stricturl(
         tld_required=tld_required,
         host_required=host_required,
         allowed_schemes=allowed_schemes,
-        quote_plus=quote_plus,
     )
     return type('UrlValue', (AnyUrl,), namespace)
 


Test patch:
diff --git a/tests/test_networks.py b/tests/test_networks.py
--- a/tests/test_networks.py
+++ b/tests/test_networks.py
@@ -679,31 +679,12 @@ class Model2(BaseModel):
         (dict(scheme='ws', user='foo', password='x', host='example.net'), 'ws://foo:x@example.net'),
         (dict(scheme='ws', host='example.net', query='a=b', fragment='c=d'), 'ws://example.net?a=b#c=d'),
         (dict(scheme='http', host='example.net', port='1234'), 'http://example.net:1234'),
-        (dict(scheme='http', user='foo@bar', host='example.net'), 'http://foo%40bar@example.net'),
-        (dict(scheme='http', user='foo', password='a b', host='example.net'), 'http://foo:a%20b@example.net'),
-        (dict(scheme='http', host='example.net', query='q=foo bar'), 'http://example.net?q=foo%20bar'),
-        (dict(scheme='http', host='example.net', path="/m&m's"), 'http://example.net/m%26m%27s'),
     ],
 )
 def test_build_url(kwargs, expected):
     assert AnyUrl(None, **kwargs) == expected
 
 
-@pytest.mark.parametrize(
-    'kwargs,expected',
-    [
-        (dict(scheme='https', host='example.com', query='query=my query'), 'https://example.com?query=my+query'),
-        (
-            dict(scheme='https', host='example.com', user='my name', password='a password'),
-            'https://my+name:a+password@example.com',
-        ),
-        (dict(scheme='https', host='example.com', path='/this is a path'), 'https://example.com/this+is+a+path'),
-    ],
-)
-def test_build_url_quote_plus(kwargs, expected):
-    assert stricturl(quote_plus=True).build(**kwargs) == expected
-
-
 @pytest.mark.parametrize(
     'kwargs,expected',
     [