diff --git a/README.md b/README.md index 2fbbfdc..4edda6b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A simple, lightweight library that provides access to MapR-DB. The client library supports all existing OJAI functionality and is absolutely compatible with Java OJAI connector, that runs under the MapR Data Access Gateway. -The MapR official documentation [link](https://mapr.com/docs/61/MapR-DB/JSON_DB/UsingPythonOJAIClient.html). +The MapR official documentation [link](https://mapr.com/docs/62/MapR-DB/JSON_DB/UsingPythonOJAIClient.html). To install the client library into your application, you need to follow these [install instructions](https://github.com/mapr/maprdb-python-client/blob/master/install_client.md). diff --git a/mapr/ojai/ojai_query/OJAIQueryCondition.py b/mapr/ojai/ojai_query/OJAIQueryCondition.py index b3768c8..6542c68 100644 --- a/mapr/ojai/ojai_query/OJAIQueryCondition.py +++ b/mapr/ojai/ojai_query/OJAIQueryCondition.py @@ -105,11 +105,11 @@ def not_matches_(self, field_path, regex): return self def like_(self, field_path, like_expression, escape_char=None): - self.__tokens.append({'$like': {field_path: like_expression}}) + self.__tokens.append({'$like': {field_path: [like_expression, escape_char] if escape_char else like_expression}}) return self def not_like_(self, field_path, like_expression, escape_char=None): - self.__tokens.append({'$notlike': {field_path: like_expression}}) + self.__tokens.append({'$notlike': {field_path: [like_expression, escape_char] if escape_char else like_expression}}) return self def is_(self, field_path, op, value): diff --git a/mapr/ojai/ojai_utils/ojai_list.py b/mapr/ojai/ojai_utils/ojai_list.py index 0abc491..952148f 100644 --- a/mapr/ojai/ojai_utils/ojai_list.py +++ b/mapr/ojai/ojai_utils/ojai_list.py @@ -34,6 +34,7 @@ def set_list(value, tags=False): else: internal_value = dump_document.set('dump', v).as_dictionary()['dump'] tmp_dict[k] = internal_value + dump_document.clear() ojai_list.append(tmp_dict) else: ojai_list.append(dump_document.set('dump', elem).as_dictionary()['dump']) diff --git a/mapr/ojai/storage/OJAIConnection.py b/mapr/ojai/storage/OJAIConnection.py index a0bb78d..3e29325 100644 --- a/mapr/ojai/storage/OJAIConnection.py +++ b/mapr/ojai/storage/OJAIConnection.py @@ -22,6 +22,7 @@ from mapr.ojai.exceptions.ConnectionError import ConnectionError from mapr.ojai.exceptions.IllegalArgumentError import IllegalArgumentError from mapr.ojai.exceptions.PathNotFoundError import PathNotFoundError +from mapr.ojai.exceptions.AccessDeniedError import AccessDeniedError from mapr.ojai.exceptions.StoreAlreadyExistsError import StoreAlreadyExistsError from mapr.ojai.exceptions.StoreNotFoundError import StoreNotFoundError from mapr.ojai.exceptions.UnknownServerError import UnknownServerError @@ -101,27 +102,39 @@ def __ping_connection(self, connection): @staticmethod def __parse_connection_url(connection_str): try: - url, options = re.sub('ojai:mapr:thin:v1@', '', connection_str).split('?', 1) + url, options = re.sub('ojai:mapr:thin:v1@', '', + connection_str).split('?', 1) except TypeError as e: - raise IllegalArgumentError(m='Connection string type must be str, but was {0}. \n{1}' - .format(type(connection_str), e)) + raise IllegalArgumentError( + m='Connection string type must be str, but was {0}. \n{1}' + .format(type(connection_str), e)) except ValueError as e: raise ValueError('{0}. \n{1}' .format(e, 'Common url string format' ' is [:][?].')) - options_dict = (urllib.parse.parse_qs(urllib.parse.urlparse(connection_str).query)) - auth = options_dict.get('auth', ['basic'])[0] - key = '{0}:{1}'.format(options_dict.get('user', [''])[0], options_dict.get('password', [''])[0]).encode() + + options_dict = dict() + for pair in urllib.parse.urlparse(connection_str).query.split(';'): + entry = pair.split('=') + if entry[1:]: + options_dict[entry[0]] = urllib.parse.unquote(entry[1]) + + key = '{0}:{1}'.format(options_dict.get('user', ''), options_dict.get('password', '')).encode() encoded_user_metadata = base64.b64encode(key).decode() - ssl = True if options_dict.get('ssl', ['true'])[0] == 'true' else False - ssl_ca = options_dict.get('sslCA', [''])[0] - ssl_target_name_override = options_dict.get('sslTargetNameOverride', [''])[0] + ssl = json.loads(options_dict.get('ssl', 'true')) + ssl_ca = options_dict.get('sslCA', '') - if ssl and ssl_ca == '': - raise AttributeError('sslCa path must be specified when ssl enabled.') + if ssl and not ssl_ca: + raise AttributeError( + 'sslCa path must be specified when ssl enabled.') - return url, auth, encoded_user_metadata, ssl, ssl_ca, ssl_target_name_override + return url, \ + options_dict.get('auth', 'basic'), \ + encoded_user_metadata, \ + ssl, \ + ssl_ca, \ + options_dict.get('sslTargetNameOverride', '') @staticmethod def __get_channel(url, @@ -176,7 +189,7 @@ def is_store_exists(self, store_path): elif response.error.err_code == ErrorCode.Value('TABLE_NOT_FOUND'): return False else: - raise UnknownServerError + raise UnknownServerError(m=response.error.error_message) def delete_store(self, store_path): self.__validate_store_path(store_path=store_path) @@ -196,6 +209,8 @@ def __validate_response(response): raise StoreAlreadyExistsError(m=response.error.error_message) elif response.error.err_code == ErrorCode.Value('PATH_NOT_FOUND'): raise PathNotFoundError(m=response.error.error_message) + elif response.error.err_code == ErrorCode.Value('ACCESS_DENIED'): + raise AccessDeniedError(m=response.error.error_message) elif response.error.err_code == ErrorCode.Value('TABLE_NOT_FOUND'): return False else: diff --git a/mapr/ojai/storage/OJAIDocumentStore.py b/mapr/ojai/storage/OJAIDocumentStore.py index 24123d3..b240775 100644 --- a/mapr/ojai/storage/OJAIDocumentStore.py +++ b/mapr/ojai/storage/OJAIDocumentStore.py @@ -380,8 +380,12 @@ def check_and_replace(self, doc, condition, _id=None): doc_str = OJAIDocumentStore.__get_doc_str(doc=doc, _id=_id) str_condition = OJAIDocumentStore.__get_str_condition( condition=condition) - self.__evaluate_doc(doc_str=doc_str, operation_type='REPLACE', - condition=str_condition) + try: + self.__evaluate_doc(doc_str=doc_str, operation_type='REPLACE', + condition=str_condition) + except DocumentNotFoundError: + return False + return True @staticmethod def __validate_document(doc_to_insert): diff --git a/mapr/ojai/storage/auth_interceptor.py b/mapr/ojai/storage/auth_interceptor.py index f49b79e..8a9a68d 100644 --- a/mapr/ojai/storage/auth_interceptor.py +++ b/mapr/ojai/storage/auth_interceptor.py @@ -49,6 +49,7 @@ def metadata_builder(self): value = 'basic {0}'.format(self._encoded_user_creds) else: value = 'bearer {0}'.format(self._token) + self._token = None return 'authorization', value def set_jwt_token(self, call, stream=False): diff --git a/mapr/ojai/utils/retry_utils.py b/mapr/ojai/utils/retry_utils.py index b2d868d..f497fb5 100644 --- a/mapr/ojai/utils/retry_utils.py +++ b/mapr/ojai/utils/retry_utils.py @@ -31,7 +31,12 @@ def __init__(self, # Retry checker function def retry_if_connection_not_established(exception): if isinstance(exception, _Rendezvous): - return exception.code() == StatusCode.UNAVAILABLE or exception.code() == StatusCode.RESOURCE_EXHAUSTED + if exception.code() == StatusCode.UNAUTHENTICATED \ + and exception.details() == 'STATUS_TOKEN_EXPIRED': + return True + else: + return exception.code() == StatusCode.UNAVAILABLE \ + or exception.code() == StatusCode.RESOURCE_EXHAUSTED elif isinstance(exception, ExpiredTokenError): return True else: diff --git a/setup.py b/setup.py index 5333722..07a1310 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,13 @@ setup(name='maprdb_python_client', - version='1.1.2', + version='1.1.7', description='MapR-DB Python Client', url='https://github.com/mapr/maprdb-python-client/', author='MapR, Inc.', keywords='ojai python client mapr maprdb', packages=find_packages(exclude=['test*', 'docs*', 'examples*']), + setup_requires=['wheel'], install_requires=['aenum>=2.0.10', 'grpcio>=1.9.1', 'grpcio-tools>=1.9.1', 'ojai-python-api>=1.1', 'python-dateutil>=2.6.1', 'retrying>=1.3.3', 'future>=0.16.0'], python_requires='>=2.7.*', diff --git a/test/document/test_document_with_tags.py b/test/document/test_document_with_tags.py index dc8300c..9fce970 100644 --- a/test/document/test_document_with_tags.py +++ b/test/document/test_document_with_tags.py @@ -239,3 +239,35 @@ def test_list_with_nested_dict(self): "surname": "Surname", "city": "City"}]})) self.assertEqual(doc.as_json_str(with_tags=False), json.dumps({"_id": "some_id", "list": [{"name": 55, "surname": "Surname", "city": "City"}]})) + + + # MAPRDB-2436 + def test_list_with_multiple_nested_levels(self): + test_doc_dict = \ + {"_id": "some_id", + "list": [ + 0, + {"name": 55, + "data": + {"surname": "Surname", + "city": "City", + "postal": { + "code": 1234, + "tag": "PY" + }}}]} + doc = OJAIDocument().from_dict(test_doc_dict) + self.assertEqual(doc.as_dictionary(), + test_doc_dict) + self.assertEqual(doc.as_json_str(), + json.dumps({"_id": "some_id", + "list": [ + {"$numberLong": 0}, + { + "name": {"$numberLong": 55}, + "data": + {"surname": "Surname", + "city": "City", + "postal": {"code": {"$numberLong": 1234}, + "tag": "PY"}}}]})) + self.assertEqual(doc.as_json_str(with_tags=False), + json.dumps(test_doc_dict)) diff --git a/test/query_test/test_query.py b/test/query_test/test_query.py index 2f98095..32c0a62 100644 --- a/test/query_test/test_query.py +++ b/test/query_test/test_query.py @@ -206,10 +206,18 @@ def test_like(self): qc = OJAIQueryCondition().like_('card', 'visa').close().build() self.assertEqual(qc.as_dictionary(), {'$like': {'card': 'visa'}}) + def test_like_with_escape_character(self): + qc = OJAIQueryCondition().like_('_id', '00\\\\%2%', '\\\\').close().build() + self.assertEqual(qc.as_dictionary(), {'$like': {'_id': ['00\\\\%2%', '\\\\']}}) + def test_not_like(self): qc = OJAIQueryCondition().not_like_('card', 'visa').close().build() self.assertEqual(qc.as_dictionary(), {'$notlike': {'card': 'visa'}}) + def test_not_like_with_escape_character(self): + qc = OJAIQueryCondition().not_like_('_id', '00|%2%', '|').close().build() + self.assertEqual(qc.as_dictionary(), {'$notlike': {'_id': ['00|%2%', '|']}}) + def test_empty_condition(self): qc = OJAIQueryCondition() self.assertTrue(qc.is_empty())