Repository: dbcli/mssql-cli

Issue:
Refactor JSON RPC Request class
There's redundant code littered through JSON RPC.

Also need to fix PEP 8 issues (#351).

Discussion:

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index ff5174d..110c2ff 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 0.17.2
+current_version = 0.17.3
 parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
 serialize = 
 	{major}.{minor}.{patch}
diff --git a/MANIFEST.in b/MANIFEST.in
index 03a9024..ff36ec7 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,4 @@
 include LICENSE.txt
 include mssqlcli/mssqltoolsservice/bin/*
 include mssqlcli/mssqltoolsservice/bin/*/*
+include mssqlcli/packages/mssqlliterals/sqlliterals.json
diff --git a/build.py b/build.py
index 214b9d0..7313396 100644
--- a/build.py
+++ b/build.py
@@ -80,6 +80,12 @@ def build():
                              utility.ROOT_DIR,
                              continue_on_error=False)
 
+        # generate sdist--only run on macOS to eliminate redundant copies when published to Azure
+        # NOTE: macOS was arbitrarily chosen as the single OS
+        if sys.platform == 'darwin':
+            utility.exec_command('%s setup.py check -r -s sdist --formats=gztar' % PYTHON,
+                                 utility.ROOT_DIR, continue_on_error=False)
+
     # Copy back the SqlToolsService binaries for this platform.
     clean_and_copy_sqltoolsservice(utility.get_current_platform())
     copy_and_rename_wheels()
@@ -89,12 +95,29 @@ def copy_and_rename_wheels():
     # Create a additional copy of each build artifact with a ever green name with 'dev-latest' as
     # it's version.
     for pkg_name in os.listdir(utility.MSSQLCLI_DIST_DIRECTORY):
-        first_dash = pkg_name.find('-')
-        second_dash = pkg_name.find('-', first_dash + 1, len(pkg_name))
-        pkg_daily_name = pkg_name.replace(pkg_name[first_dash + 1:second_dash], 'dev-latest')
+        file_name, file_extension = os.path.splitext(pkg_name)
+
+        if file_extension == '.gz':
+            file_extension = '.tar.gz'
+            # rename sdist to make underscore consistent with wheel
+            file_name = file_name.replace('mssql-cli', 'mssql_cli').replace('.tar', '')
+            os.rename(os.path.join(utility.MSSQLCLI_DIST_DIRECTORY, pkg_name),
+                      os.path.join(utility.MSSQLCLI_DIST_DIRECTORY, file_name + file_extension))
+            pkg_name = file_name + file_extension
+
+            end_index_to_replace = len(file_name)
+
+        first_dash = file_name.find('-')
+
+        if file_extension == '.whl':
+            end_index_to_replace = file_name.find('-', first_dash + 1, len(file_name))
+
+        pkg_daily_name = file_name.replace(file_name[first_dash + 1:end_index_to_replace],
+                                           'dev-latest')
 
         original_path = os.path.join(utility.MSSQLCLI_DIST_DIRECTORY, pkg_name)
-        updated_path = os.path.join(utility.MSSQLCLI_DIST_DIRECTORY, pkg_daily_name)
+        updated_path = os.path.join(utility.MSSQLCLI_DIST_DIRECTORY,
+                                    pkg_daily_name + file_extension)
         copyfile(original_path, updated_path)
 
 
diff --git a/build_scripts/debian/build.sh b/build_scripts/debian/build.sh
index 0dc015c..d95d2be 100755
--- a/build_scripts/debian/build.sh
+++ b/build_scripts/debian/build.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-CLI_VERSION=0.17.2
+CLI_VERSION=0.17.3
 
 if [ -z "$1" ]
   then
diff --git a/build_scripts/rpm/mssql-cli.spec b/build_scripts/rpm/mssql-cli.spec
index a959e92..d8005ae 100644
--- a/build_scripts/rpm/mssql-cli.spec
+++ b/build_scripts/rpm/mssql-cli.spec
@@ -11,7 +11,7 @@
 %define name           mssql-cli
 %define release        1%{?dist}
 %define time_stamp     %(date +%y%m%d%H%M)
-%define base_version   0.17.2
+%define base_version   0.17.3
 %define python_dir     %{_builddir}/python_env
 %define python_url     https://www.python.org/ftp/python/3.6.1/Python-3.6.1.tgz
 %define cli_lib_dir    %{_libdir}/mssql-cli
