Repository: Azure/msrest-for-python

Issue:
Add support to disable validation on the runtime side
Should be a flag in configuration to disable runtime validation.

This flag should be default to `client-side-validation` value from Autorest:
https://github.com/Azure/autorest/issues/1583#issuecomment-311142690

FYI @matthchr 

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=h1) Report
> Merging [#53](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=desc) into [master](https://codecov.io/gh/Azure/msrest-for-python/commit/bad8585bcbe5f92f3b2c892c8b373ee367dff70f?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/Azure/msrest-for-python/pull/53/graphs/tree.svg?token=Lny6ZO1MUF&src=pr&width=650&height=150)](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master      #53   +/-   ##
=======================================
  Coverage   77.77%   77.77%           
=======================================
  Files         260      260           
  Lines       12696    12696           
=======================================
  Hits         9874     9874           
  Misses       2822     2822
```


| [Impacted Files](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...rmat\_test\_service/operations/queries\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9VcmxNdWx0aUNvbGxlY3Rpb25Gb3JtYXQvYXV0b19yZXN0X3VybF9tdXRsaV9jb2xsZWN0aW9uX2Zvcm1hdF90ZXN0X3NlcnZpY2Uvb3BlcmF0aW9ucy9xdWVyaWVzX29wZXJhdGlvbnMucHk=) | `76.78% <0%> (ø)` | :arrow_up: |
| [...ion\_test\_service/operations/duration\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9Cb2R5RHVyYXRpb24vYXV0b19yZXN0X2R1cmF0aW9uX3Rlc3Rfc2VydmljZS9vcGVyYXRpb25zL2R1cmF0aW9uX29wZXJhdGlvbnMucHk=) | `76.62% <0%> (ø)` | :arrow_up: |
| [...ger\_bat\_byte\_service/operations/byte\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9Cb2R5Qnl0ZS9hdXRvX3Jlc3Rfc3dhZ2dlcl9iYXRfYnl0ZV9zZXJ2aWNlL29wZXJhdGlvbnMvYnl0ZV9vcGVyYXRpb25zLnB5) | `76.84% <0%> (ø)` | :arrow_up: |
| [...rvice/operations/http\_client\_failure\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9IdHRwL2F1dG9fcmVzdF9odHRwX2luZnJhc3RydWN0dXJlX3Rlc3Rfc2VydmljZS9vcGVyYXRpb25zL2h0dHBfY2xpZW50X2ZhaWx1cmVfb3BlcmF0aW9ucy5weQ==) | `71.54% <0%> (ø)` | :arrow_up: |
| [...ed\_host\_test\_client/operations/paths\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9DdXN0b21CYXNlVXJpL2F1dG9fcmVzdF9wYXJhbWV0ZXJpemVkX2hvc3RfdGVzdF9jbGllbnQvb3BlcmF0aW9ucy9wYXRoc19vcGVyYXRpb25zLnB5) | `83.33% <0%> (ø)` | :arrow_up: |
| [...nal\_test\_service/operations/implicit\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9SZXF1aXJlZE9wdGlvbmFsL2F1dG9fcmVzdF9yZXF1aXJlZF9vcHRpb25hbF90ZXN0X3NlcnZpY2Uvb3BlcmF0aW9ucy9pbXBsaWNpdF9vcGVyYXRpb25zLnB5) | `52.5% <0%> (ø)` | :arrow_up: |
| [...ionary\_service/operations/dictionary\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9Cb2R5RGljdGlvbmFyeS9hdXRvX3Jlc3Rfc3dhZ2dlcl9iYV90ZGljdGlvbmFyeV9zZXJ2aWNlL29wZXJhdGlvbnMvZGljdGlvbmFyeV9vcGVyYXRpb25zLnB5) | `74.49% <0%> (ø)` | :arrow_up: |
| [...e\_test\_service/operations/http\_retry\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9IdHRwL2F1dG9fcmVzdF9odHRwX2luZnJhc3RydWN0dXJlX3Rlc3Rfc2VydmljZS9vcGVyYXRpb25zL2h0dHBfcmV0cnlfb3BlcmF0aW9ucy5weQ==) | `72.46% <0%> (ø)` | :arrow_up: |
| [...t\_service/operations/datetimerfc1123\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9Cb2R5RGF0ZVRpbWVSZmMxMTIzL2F1dG9fcmVzdF9yZmMxMTIzX2RhdGVfdGltZV90ZXN0X3NlcnZpY2Uvb3BlcmF0aW9ucy9kYXRldGltZXJmYzExMjNfb3BlcmF0aW9ucy5weQ==) | `74.39% <0%> (ø)` | :arrow_up: |
| [...test\_service/operations/polymorphism\_operations.py](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree#diff-dGVzdC92YW5pbGxhL0V4cGVjdGVkL0FjY2VwdGFuY2VUZXN0cy9Cb2R5Q29tcGxleC9hdXRvX3Jlc3RfY29tcGxleF90ZXN0X3NlcnZpY2Uvb3BlcmF0aW9ucy9wb2x5bW9ycGhpc21fb3BlcmF0aW9ucy5weQ==) | `71.42% <0%> (ø)` | :arrow_up: |
| ... and [45 more](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=footer). Last update [bad8585...42a6a8b](https://codecov.io/gh/Azure/msrest-for-python/pull/53?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/msrest/configuration.py b/msrest/configuration.py
index 7b47b39..4b46b56 100644
--- a/msrest/configuration.py
+++ b/msrest/configuration.py
@@ -44,6 +44,19 @@ from .pipeline import (
 from .version import msrest_version
 
 
+def default_session_configuration_callback(session, global_config, local_config, **kwargs):
+    """Configuration callback if you need to change default session configuration.
+
+    :param requests.Session session: The session.
+    :param Configuration global_config: The global configuration.
+    :param dict local_config: The on-the-fly configuration passed on the call.
+    :param dict kwargs: The current computed values for session.request method.
+    :return: Must return kwargs, to be passed to session.request. If None is return, initial kwargs will be used.
+    :rtype: dict
+    """
+    return kwargs
+
+
 class Configuration(object):
     """Client configuration.
 
@@ -79,6 +92,8 @@ class Configuration(object):
         # - kwargs['msrest']['session'] with the current session
         self.hooks = []
 
+        self.session_configuration_callback = default_session_configuration_callback
+
         self._config = configparser.ConfigParser()
         self._config.optionxform = str
 
diff --git a/msrest/serialization.py b/msrest/serialization.py
index 1902ca2..cc03063 100644
--- a/msrest/serialization.py
+++ b/msrest/serialization.py
@@ -350,6 +350,7 @@ class Serializer(object):
             }
         self.dependencies = dict(classes) if classes else {}
         self.key_transformer = full_restapi_key_transformer
+        self.client_side_validation = True
 
     def _serialize(self, target_obj, data_type=None, **kwargs):
         """Serialize data into a string according to type.
@@ -440,9 +441,10 @@ class Serializer(object):
                 raise_with_traceback(
                     SerializationError, "Unable to build a model: "+str(err), err)
 
-        errors = _recursive_validate(data_type, data)
-        if errors:
-            raise errors[0]
+        if self.client_side_validation:
+            errors = _recursive_validate(data_type, data)
+            if errors:
+                raise errors[0]
         return self._serialize(data, data_type, **kwargs)
 
     def url(self, name, data, data_type, **kwargs):
@@ -454,7 +456,8 @@ class Serializer(object):
         :raises: TypeError if serialization fails.
         :raises: ValueError if data is None
         """
-        data = self.validate(data, name, required=True, **kwargs)
+        if self.client_side_validation:
+            data = self.validate(data, name, required=True, **kwargs)
         try:
             output = self.serialize_data(data, data_type, **kwargs)
             if data_type == 'bool':
@@ -478,7 +481,8 @@ class Serializer(object):
         :raises: TypeError if serialization fails.
         :raises: ValueError if data is None
         """
-        data = self.validate(data, name, required=True, **kwargs)
+        if self.client_side_validation:
+            data = self.validate(data, name, required=True, **kwargs)
         try:
             if data_type in ['[str]']:
                 data = ["" if d is None else d for d in data]
@@ -504,7 +508,8 @@ class Serializer(object):
         :raises: TypeError if serialization fails.
         :raises: ValueError if data is None
         """
-        data = self.validate(data, name, required=True, **kwargs)
+        if self.client_side_validation:
+            data = self.validate(data, name, required=True, **kwargs)
         try:
             if data_type in ['[str]']:
                 data = ["" if d is None else d for d in data]
diff --git a/msrest/service_client.py b/msrest/service_client.py
index d0e6fdb..eed50c5 100644
--- a/msrest/service_client.py
+++ b/msrest/service_client.py
@@ -145,6 +145,11 @@ class ServiceClient(object):
         for protocol in self._protocols:
             session.mount(protocol,
                           requests.adapters.HTTPAdapter(max_retries=max_retries))
+
+        output_kwargs = self.config.session_configuration_callback(session, self.config, config, **kwargs)
+        if output_kwargs is not None:
+            kwargs = output_kwargs
+
         return kwargs
 
     def send_formdata(self, request, headers=None, content=None, **config):


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 8e73444..ee10d48 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -50,6 +50,22 @@ class TestServiceClient(unittest.TestCase):
         self.creds = mock.create_autospec(OAuthTokenAuthentication)
         return super(TestServiceClient, self).setUp()
 
+    def test_session_callback(self):
+
+        client = ServiceClient(self.creds, self.cfg)
+        local_session = requests.Session()
+
+        def callback(session, global_config, local_config, **kwargs):
+            self.assertIs(session, local_session)
+            self.assertIs(global_config, self.cfg)
+            self.assertTrue(local_config["test"])
+            return {'used_callback': True}
+
+        self.cfg.session_configuration_callback = callback
+
+        output_kwargs = client._configure_session(local_session, **{"test": True})
+        self.assertTrue(output_kwargs['used_callback'])
+
     def test_client_request(self):
 
         client = ServiceClient(self.creds, self.cfg)
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index c6cf07d..4d6a80a 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -172,6 +172,32 @@ class TestRuntimeSerialized(unittest.TestCase):
         self.s = Serializer({'TestObj': self.TestObj})
         return super(TestRuntimeSerialized, self).setUp()
 
+    def test_validation_flag(self):
+        s = Serializer()
+        s.client_side_validation = True
+
+        with self.assertRaises(ValidationError):
+            s.query("filter", "", "str", min_length=666)
+        with self.assertRaises(ValidationError):
+            s.url("filter", "", "str", min_length=666)
+        with self.assertRaises(ValidationError):
+            s.header("filter", "", "str", min_length=666)
+
+        test_obj = self.TestObj()
+        self.TestObj._validation = {
+            'attr_b': {'required': True},
+        }
+        test_obj.attr_b = None
+
+        with self.assertRaises(ValidationError):
+            self.s.body(test_obj, 'TestObj')
+
+        s.client_side_validation = False
+        s.query("filter", "", "str", min_length=666)
+        s.url("filter", "", "str", min_length=666)
+        s.header("filter", "", "str", min_length=666)
+        s.body(test_obj, 'TestObj')
+
     def test_serialize_direct_model(self):
         testobj = self.TestObj()
         testobj.attr_a = "myid"