-
Notifications
You must be signed in to change notification settings - Fork 2
Added SQLServer_Persister #30
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| import platform | ||
| import subprocess | ||
| from collections.abc import MutableMapping | ||
| from py2store.util import ModuleNotFoundErrorNiceMessage | ||
|
|
||
| with ModuleNotFoundErrorNiceMessage(): | ||
| import pyodbc | ||
|
|
||
|
|
||
| class SQLServerPersister(MutableMapping): | ||
| def __init__(self, | ||
| conn_protocol='tcp', | ||
| host='localhost', | ||
| port='1433', | ||
| db_username='SA', | ||
| db_pass='Admin123x', | ||
| db_name='py2store', | ||
| table_name='py2store_default_table', | ||
| key_fields=('id',), | ||
| data_fields=('name',), | ||
| create_table_if_not_exists=True, | ||
| NVARCHAR_LENGTH=100): | ||
| """ | ||
|
|
||
| :param conn_protocol: Which connection protocol to use to connect to MS SQLServer | ||
| :param host: HOST where SQLServer is running | ||
| :param port: PORT of SQLServer | ||
| :param db_username: Username tht will be used to connect to the SQLServer | ||
| :param db_pass: Database password | ||
| :param db_name: Database Name, to connect to | ||
| :param table_name: Table name on which you are going to perform CRUD ops | ||
| :param key_fields: Primary key fields of the table | ||
| :param data_fields: Non primary fields of the table | ||
| :param create_table_if_not_exists: Flag to create database table if it doesn't exist previously | ||
| :param NVARCHAR_LENGTH: Length of the NVARCHAR column types in case the table isn't created previously | ||
| """ | ||
|
|
||
| self.__check_dependencies() | ||
| self._sql_server_client = pyodbc.connect('DRIVER={{ODBC Driver 17 for SQL Server}};' | ||
| 'SERVER={}:{},{};' | ||
| 'DATABASE={};' | ||
| 'UID={};' | ||
| 'PWD={}' | ||
| .format(conn_protocol, host, port, db_name, db_username, db_pass)) | ||
|
|
||
| self._cursor = self._sql_server_client.cursor() | ||
|
|
||
| self._table_name = table_name | ||
| self.key_fields = key_fields | ||
| self.data_fields = data_fields | ||
| self.NVARCHAR_LENGTH = NVARCHAR_LENGTH | ||
|
|
||
| if create_table_if_not_exists: | ||
| self.__create_table() | ||
|
|
||
| def __create_table(self): | ||
| """ | ||
| Create table if it doesn't exist previously | ||
| :return: | ||
| """ | ||
| if not self._cursor.tables(table=self._table_name, tableType='TABLE').fetchone(): | ||
| base_query = 'CREATE TABLE {table_name} ({attributes})' | ||
| attributes = '' | ||
|
|
||
| for key_field in self.key_fields: | ||
| attributes += '{column} {type} PRIMARY KEY,'.format(column=key_field, | ||
| type='NVARCHAR({})'.format(self.NVARCHAR_LENGTH)) | ||
|
|
||
| for data_field in self.data_fields: | ||
| attributes += '{column} {type},'.format(column=data_field, type='NVARCHAR(100)') | ||
|
|
||
| create_table_query = base_query.format(table_name=self._table_name, attributes=attributes) | ||
|
|
||
| self._cursor.execute(create_table_query) | ||
| self._sql_server_client.commit() | ||
|
|
||
| @staticmethod | ||
| def __check_dependencies(): | ||
| """ | ||
| Checks for the required dependencies for the OS | ||
| Currently checks are there for a Ubuntu machine only, more can be added | ||
| :return: | ||
| """ | ||
| if 'ubuntu' in platform.platform().lower(): | ||
| result = subprocess.Popen(["dpkg", "-s", "msodbcsql17"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code will be executed every time you init a new SQLServerPersister instance.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the intended behaviour, As py2store will be used for every storage type, but whenever user wants to use to for the SQLServer and creates an instance of the |
||
| out, err = result.communicate() | ||
| if not out: | ||
| raise ModuleNotFoundError("ODBC Driver for SQL Server is missing. Please refer " | ||
| "https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-" | ||
| "microsoft-odbc-driver-for-sql-server?view=sql-server-2017") | ||
|
|
||
| def __getitem__(self, k): | ||
| base_query = "SELECT * from {table} where {{conditions}}".format(table=self._table_name) | ||
|
|
||
| conditions = '' | ||
| for key, val in k.items(): | ||
| conditions += "{key} = '{val}' AND ".format(key=key, val=val) | ||
|
|
||
| select_query = base_query.format(conditions=conditions) | ||
| select_query = select_query.rstrip('AND ') | ||
|
|
||
| self._cursor.execute(select_query.format(value=k)) | ||
| record = self._cursor.fetchone() | ||
| if record: | ||
| return record | ||
| raise KeyError(f"No record found for primary_key: {k}") | ||
|
|
||
| def __setitem__(self, k, v): | ||
| base_query = "INSERT into {table} ({{attributes}}) VALUES ({{values}})".format(table=self._table_name) | ||
|
|
||
| attributes = ','.join([key for key in k]) | ||
| values = ','.join(["'{}'".format(val) for val in v]) | ||
| insert_query = base_query.format(attributes=attributes, values=values) | ||
|
|
||
| try: | ||
| self._cursor.execute(insert_query) | ||
| except pyodbc.IntegrityError as e: | ||
| raise KeyError("Cannot insert a duplicate entry") | ||
|
|
||
| self._sql_server_client.commit() | ||
|
|
||
| def __delitem__(self, k): | ||
| base_query = "DELETE FROM {table} where {{conditions}}".format(table=self._table_name) | ||
|
|
||
| conditions = '' | ||
| for key, val in k.items(): | ||
| conditions += "{key} = '{val}' AND ".format(key=key, val=val) | ||
|
|
||
| select_query = base_query.format(conditions=conditions) | ||
| select_query = select_query.rstrip('AND ') | ||
|
|
||
| self._cursor.execute(select_query.format(value=k)) | ||
| self._sql_server_client.commit() | ||
|
|
||
| def __iter__(self): | ||
| select_all_query = "SELECT * from {table};".format(table=self._table_name) | ||
| self._cursor.execute(select_all_query) | ||
| records = self._cursor.fetchall() | ||
|
|
||
| for record in records: | ||
| yield record | ||
|
|
||
| def __len__(self): | ||
| select_all_query = "SELECT * from {table};".format(table=self._table_name) | ||
| self._cursor.execute(select_all_query) | ||
| records = self._cursor.fetchall() | ||
| return len(records) | ||
|
|
||
|
|
||
| def test_sqlserver_persister(): | ||
| """ | ||
| A test case for the persister which performs many actions | ||
|
|
||
| Output of the test case is given below: | ||
|
|
||
| Adding Records | ||
| ========================= | ||
| Adding a Duplicate Record | ||
| Cannot Enter a Duplicate Record | ||
| ========================= | ||
| Fetching Records | ||
| ('1', 'Test 1') | ||
| ('2', 'Test 2') | ||
| ('3', 'Test 3') | ||
| ========================= | ||
| Iterating over the records | ||
| ('1', 'Test 1') | ||
| ('2', 'Test 2') | ||
| ('3', 'Test 3') | ||
| ========================= | ||
| Getting the length | ||
| 3 | ||
| ===================== | ||
| Deleting Records | ||
| ===================== | ||
| Getting the length AGAIN after deletion | ||
| 0 | ||
| ===================== | ||
|
|
||
| :return: | ||
| """ | ||
| sql_server_persister = SQLServerPersister() | ||
|
|
||
| print("Adding Records") | ||
| sql_server_persister[('id', 'name',)] = ('1', 'Test 1',) | ||
| sql_server_persister[('id', 'name',)] = ('2', 'Test 2',) | ||
| sql_server_persister[('id', 'name',)] = ('3', 'Test 3',) | ||
| print("=========================") | ||
|
|
||
| print("Adding a Duplicate Record") | ||
| try: | ||
| sql_server_persister[('id', 'name',)] = ('1', 'Test 4',) | ||
| except KeyError: | ||
| print("Cannot Enter a Duplicate Record") | ||
| print("=========================") | ||
|
|
||
| print("Fetching Records") | ||
| print(sql_server_persister[{'id': '1', | ||
| 'name': 'Test 1'}]) | ||
| print(sql_server_persister[{'name': 'Test 2'}]) | ||
| print(sql_server_persister[{'id': 3}]) | ||
| print("=========================") | ||
|
|
||
| print("Iterating over the records") | ||
| for record in sql_server_persister: | ||
| print(record) | ||
| print("=========================") | ||
|
|
||
| print("Getting the length") | ||
| print(len(sql_server_persister)) | ||
| print("=====================") | ||
|
|
||
| print("Deleting Records") | ||
| del sql_server_persister[{'id': '1'}] | ||
| del sql_server_persister[{'name': 'Test 2'}] | ||
| del sql_server_persister[{'id': '3', 'name': 'Test 3'}] | ||
| print("=====================") | ||
|
|
||
| print("Getting the length AGAIN after deletion") | ||
| print(len(sql_server_persister)) | ||
| print("=====================") | ||
|
|
||
|
|
||
| test_sqlserver_persister() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to make sure this table will be created, if it's not present, so user dont have to worry about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done https://github.com/i2mint/py2store/pull/30/files#diff-a7ee3ec855fb8f76cd4fc6b530d67f40R56