diff --git a/doc/release_guide.md b/doc/release_guide.md
index b0504d6..5db28a8 100644
--- a/doc/release_guide.md
+++ b/doc/release_guide.md
@@ -1,13 +1,15 @@
-mssql-cli release guide
+mssql-cli Release Guide
 ========================================
 # Table of Contents
-1. [Requirements](#Requirements)
-2. [Local builds](#Local)
-3. [Daily builds](#Daily)
-4. [Official builds](#Official)
- 
-# <a name="Requirements"></a> Requirements
-### Install Dependencies
+1. [Requirements](#requirements)
+2. [Creating a New Version](#creating-a-new-version)
+3. [Generating Release Files](#generating-release-files)
+4. [Publishing Release Files](#publishing-release-files)
+5. [Installing Specific Release Files from Azure Storage](#installing-specific-release-files-from-azure-storage)
+
+
+# Requirements
+## Installing Dependencies
 1.  Add `<clone_root>` to your PYTHONPATH environment variable:
     #### Windows
     ```
@@ -21,59 +23,24 @@ mssql-cli release guide
     ```
     python <clone_root>/dev_setup.py
     ```
-### Configure Azure Storage Account
+## Azure Storage Account Configuration
 1. The Azure Storage account needs a container named **daily**.
 
 2. A sub folder named **mssql-cli** under the previous container.
 
 3. Set the Azure Storage account connection string via environment variable. This will be the storage account where builds get published to.
-    #### Windows
+
+    **Windows:**
     ```
     set AZURE_STORAGE_CONNECTION_STRING=<connection string>
     ```
 
-    #### MacOS/Linux
+    **MacOS/Linux:**
     ```
     export AZURE_STORAGE_CONNECTION_STRING='<connection_string>'
     ```
 
-# Create Builds
-## <a name="Local"></a>Local Builds
-The steps below outline how to build mssql-cli locally on your dev environment.
-### Build Instructions
-1. To build a wheel package for the current platform, run:
-    ```
-    python build.py build
-    ```
-
-### Installation Instructions
-2. The prior build step created a wheel package, located in the `./dist/` folder. To test the installation of this wheel package, execute the following from `<clone_root>`, replacing values for `<version>` and `<timestamp>`:
-    ```
-    sudo pip install --no-index -i ./dist/mssql_cli-<version>.dev<timestamp>-py2.py3-none-win_amd64.whl
-    ```
-    
-## <a name="Daily"></a>Daily Builds
-The steps below outline how daily builds of mssql-cli are generated. These steps run on each supported platform via Visual Studio Team Services. 
-### Build Instructions
-1. Build mssql-cli:
-    ```
-    python build.py build
-    ```
-
-2. Publish build to daily storage account:
-    ```
-    python release.py publish_daily
-    ```
-### Installation Instructions
-3. Test install from daily storage account:
-    ```
-    pip install --pre --no-cache --extra-index-url https://mssqlcli.blob.core.windows.net/daily/whl mssql-cli
-    ```
-    
-## <a name="Official"></a>Official Builds
-The steps below outline how to build official builds and publish to PYPI.
-
-### <a name="BumpVersion"></a>Bump Version
+# Creating a New Version
 The versioning format for `mssql-cli` uses the following naming scheme:
 
 	Versioning schema: {major}.{minor}.{patch}
@@ -83,60 +50,103 @@ To bump a particular segment of the version, from `<clone_root>` execute:
 > bumpversion major          ->  <b>2</b>.0
 > bumpversion minor          ->  1.<b>1</b>
 > bumpversion patch          ->  1.0.<b>1</b>
-
 </pre>
 Check-in changes after running `bumpversion` and **validating the build output following a version bump**.
 
 **Note**: bumpversion does not allow version bumping if your workspace has pending changes. This is to protect against any manual updates that may have been made which can lead to inconsistent versions across files. If you know what you are doing you can override this by appending `--allow-dirty` to the bumpversion command.
 
-### Publish Build
-**Important: ensure that the build is uploaded from macOS.**
 
-1. Add a .pypirc configuration file:
+# Generating Release Files
+The steps below outline how to create wheel and source distribution files to publish to Azure Storage, and eventually PyPI for official release.
 
-    - Create a .pypirc file in your user directory:
-        #### Windows: 
-            Example: C:\Users\bob\.pypirc
-		#### MacOS/Linux: 
-            Example: /Users/bob/.pypirc
-    
-    - Add the following content to the .pypirc file, replace `your_username` and `your_passsword` with your account information created from step 1:
-        ```
-        [distutils]
-        index-servers=
-            pypi
-        
-        [pypi]
-        username = sqlcli
-        password = <Get Password from Azure Key Vault> (https://sqltoolssecretstore.vault.azure.net/secrets/PYPI-AccountName-sqlcli/)
-
-        ```
-2. Set env var to indicate a official build:
-    #### Windows
-    ```
-    set MSSQL_CLI_OFFICIAL_BUILD=True
-    ```
+## Daily Release Configuration
+Release files are generated for daily release by default, as long as the `MSSQL_CLI_OFFICIAL_BUILD` environment variable is **not** set to `True`.
+
+## Official Release Configuration
+The `MSSQL_CLI_OFFICIAL_BUILD` enviornment variable must be set to `True` before build files are created. Although this step can be completed locally, we recommend running production pipelines in Azure DevOps with the enviornment variable set for each run.
+
+If configured locally, instructions per OS are as follows:
+- **Windows**: `set MSSQL_CLI_OFFICIAL_BUILD=True`
+- **macOS/Linux**: `export MSSQL_CLI_OFFICIAL_BUILD=True`
+
+## Building Release Files
+To build a release package with wheel and source distribution files for the current platform, run:
+```
+python build.py build
+```
+Distribution files will be generated in `./dist/`. These files are eventually published to Azure Storage and PyPI for distribution.
+
+**Note:** source distribution files will only get created on macOS. This platform was arbitrarily chosen to prevent redundant copies of source distributions when `build` is run on each platform in Azure DevOps.
+
+# Publishing Release Files
+The following instructions outline how to publish release files once generated n the `./dist/` folder.
+
+## Publishing Daily Builds to Azure Storage
+Publish build to daily storage account by calling:
+```
+python release.py publish_daily
+```
     
-    #### MacOS/Linux
-    ```
-    export MSSQL_CLI_OFFICIAL_BUILD=True
-    ```
-3. Build for the current platform:
-    ```
-    python build.py build
-    ```
+## Publishing Official Builds
+The steps below outline how to build official builds and publish to PyPI.
 
-4. Publish to daily storage account:
-    ```
-    python release.py publish_daily
-    ```
+### Configuration with PyPI
+A `.pypirc` configuration file must be created in order to publish to PyPI. Place in the following user directories.
+
+Examples for each OS:
+* Windows: `C:\Users\bob\.pypirc`
+* MacOS/Linux: `/Users/bob/.pypirc`
+
+Add the below contents to the `.pypirc` file. **Note**: consult the mssql-cli OneNote for obtaining access to Azure Key Vault credentials, needed below.
+
+```
+[distutils]
+index-servers=
+    pypi
+    testpypi
+
+[pypi]
+username: sqlcli
+password: <Get Password from Azure Key Vault>
+
+[testpypi]
+repository: https://test.pypi.org/legacy/
+username: sqlcli
+password: <Get Password from Azure Key Vault>
+```
+
+### Test Publishing with TestPyPI
+
+[TestPyPi](test.pypi.org) can be used to test distribution before going to production. To test publishing to TestPyPI, use the above `.pypirc` file and call:
+```
+twine upload --repository testpypi dist/*
+```
+
+[Click here](https://packaging.python.org/guides/using-testpypi/) to view TestPyPI docs.
+
+### Publishing Official Release Files to PyPI
+**Important: ensure that the build is uploaded from macOS.**
+
+Follow the instructions below to publish a new release to PyPI after official release build files have been published to Azure Storage:
     
-5. Download official wheels from storage account:
+1. Download official wheels from storage account:
     ```
     python release.py download_official_wheels
     ```
     
-6. Publish downloaded wheels to PYPI:
+2. Publish downloaded wheels to PyPI:
     ```
     python release.py publish_official
     ```
+
+# Installing Specific Release Files from Azure Storage
+To test the installation a wheel or source distribution, execute the following from `<clone_root>`, replacing values for `<version>` and `<timestamp>`:
+```
+pip install --no-index -i ./dist/mssql_cli-<version>.dev<timestamp>-py2.py3-none-win_amd64.whl
+```
+
+
+To install the latest release file from the daily storage account, call:
+```
+pip install --pre --no-cache --extra-index-url https://mssqlcli.blob.core.windows.net/daily/whl mssql-cli
+```
\ No newline at end of file
diff --git a/mssqlcli/__init__.py b/mssqlcli/__init__.py
index 84d3488..e0cd4e2 100644
--- a/mssqlcli/__init__.py
+++ b/mssqlcli/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '0.17.2'
+__version__ = '0.17.3'
diff --git a/mssqlcli/jsonrpc/contracts/__init__.py b/mssqlcli/jsonrpc/contracts/__init__.py
index c2dc27e..4673757 100644
--- a/mssqlcli/jsonrpc/contracts/__init__.py
+++ b/mssqlcli/jsonrpc/contracts/__init__.py
@@ -2,32 +2,5 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for license information.
 # --------------------------------------------------------------------------------------------
-import abc
 
-ABC = abc.ABCMeta('ABC', (object,), {})  # compatibile with Python 2 *and* 3.
-
-
-class Request(ABC):
-    """
-        Abstract request class.
-    """
-    @abc.abstractmethod
-    def execute(self):
-        """
-            Executes the request.
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_response(self):
-        """
-            Retrieves expected response.
-        """
-        pass
-
-    @abc.abstractmethod
-    def completed(self):
-        """
-            Return state of request.
-        """
-        pass
+from mssqlcli.jsonrpc.contracts.request import Request
diff --git a/mssqlcli/jsonrpc/contracts/connectionservice.py b/mssqlcli/jsonrpc/contracts/connectionservice.py
index ca87c59..2b2d02f 100644
--- a/mssqlcli/jsonrpc/contracts/connectionservice.py
+++ b/mssqlcli/jsonrpc/contracts/connectionservice.py
@@ -1,83 +1,46 @@
 # pylint: disable=too-many-instance-attributes
 # pylint: disable=too-few-public-methods
 
-import logging
 from mssqlcli.jsonrpc.contracts import Request
 
 
-logger = logging.getLogger(u'mssqlcli.connectionservice')
-
-
 class ConnectionRequest(Request):
     """
         SqlToolsService Connection request.
     """
-    METHOD_NAME = u'connection/connect'
 
     def __init__(self, request_id, owner_uri, json_rpc_client, parameters):
-        self.request_id = request_id
-        self.owner_uri = owner_uri
-        self.finished = False
-        self.json_rpc_client = json_rpc_client
-        self.params = ConnectionParams(parameters)
-
-    def execute(self):
-        self.json_rpc_client.submit_request(
-            self.METHOD_NAME, self.params.format(), self.request_id)
-
-    def get_response(self):
-        """
-            Get latest response, event or exception if it occured.
-        """
-        try:
-            response = self.json_rpc_client.get_response(self.request_id, self.owner_uri)
-            decoded_response = None
-            if response:
-                logger.debug(response)
-                decoded_response = self.decode_response(response)
-
-            if isinstance(decoded_response, ConnectionCompleteEvent):
-                self.finished = True
-                self.json_rpc_client.request_finished(self.request_id)
-                self.json_rpc_client.request_finished(self.owner_uri)
-
-            return decoded_response
-
-        except Exception as error:  # pylint: disable=broad-except
-            logger.info(str(error))
-            self.finished = True
-            self.json_rpc_client.request_finished(self.request_id)
-            self.json_rpc_client.request_finished(self.owner_uri)
-            return ConnectionCompleteEvent({
-                u'params': {
-                    u'ownerUri': self.owner_uri,
-                    u'connectionId': None,
-                    u'messages': str(error),
-                    u'errorMessage': u'Connection request encountered an exception',
-                    u'errorNumber': None
-                }
-            })
-
-    def completed(self):
-        """
-            Get current request state.
-        """
-        return self.finished
+        super(ConnectionRequest, self).__init__(request_id, owner_uri, json_rpc_client,
+                                                ConnectionParams(parameters),
+                                                u'connection/connect',
+                                                ConnectionCompleteEvent)
+
+    @classmethod
+    def response_error(cls, error):
+        return ConnectionCompleteEvent({
+            u'params': {
+                u'ownerUri': cls.owner_uri,
+                u'connectionId': None,
+                u'messages': str(error),
+                u'errorMessage': u'Connection request encountered an exception',
+                u'errorNumber': None
+            }
+        })
 
     @staticmethod
-    def decode_response(obj):
+    def decode_response(response):
         """
             Decode response dictionary into a Connection parameter type.
         """
 
-        if u'result' in obj:
-            return ConnectionResponse(obj)
+        if u'result' in response:
+            return ConnectionResponse(response)
 
-        if 'method' in obj and obj['method'] == 'connection/complete':
-            return ConnectionCompleteEvent(obj)
+        if 'method' in response and response['method'] == 'connection/complete':
+            return ConnectionCompleteEvent(response)
 
         # Could not decode return st
-        return obj
+        return response
 
 
 class ConnectionDetails:
diff --git a/mssqlcli/jsonrpc/contracts/request.py b/mssqlcli/jsonrpc/contracts/request.py
new file mode 100644
index 0000000..e979cfa
--- /dev/null
+++ b/mssqlcli/jsonrpc/contracts/request.py
@@ -0,0 +1,73 @@
+# pylint: disable=too-many-arguments
+import logging
+import abc
+
+ABC = abc.ABCMeta('ABC', (object,), {})  # compatibile with Python 2 *and* 3.
+logger = logging.getLogger(u'mssqlcli.connectionservice')
+
+class Request(ABC):
+    """
+        Abstract request class.
+    """
+
+    def __init__(self, request_id, owner_uri, json_rpc_client, parameters,
+                 method_name, connectionCompleteEvent):
+        self.request_id = request_id
+        self.owner_uri = owner_uri
+        self.json_rpc_client = json_rpc_client
+        self.params = parameters
+        self.method_name = method_name
+        self.connectionCompleteEvent = connectionCompleteEvent
+        self.finished = False
+
+    @staticmethod
+    @abc.abstractmethod
+    def decode_response(response):
+        """
+            Returns decoded response.
+        """
+        pass
+
+    @classmethod
+    @abc.abstractmethod
+    def response_error(cls, error):
+        """
+            Returns object when response fails.
+        """
+        pass
+
+    def execute(self):
+        """
+            Executes the request.
+        """
+        self.json_rpc_client.submit_request(
+            self.method_name, self.params.format(), self.request_id)
+
+    def get_response(self):
+        """
+            Get latest response, event or exception if it occured.
+        """
+        try:
+            response = self.json_rpc_client.get_response(self.request_id, self.owner_uri)
+            decoded_response = None
+            if response:
+                logger.debug(response)
+                decoded_response = self.decode_response(response)
+
+            if isinstance(decoded_response, self.connectionCompleteEvent):
+                self.finished = True
+                self.json_rpc_client.request_finished(self.request_id)
+                self.json_rpc_client.request_finished(self.owner_uri)
+            return decoded_response
+        except Exception as error:  # pylint: disable=broad-except
+            logger.info(str(error))
+            self.finished = True
+            self.json_rpc_client.request_finished(self.request_id)
+            self.json_rpc_client.request_finished(self.owner_uri)
+            return self.response_error(error)
+
+    def completed(self):
+        """
+            Return state of request.
+        """
+        return self.finished
diff --git a/release.py b/release.py
index 603f1e4..7bf484b 100644
--- a/release.py
+++ b/release.py
@@ -100,13 +100,13 @@ def publish_official():
     """
     Publish mssql-cli package to PyPi.
     """
-    mssqlcli_wheel_dir = os.listdir(utility.MSSQLCLI_DIST_DIRECTORY)
+    mssqlcli_dist_dir = os.listdir(utility.MSSQLCLI_DIST_DIRECTORY)
     # Run twine action for mssql-cli.
     # Only authorized users with credentials will be able to upload this package.
     # Credentials will be stored in a .pypirc file.
-    for wheel in mssqlcli_wheel_dir:
+    for file_dist in mssqlcli_dist_dir:
         utility.exec_command(
-            'twine upload {}'.format(wheel),
+            'twine upload {}'.format(file_dist),
             utility.MSSQLCLI_DIST_DIRECTORY)
 
 
diff --git a/setup.py b/setup.py
index f267789..fe354f2 100644
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,7 @@ setup(
     name='mssql-cli',
     author='Microsoft Corporation',
     author_email='sqlcli@microsoft.com',
-    version=version if os.environ.get('MSSQL_CLI_OFFICIAL_BUILD', '') \
+    version=version if os.environ.get('MSSQL_CLI_OFFICIAL_BUILD', '').lower() == 'true' \
                     else get_timestamped_version(version),
     license='BSD-3',
     url='https://github.com/dbcli/mssql-cli',


Test patch:
diff --git a/mssqlcli/jsonrpc/contracts/queryexecutestringservice.py b/mssqlcli/jsonrpc/contracts/queryexecutestringservice.py
index c8bc291..9cbc1e3 100644
--- a/mssqlcli/jsonrpc/contracts/queryexecutestringservice.py
+++ b/mssqlcli/jsonrpc/contracts/queryexecutestringservice.py
@@ -12,51 +12,18 @@ class QueryExecuteStringRequest(Request):
         Uses SQL Tools Service query/executeString method.
     """
 
-    METHOD_NAME = u'query/executeString'
-
     def __init__(self, request_id, owner_uri, json_rpc_client, parameters):
-        self.request_id = request_id
-        self.owner_uri = owner_uri
-        self.finished = False
-        self.json_rpc_client = json_rpc_client
-        self.params = QueryExecuteStringParams(parameters)
-
-    def get_response(self):
-        """
-            Get latest response, event or exception if occured.
-        """
-        try:
-            response = self.json_rpc_client.get_response(self.request_id, self.owner_uri)
-
-            decoded_response = None
-            if response:
-                decoded_response = QueryExecuteStringRequest.decode_response(response)
-
-            if isinstance(decoded_response,
-                          (QueryCompleteEvent, QueryExecuteErrorResponseEvent)):
-                self.finished = True
-                self.json_rpc_client.request_finished(self.request_id)
-                self.json_rpc_client.request_finished(self.owner_uri)
-
-            return decoded_response
-
-        except Exception as error:  # pylint: disable=broad-except
-            self.finished = True
-            self.json_rpc_client.request_finished(self.request_id)
-            self.json_rpc_client.request_finished(self.owner_uri)
-            return QueryCompleteEvent(
-                {u'params': None}, exception_message=str(error)
-            )
-
-    def execute(self):
-        self.json_rpc_client.submit_request(
-            self.METHOD_NAME, self.params.format(), self.request_id)
-
-    def completed(self):
-        """
-            Get current request state.
-        """
-        return self.finished
+        super(QueryExecuteStringRequest, self).__init__(request_id, owner_uri, json_rpc_client,
+                                                        QueryExecuteStringParams(parameters),
+                                                        u'query/executeString',
+                                                        (QueryCompleteEvent,
+                                                         QueryExecuteErrorResponseEvent))
+
+    @classmethod
+    def response_error(cls, error):
+        return QueryCompleteEvent(
+            {u'params': None}, exception_message=str(error)
+        )
 
     @staticmethod
     def decode_response(response):
@@ -140,44 +107,14 @@ class QuerySubsetRequest(Request):
         SqlToolsService QuerySubset Request.
     """
 
-    METHOD_NAME = u'query/subset'
-
     def __init__(self, request_id, owner_uri, json_rpc_client, parameters):
-        self.request_id = request_id
-        self.owner_uri = owner_uri
-        self.finished = False
-        self.json_rpc_client = json_rpc_client
-        self.params = QuerySubsetParams(parameters)
-
-    def completed(self):
-        """
-            Get current request state.
-        """
-        return self.finished
-
-    def get_response(self):
-        try:
-            response = self.json_rpc_client.get_response(self.request_id, self.owner_uri)
-            decoded_response = None
-            if response:
-                decoded_response = QuerySubsetRequest.decode_response(response)
-
-            if isinstance(decoded_response, ResultSubset):
-                self.finished = True
-                self.json_rpc_client.request_finished(self.request_id)
-                self.json_rpc_client.request_finished(self.owner_uri)
-
-            return decoded_response
-
-        except Exception as error:      # pylint: disable=broad-except
-            self.finished = True
-            self.json_rpc_client.request_finished(self.request_id)
-            self.json_rpc_client.request_finished(self.owner_uri)
-            return ResultSubset(None, error_message=str(error))
-
-    def execute(self):
-        self.json_rpc_client.submit_request(
-            self.METHOD_NAME, self.params.format(), self.request_id)
+        super(QuerySubsetRequest, self).__init__(request_id, owner_uri, json_rpc_client,
+                                                 QuerySubsetParams(parameters),
+                                                 u'query/subset', ResultSubset)
+
+    @classmethod
+    def response_error(cls, error):
+        return ResultSubset(None, error_message=str(error))
 
     @staticmethod
     def decode_response(response):
diff --git a/tests/jsonrpc/__init__.py b/tests/jsonrpc/__init__.py
new file mode 100644
index 0000000..23a2353
--- /dev/null
+++ b/tests/jsonrpc/__init__.py
@@ -0,0 +1,8 @@
+BASELINE_REQUEST = {
+    u'jsonrpc': u'2.0',
+    u'params': {
+        u'Key': u'Value'
+    },
+    u'method': u'testMethod/DoThis',
+    u'id': 1
+}
diff --git a/tests/jsonrpc/test_jsonrpc.py b/tests/jsonrpc/test_jsonrpc.py
index 3ad0fd3..5d7c6d9 100644
--- a/tests/jsonrpc/test_jsonrpc.py
+++ b/tests/jsonrpc/test_jsonrpc.py
@@ -6,7 +6,7 @@
 import io
 import unittest
 import mssqlcli.jsonrpc.jsonrpcclient as jsonrpc
-
+from jsonrpc import BASELINE_REQUEST
 
 class JsonRpcTest(unittest.TestCase):
     """
@@ -42,12 +42,7 @@ class JsonRpcTest(unittest.TestCase):
         test_stream.seek(0)
         json_rpc_reader = jsonrpc.JsonRpcReader(test_stream)
         response = json_rpc_reader.read_response()
-        baseline = {
-            u'jsonrpc': u'2.0',
-            u'params': {
-                u'Key': u'Value'},
-            u'method': u'testMethod/DoThis',
-            u'id': 1}
+        baseline = BASELINE_REQUEST
         self.assertEqual(response, baseline)
 
         json_rpc_reader.close()
diff --git a/tests/jsonrpc/test_jsonrpcclient.py b/tests/jsonrpc/test_jsonrpcclient.py
index 5efcfe2..444b3a8 100644
--- a/tests/jsonrpc/test_jsonrpcclient.py
+++ b/tests/jsonrpc/test_jsonrpcclient.py
@@ -4,7 +4,7 @@ import io
 import threading
 import pytest
 import mssqlcli.jsonrpc.jsonrpcclient as json_rpc_client
-
+from jsonrpc import BASELINE_REQUEST
 
 class JsonRpcClientTests(unittest.TestCase):
     """
@@ -212,6 +212,7 @@ class JsonRpcClientTests(unittest.TestCase):
         finally:
             test_client.shutdown()
 
+    @pytest.mark.unstable
     def test_response_stream_closed_exception(self):
         """
             Verify response stream closed, exception returned and response thread died.
@@ -305,12 +306,7 @@ class JsonRpcClientTests(unittest.TestCase):
         # Sleeping to give background threads a chance to process response.
         time.sleep(1)
 
-        baseline = {
-            u'jsonrpc': u'2.0',
-            u'params': {
-                u'Key': u'Value'},
-            u'method': u'testMethod/DoThis',
-            u'id': 1}
+        baseline = BASELINE_REQUEST
         response = test_client.get_response(request_id=1)
         self.assertEqual(response, baseline)
         test_client.shutdown()
diff --git a/tests/test_globalization.py b/tests/test_globalization.py
index 411142c..d8b29f8 100644
--- a/tests/test_globalization.py
+++ b/tests/test_globalization.py
@@ -163,7 +163,7 @@ class TestGlobalizationResultSet(GlobalizationTests):
         finally:
             shutdown(client)
 
-
+@pytest.mark.unstable
 class TestGlobalizationMetadata(GlobalizationTests):
     @staticmethod
     @pytest.fixture(scope="class")
@@ -172,7 +172,6 @@ class TestGlobalizationMetadata(GlobalizationTests):
         yield db
         clean_up_test_db(db)
 
-    @pytest.mark.unstable
     def test_schema_metadata_double(self, test_db):
         self.run_schema_metadata_validation('double', test_db)