From db669d5515cd948209d1bae42b57480a43cd1535 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Mon, 3 Jun 2019 17:44:42 +0100 Subject: [PATCH 01/24] PYCBC-476: Add method to list buckets in Cluster. Change-Id: If5580ea40a7311c5fea3178ff13c0a14ab3e72e2 (cherry picked from commit cdbaa2553a0408fa1e2dcd373b85a522e94f0255) Reviewed-on: http://review.couchbase.org/110067 Tested-by: Ellis Breen Reviewed-by: Mike Goldsmith Tested-by: Build Bot --- couchbase/admin.py | 28 ++++++++++++++++++++++++++++ couchbase/tests/cases/admin_t.py | 14 +++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/couchbase/admin.py b/couchbase/admin.py index 348f1d3d9..b82c4dfa8 100644 --- a/couchbase/admin.py +++ b/couchbase/admin.py @@ -222,6 +222,34 @@ def bucket_remove(self, name): bucket_delete = bucket_remove + class BucketInfo(object): + """ + Information about a bucket + """ + def __init__(self, + raw_json # type: JSON + ): + self.raw_json = raw_json + + def name(self): + """ + Name of the bucket. + :return: A :class:`str` containing the bucket name. + """ + return self.raw_json.get("name") + + def __str__(self): + return "Bucket named {}".format(self.name) + + def buckets_list(self): + """ + Retrieve the list of buckets from the server + :return: An iterable of :Class:`Admin.BucketInfo` objects describing + the buckets currently active on the cluster. + """ + buckets_list = self.http_request(path='/pools/default/buckets', method='GET') + return map(Admin.BucketInfo, buckets_list.value) + def bucket_info(self, name): """ Retrieve information about the bucket. diff --git a/couchbase/tests/cases/admin_t.py b/couchbase/tests/cases/admin_t.py index 69de5d46c..7ed8cbd60 100644 --- a/couchbase/tests/cases/admin_t.py +++ b/couchbase/tests/cases/admin_t.py @@ -30,7 +30,7 @@ import couchbase import time - +import json class AdminSimpleTest(CouchbaseTestCase): def setUp(self): super(AdminSimpleTest, self).setUp() @@ -70,6 +70,18 @@ def test_bucket_param(self): bucket='default') self.assertIsNotNone(admin) + def test_bucket_list(self): + buckets_to_add = {'fred': {}, 'jane': {}, 'sally': {}} + try: + for bucket, kwargs in buckets_to_add.items(): + self.admin.bucket_create(bucket, bucket_password='password', **kwargs) + + self.assertEqual(set(), {"fred", "jane", "sally"}.difference( + set(map(Admin.BucketInfo.name, self.admin.buckets_list())))) + finally: + for bucket, kwargs in buckets_to_add.items(): + self.admin.bucket_remove(bucket) + def test_bad_request(self): self.assertRaises(HTTPError, self.admin.http_request, '/badpath') From 70ac52bb26dbd5025c268df845a4f39da4c097c2 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 4 Jun 2019 13:16:51 +0100 Subject: [PATCH 02/24] PYCBC-572 Tests should run against mock by default Change-Id: I797322cd790c5b6d43d50a20af1de069d36a34ec Reviewed-on: http://review.couchbase.org/110139 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Mike Goldsmith --- tests.ini.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.ini.sample b/tests.ini.sample index a0ceea8a6..0613ab82d 100644 --- a/tests.ini.sample +++ b/tests.ini.sample @@ -25,7 +25,7 @@ bucket_name = default bucket_password = ; Set this to true if there is a real cluster available -enabled = True +enabled = False [mock] ; Set this to enabled to use the mock enabled = True From 2bee1bfdcbbeaaa120b8f63ebd5afccef50e6358 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 4 Jun 2019 19:33:35 +0100 Subject: [PATCH 03/24] PYCBC-582: Fix touch tests with sleep that runs the event loop. Reviewed-on: http://review.couchbase.org/109852 Reviewed-by: Mike Goldsmith Tested-by: Build Bot Change-Id: Idcd2e007f212e05624375058267c21d103cdeeba Reviewed-on: http://review.couchbase.org/110157 Tested-by: Build Bot Reviewed-by: Ellis Breen --- couchbase/tests/base.py | 11 +++++++++++ couchbase/tests/cases/touch_t.py | 12 ++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/couchbase/tests/base.py b/couchbase/tests/base.py index 32cface98..6c1315d4a 100644 --- a/couchbase/tests/base.py +++ b/couchbase/tests/base.py @@ -559,6 +559,17 @@ def setUp(self, **kwargs): super(ConnectionTestCaseBase, self).setUp() self.cb = self.make_connection(**kwargs) + def sleep(self, duration): + expected_end=time.time()+duration + while True: + remaining_time=expected_end-time.time() + if remaining_time<=0: + break + try: + self.cb.get("dummy",ttl=remaining_time) + except: + pass + def tearDown(self): super(ConnectionTestCaseBase, self).tearDown() if hasattr(self, '_implDtorHook'): diff --git a/couchbase/tests/cases/touch_t.py b/couchbase/tests/cases/touch_t.py index 19bd03619..a69eb8cf2 100644 --- a/couchbase/tests/cases/touch_t.py +++ b/couchbase/tests/cases/touch_t.py @@ -33,13 +33,13 @@ def test_trivial_touch(self): self.cb.upsert(key, "value", ttl=1) rv = self.cb.touch(key, ttl=0) self.assertTrue(rv.success) - time.sleep(2) + self.sleep(2) rv = self.cb.get(key) self.assertTrue(rv.success) self.assertEqual(rv.value, "value") self.cb.touch(key, ttl=1) - time.sleep(2) + self.sleep(2) rv = self.cb.get(key, quiet=True) self.assertFalse(rv.success) self.assertTrue(E.NotFoundError._can_derive(rv.rc)) @@ -47,7 +47,7 @@ def test_trivial_touch(self): def test_trivial_multi_touch(self): kv = self.gen_kv_dict(prefix="trivial_multi_touch") self.cb.upsert_multi(kv, ttl=1) - time.sleep(2) + self.sleep(10) rvs = self.cb.get_multi(kv.keys(), quiet=True) self.assertFalse(rvs.all_ok) @@ -57,7 +57,7 @@ def test_trivial_multi_touch(self): self.assertTrue(rvs.all_ok) self.cb.touch_multi(kv.keys(), ttl=1) - time.sleep(2) + self.sleep(10) rvs = self.cb.get_multi(kv.keys(), quiet=True) self.assertFalse(rvs.all_ok) @@ -69,10 +69,10 @@ def test_dict_touch_multi(self): {k_missing : "missing_val", k_existing : "existing_val"}) self.cb.touch_multi({k_missing : 1, k_existing : 3}) - time.sleep(2) + self.sleep(2) rvs = self.cb.get_multi([k_missing, k_existing], quiet=True) self.assertTrue(rvs[k_existing].success) self.assertFalse(rvs[k_missing].success) - time.sleep(2) + self.sleep(2) rv = self.cb.get(k_existing, quiet=True) self.assertFalse(rv.success) From 5e2bd4cf7588cb78eff3912259c21ad7be7a0d3a Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Wed, 5 Jun 2019 00:46:51 +0100 Subject: [PATCH 04/24] Fix RST for PyPi Change-Id: I7adef10aa6afd984a71193166d2b7f1c784d3437 Reviewed-on: http://review.couchbase.org/110177 Tested-by: Ellis Breen Tested-by: Build Bot Reviewed-by: Eric Schneider Tested-by: Eric Schneider Reviewed-by: Ellis Breen --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index db86fd8d6..c1d1eb567 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,7 @@ version by issuing the following incantation Prerequisites ~~~~~~~~~~~~~ .. |libcouchbase_version| replace:: 2.9.0 + - Couchbase Server (http://couchbase.com/download) - libcouchbase_. version |libcouchbase_version| or greater (Bundled in Windows installer) - libcouchbase development files. From 73e1b978658c29af44af846add4a71aa5dde7b74 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 2 Jul 2019 11:28:06 +0100 Subject: [PATCH 05/24] PYCBC-480: Add Search/Admin/ClusterManager API documentation Also some small tweaks to docstrings to get correct formatting. Change-Id: Ifbeee0bd042996b1a13479afa0afd4935d4943f5 Reviewed-on: http://review.couchbase.org/111478 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Mike Goldsmith --- couchbase/bucket.py | 2 +- couchbase/cluster.py | 9 ++++++++- docs/source/api/couchbase.rst | 9 +++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/couchbase/bucket.py b/couchbase/bucket.py index e399530be..6575f0619 100644 --- a/couchbase/bucket.py +++ b/couchbase/bucket.py @@ -1504,7 +1504,7 @@ def n1ql_query(self, query, *args, **kwargs): otherwise defaulting to :class:`~.N1QLRequest`. :param query: The query to execute. This may either be a - :class:`.N1QLQuery` object, or a string (which will be + :class:`~.N1QLQuery` object, or a string (which will be implicitly converted to one). :param kwargs: Arguments for :class:`.N1QLRequest`. :return: An iterator which yields rows. Each row is a dictionary diff --git a/couchbase/cluster.py b/couchbase/cluster.py index 8dfc40066..e727390e2 100644 --- a/couchbase/cluster.py +++ b/couchbase/cluster.py @@ -56,6 +56,7 @@ def __init__(self, connection_string='couchbase://localhost', bucket_class=Bucket): """ Creates a new Cluster object + :param connection_string: Base connection string. It is an error to specify a bucket in the string. :param bucket_class: :class:`couchbase.bucket.Bucket` implementation to @@ -75,6 +76,7 @@ def authenticate(self, authenticator=None, username=None, password=None): """ Set the type of authenticator to use when opening buckets or performing cluster management operations + :param authenticator: The new authenticator to use :param username: The username to authenticate with :param password: The password to authenticate with @@ -92,6 +94,7 @@ def open_bucket(self, bucket_name, **kwargs): # type: (str, str) -> Bucket """ Open a new connection to a Couchbase bucket + :param bucket_name: The name of the bucket to open :param kwargs: Additional arguments to provide to the constructor :return: An instance of the `bucket_class` object provided to @@ -301,8 +304,11 @@ def _warning(self, clash_param_dict, auth_type): def cluster_manager(self): """ - Returns an instance of :class:`~.couchbase.admin.Admin` which may be + Returns an object which may be used to create and manage buckets in the cluster. + + :returns: the cluster manager + :rtype: couchbase.admin.Admin """ credentials = self.authenticator.get_credentials()['options'] connection_string = str(self.connstr) @@ -312,6 +318,7 @@ def n1ql_query(self, query, *args, **kwargs): """ Issue a "cluster-level" query. This requires that at least one connection to a bucket is active. + :param query: The query string or object :param args: Additional arguments to :cb_bmeth:`n1ql_query` diff --git a/docs/source/api/couchbase.rst b/docs/source/api/couchbase.rst index c4f7e3e6a..2cdbb3dca 100644 --- a/docs/source/api/couchbase.rst +++ b/docs/source/api/couchbase.rst @@ -1,3 +1,12 @@ +================= +Cluster object +================= + +.. module:: couchbase.cluster + +.. autoclass:: Cluster + :members: + ================= Bucket object ================= From 1157a6c8ec4327b4d723b38fbfa1c5a243a232b8 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 2 Jul 2019 13:06:48 +0100 Subject: [PATCH 06/24] PYCBC-581: Index/augment N1QLRequest/Analytics docs Change-Id: I10d4101bf7f5250b085f5ccf615f9277e6c4a726 Reviewed-on: http://review.couchbase.org/111482 Reviewed-by: Mike Goldsmith Tested-by: Build Bot --- couchbase/n1ql.py | 22 ++++++++++++++++++++-- docs/source/api/couchbase.rst | 7 +++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/couchbase/n1ql.py b/couchbase/n1ql.py index cc2d8b3b1..1179dce42 100644 --- a/couchbase/n1ql.py +++ b/couchbase/n1ql.py @@ -21,10 +21,11 @@ from couchbase.views.iterator import AlreadyQueriedError from couchbase.exceptions import CouchbaseError import sys +from typing import Iterator # Not used internally, but by other modules from couchbase.mutation_state import MutationState, MissingTokenError - +from couchbase import JSON class N1QLError(CouchbaseError): @property @@ -428,10 +429,22 @@ def raw(self): @property def meta(self): + """ + The metadata as a property + + :return: the query metadata + :rtype: JSON + """ return self.meta_retrieve() @property def metrics(self): + """ + Get query metrics from the metadata + + :return: a dictionary containing the metrics metadata + :rtype: JSON + """ return self.meta_retrieve().get('metrics', None) def meta_retrieve(self, meta_lookahead = None): @@ -512,7 +525,12 @@ def get_single_result(self): return r def __iter__(self): - # type: ()->JSON + # type: ()->Iterator[JSON] + """ + An iterator through the results. + + :returns: Iterator[JSON] + """ if self.buffered_remainder: while len(self.buffered_remainder)>0: yield self.buffered_remainder.pop(0) diff --git a/docs/source/api/couchbase.rst b/docs/source/api/couchbase.rst index 2cdbb3dca..4b207b1ed 100644 --- a/docs/source/api/couchbase.rst +++ b/docs/source/api/couchbase.rst @@ -312,6 +312,13 @@ N1QL Query Methods .. automethod:: n1ql_query +Analytics Query Methods +======================= + +.. currentmodule:: couchbase.bucket +.. class:: Bucket + + .. automethod:: analytics_query Full-Text Search Methods ======================== From a8479bc5916d09e5fb7c93b66f7d57d16839f97b Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Wed, 10 Jul 2019 15:58:02 +0100 Subject: [PATCH 07/24] PYCBC-608: Fix segfault due to deallocated key (release25) MOTIVATION ---------- When attaching a tracing scope to a given key prior to scheduling an operation, we decrement the reference count. This occasionally leads to dangling references to a key which has been incorrectly destroyed. The PYCBC_XDECREF command appears to be a remnant from earlier code which copied the reference and incremented the reference count. It doesn't appear to be correct or necessary now. CHANGES ------- Remove the command to decrement the key reference count. RESULTS ------- We no longer experience dangling references to deleted keys. Code works as expected. Change-Id: I61d51d691e95bae4bf9e174212d76b0dfcc62dec Reviewed-on: http://review.couchbase.org/111809 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Sergey Avseyev --- src/ext.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ext.c b/src/ext.c index 32c439686..16f7e4ff6 100644 --- a/src/ext.c +++ b/src/ext.c @@ -1688,7 +1688,6 @@ void pycbc_MultiResult_init_context(pycbc_MultiResult *self, PyObject *curkey, PYCBC_DEBUG_PYFORMAT_CONTEXT(context, "After insertion:[%R]", mres_dict); DONE: PYCBC_PYBUF_RELEASE(&keybuf); - PYCBC_XDECREF(curkey); } int pycbc_is_async_or_pipeline(const pycbc_Bucket *self) { return self->flags & PYCBC_CONN_F_ASYNC || self->pipeline_queue; } From f979af63411a672e79e72e08f4e97430de3c6904 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Wed, 31 Jul 2019 16:34:06 +0100 Subject: [PATCH 08/24] PYCBC-446: Gracefully fail on signal receipt Motivation ---------- If the Python process is interrupted by a signal, lcb_wait3 may return before all remaining jobs have completed/timed out. This can lead to an exception being thrown after the wait, as normally this would only return after this point. We should gracefully handle this scenario. Changes ------- Add a field 'check_type' to the Bucket class. Instead of using pycbc_assert to raise an abort() if there are remaining operations to complete after lcb_wait has returned, according to the check_type value, do the following: PYCBC_CHECK_NONE: don't raise an exception. PYCBC_CHECK_STRICT: (the default): raise an SDKInternalError PYCBC_CHECK_FAIL: (for testing): always fail and raise an exception Results ------- It is difficult to automatically reproduce this, but as we are now raising a Python exception rather than an abort(), as verified by the tests, the end user should be able to handle this by catching the exception if necessary. Or they can turn off checking by setting the flag to PYCBC_CHECK_NONE. Change-Id: I5497267ba58496c57fe03b040fbc4eeb67fb16bd Reviewed-on: http://review.couchbase.org/112725 Tested-by: Ellis Breen Reviewed-by: Brett Lawson Reviewed-by: Sergey Avseyev Tested-by: Build Bot --- couchbase/tests/cases/misc_t.py | 25 ++++++++++++++++++++++++- src/bucket.c | 11 ++++++++--- src/constants.c | 5 +++++ src/oputil.c | 10 +++++++++- src/pycbc.h | 8 ++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/couchbase/tests/cases/misc_t.py b/couchbase/tests/cases/misc_t.py index 66a188087..37ab50100 100644 --- a/couchbase/tests/cases/misc_t.py +++ b/couchbase/tests/cases/misc_t.py @@ -29,7 +29,7 @@ from couchbase.tests.base import ConnectionTestCaseBase from couchbase.user_constants import FMT_JSON, FMT_AUTO, FMT_JSON, FMT_PICKLE -from couchbase.exceptions import ClientTemporaryFailError +from couchbase.exceptions import ClientTemporaryFailError, InternalSDKError from couchbase.exceptions import CouchbaseError import couchbase import re @@ -37,6 +37,7 @@ from couchbase import enable_logging from couchbase import COMPRESS_INOUT import logging +import time class MiscTest(ConnectionTestCaseBase): @@ -243,3 +244,25 @@ def test_compression_named(self): import couchbase._libcouchbase as _LCB cb = self.make_connection() cb.compression =couchbase.COMPRESS_INOUT + + def test_consistency_check_pyexception(self): + items = {str(k): str(v) for k, v in zip(range(0, 100), range(0, 100))} + self.cb.upsert_multi(items) + self.cb.get_multi(items.keys()) + self.cb.check_type = _LCB.PYCBC_CHECK_FAIL + + for x in range(0, 10): + init_time = time.time() + exception = None + while (time.time() - init_time) < 10: + try: + self.cb.get_multi(items.keys()) + except Exception as e: + exception = e + break + + def raiser(): + raise exception + + self.assertRaisesRegex(InternalSDKError, r'self->nremaining!=0, resetting to 0', raiser) + diff --git a/src/bucket.c b/src/bucket.c index a1d8ec82e..120ef937d 100644 --- a/src/bucket.c +++ b/src/bucket.c @@ -617,6 +617,11 @@ static struct PyMemberDef Bucket_TABLE_members[] = { "This attribute can only be set from the constructor.\n") }, + { "check_type", T_UINT, offsetof(pycbc_Bucket, check_type), + 0, + PyDoc_STR("What sort of nremaining consistency check to do after a wait.") + }, + { "bucket", T_OBJECT_EX, offsetof(pycbc_Bucket, bucket), READONLY, PyDoc_STR("Name of the bucket this object is connected to") @@ -859,13 +864,13 @@ Bucket__init__(pycbc_Bucket *self, PyObject *dfl_fmt = NULL; PyObject *tc = NULL; struct lcb_create_st create_opts = { 0 }; - + self->check_type = PYCBC_CHECK_STRICT; /** * This xmacro enumerates the constructor keywords, targets, and types. * This was converted into an xmacro to ease the process of adding or * removing various parameters. */ -#define XCTOR_ARGS_NOTRACING(X) \ +#define XCTOR_ARGS_NOTRACING(X) \ X("connection_string", &create_opts.v.v3.connstr, "z") \ X("connstr", &create_opts.v.v3.connstr, "z") \ X("username", &create_opts.v.v3.username, "z") \ @@ -878,7 +883,7 @@ Bucket__init__(pycbc_Bucket *self, X("_flags", &self->flags, "I") \ X("_conntype", &conntype, "i") \ X("_iops", &iops_O, "O") \ - + X("_check_inconsistent", &self->check_type, "I") #ifdef PYCBC_TRACING #define XCTOR_ARGS(X)\ XCTOR_ARGS_NOTRACING(X)\ diff --git a/src/constants.c b/src/constants.c index 1941005b8..3dd507183 100644 --- a/src/constants.c +++ b/src/constants.c @@ -201,6 +201,11 @@ do_all_constants(PyObject *module, pycbc_constant_handler handler) ADD_MACRO(LCB_CNTL_N1QL_TIMEOUT); ADD_MACRO(LCB_CNTL_COMPRESSION_OPTS); ADD_MACRO(LCB_CNTL_LOG_REDACTION); + + ADD_MACRO(PYCBC_CHECK_NONE); + ADD_MACRO(PYCBC_CHECK_STRICT); + ADD_MACRO(PYCBC_CHECK_FAIL); + ADD_STRING(LCB_LOG_MD_OTAG); ADD_STRING(LCB_LOG_MD_CTAG); ADD_STRING(LCB_LOG_SD_OTAG); diff --git a/src/oputil.c b/src/oputil.c index 994dc7f3d..ac97805b8 100644 --- a/src/oputil.c +++ b/src/oputil.c @@ -64,11 +64,18 @@ pycbc_common_vars_wait, struct pycbc_common_vars *cv, pycbc_Bucket *self) } pycbc_oputil_wait_common(self, context); - if (!pycbc_assert(self->nremaining == 0)) { + if (self->nremaining || self->check_type == PYCBC_CHECK_FAIL) { fprintf(stderr, "Remaining count %d!= 0. Adjusting", (int)self->nremaining); self->nremaining = 0; + if (self->check_type != PYCBC_CHECK_NONE) { + cv->ret = NULL; + PYCBC_EXC_WRAP(PYCBC_EXC_INTERNAL, + 0, + "self->nremaining!=0, resetting to 0"); + goto FAIL; + } } if (pycbc_multiresult_maybe_raise(cv->mres)) { @@ -76,6 +83,7 @@ pycbc_common_vars_wait, struct pycbc_common_vars *cv, pycbc_Bucket *self) } cv->ret = pycbc_multiresult_get_result(cv->mres); +FAIL: Py_DECREF(cv->mres); cv->mres = NULL; diff --git a/src/pycbc.h b/src/pycbc.h index c3c49b0bb..2886abc79 100644 --- a/src/pycbc.h +++ b/src/pycbc.h @@ -507,6 +507,12 @@ struct pycbc_Tracer; #endif #endif +typedef enum { + PYCBC_CHECK_STRICT, + PYCBC_CHECK_NONE, + PYCBC_CHECK_FAIL +} pycbc_check_type; + typedef struct { PyObject_HEAD @@ -579,6 +585,8 @@ typedef struct { pycbc_dur_params dur_global; unsigned long dur_timeout; + pycbc_check_type check_type; + } pycbc_Bucket; #ifdef PYCBC_TRACING From 7385cd802ac8f96f7a36dffe14bf0d1cf284c936 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Mon, 5 Aug 2019 18:39:13 +0100 Subject: [PATCH 09/24] PYCBC-478: Expose settings for compression min_size/min_ratio Change-Id: I867a14089a15b51cfffbd5681e451f780b38ac53 Reviewed-on: http://review.couchbase.org/112932 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Brett Lawson Reviewed-by: Sergey Avseyev --- couchbase/bucket.py | 26 ++++++++++++++++++++++++ couchbase/tests/cases/misc_t.py | 36 ++++++++++++++++++++++++--------- docs/source/api/couchbase.rst | 4 ++++ src/constants.c | 2 ++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/couchbase/bucket.py b/couchbase/bucket.py index 6575f0619..b16c022a7 100644 --- a/couchbase/bucket.py +++ b/couchbase/bucket.py @@ -1736,6 +1736,32 @@ def compression(self): def compression(self, value): self._cntl(_LCB.LCB_CNTL_COMPRESSION_OPTS, value_type='int', value=value) + @property + def compression_min_size(self): + """ + Minimum size (in bytes) of the document payload to be compressed when compression enabled. + + :type: int + """ + return self._cntl(_LCB.LCB_CNTL_COMPRESSION_MIN_SIZE, value_type='uint32_t') + + @compression_min_size.setter + def compression_min_size(self, value): + self._cntl(_LCB.LCB_CNTL_COMPRESSION_MIN_SIZE, value_type='uint32_t', value=value) + + @property + def compression_min_ratio(self): + """ + Minimum compression ratio (compressed / original) of the compressed payload to allow sending it to cluster. + + :type: float + """ + return self._cntl(_LCB.LCB_CNTL_COMPRESSION_MIN_RATIO, value_type='float') + + @compression_min_ratio.setter + def compression_min_ratio(self, value): + self._cntl(_LCB.LCB_CNTL_COMPRESSION_MIN_RATIO, value_type='float', value=value) + @property def is_ssl(self): """ diff --git a/couchbase/tests/cases/misc_t.py b/couchbase/tests/cases/misc_t.py index 37ab50100..67c48c8f1 100644 --- a/couchbase/tests/cases/misc_t.py +++ b/couchbase/tests/cases/misc_t.py @@ -29,7 +29,7 @@ from couchbase.tests.base import ConnectionTestCaseBase from couchbase.user_constants import FMT_JSON, FMT_AUTO, FMT_JSON, FMT_PICKLE -from couchbase.exceptions import ClientTemporaryFailError, InternalSDKError +from couchbase.exceptions import ClientTemporaryFailError, InternalSDKError, ArgumentError, CouchbaseInputError from couchbase.exceptions import CouchbaseError import couchbase import re @@ -229,16 +229,34 @@ def test_multi_auth(self): def test_compression(self): import couchbase._libcouchbase as _LCB items = list(_LCB.COMPRESSION.items()) - for entry in range(0, len(items)*2): + for entry in range(0, len(items) * 2): connstr, cntl = items[entry % len(items)] print(connstr + "," + str(cntl)) - cb = self.make_connection(compression=connstr) - self.assertEqual(cb.compression, cntl) - value = "world" + str(entry) - cb.upsert("hello", value) - cb.compression = items[(entry + 1) % len(items)][1] - self.assertEqual(value, cb.get("hello").value) - cb.remove("hello") + sends_compressed = self.send_compressed(entry) + for min_size in [0, 31, 32] if sends_compressed else [None]: + for min_ratio in [0, 0.5] if sends_compressed else [None]: + def set_comp(): + cb.compression_min_size = min_size + + cb = self.make_connection(compression=connstr) + if min_size: + if min_size < 32: + self.assertRaises(CouchbaseInputError, set_comp) + else: + set_comp() + + if min_ratio: + cb.compression_min_ratio = min_ratio + self.assertEqual(cb.compression, cntl) + value = "world" + str(entry) + cb.upsert("hello", value) + cb.compression = items[(entry + 1) % len(items)][1] + self.assertEqual(value, cb.get("hello").value) + cb.remove("hello") + + @staticmethod + def send_compressed(entry): + return entry in map(_LCB.__getattribute__, ('COMPRESS_FORCE', 'COMPRESS_INOUT', 'COMPRESS_OUT')) def test_compression_named(self): import couchbase._libcouchbase as _LCB diff --git a/docs/source/api/couchbase.rst b/docs/source/api/couchbase.rst index 4b207b1ed..2f8a59eda 100644 --- a/docs/source/api/couchbase.rst +++ b/docs/source/api/couchbase.rst @@ -488,6 +488,10 @@ Attributes .. autoattribute:: compression + .. autoattribute:: compression_min_size + + .. autoattribute:: compression_min_ratio + .. attribute:: default_format Specify the default format (default: :const:`~couchbase.FMT_JSON`) diff --git a/src/constants.c b/src/constants.c index 3dd507183..5d96dfb7f 100644 --- a/src/constants.c +++ b/src/constants.c @@ -247,6 +247,8 @@ do_all_constants(PyObject *module, pycbc_constant_handler handler) ADD_CONSTANT("PYCBC_TRACING",0); #endif setup_compression_map(module, public_constants, handler); + ADD_MACRO(LCB_CNTL_COMPRESSION_MIN_SIZE); + ADD_MACRO(LCB_CNTL_COMPRESSION_MIN_RATIO); setup_crypto_exceptions(module, handler); PyModule_AddObject( module, "CRYPTO_EXCEPTIONS", pycbc_gen_crypto_exception_map()); From cd08660df53dfbe8ba960ca88c25323fb9ce9b2a Mon Sep 17 00:00:00 2001 From: Matt Carabine Date: Thu, 29 Aug 2019 12:39:41 +0100 Subject: [PATCH 10/24] PYCBC-635: Make typing module requirement conditional Motivation ---------- The Python SDK uses the typing module to provide basic type hints to users of the library. This was added in Python 3.5 to the standard library and is available as an external module for earlier versions of Python. While this module can be installed on later versions of Python, this is bad practice as it will clobber the standard library version for no benefit. Many build systems may actively prevent this clobbering occurring, which means that the Python SDK can then not be installed. Changes ------- Make the typing module conditional on the version of Python being used. It will now only install typing if the Python version is less than 3.7. A similar change was already in master, but this backports it to the 2.x release line. Results ------- For versions of Python 3.7 or greater, the typing module is now no longer installed. It is still installed for all releases of Python that need to have it installed. Change-Id: Ibf62532191380c199a851c759fe99e1644e752c5 Reviewed-on: http://review.couchbase.org/114013 Tested-by: Build Bot Reviewed-by: Ellis Breen --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a7085b781..a4a648e7d 100644 --- a/setup.py +++ b/setup.py @@ -230,8 +230,8 @@ def comp_option_pattern(prefix): 'acouchbase.py34only' ] if sys.version_info >= (3, 4) else []), package_data=pkgdata, - setup_requires=['typing'] + conan_and_cmake_deps, - install_requires=['typing'] + pip_not_on_win_python_lt_3, + setup_requires=["typing; python_version<'3.7'"] + conan_and_cmake_deps, + install_requires=["typing; python_version<'3.7'"] + pip_not_on_win_python_lt_3, tests_require=['nose', 'testresources>=0.2.7', 'basictracer==2.2.0'], test_suite='couchbase.tests.test_sync', **setup_kw From 56754ee8ffc76f5565e499b3ee876f0009d3fc00 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Thu, 29 Aug 2019 14:46:25 +0100 Subject: [PATCH 11/24] PYCBC-636: Fix broken dev_requirements.txt Change-Id: I8c43bf2608cd38d9144c05992aa466d7b8edd307 Reviewed-on: http://review.couchbase.org/114017 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Mike Goldsmith --- dev_requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 2a469dd6d..c8feda753 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,8 +1,8 @@ requests nose==1.3.0 pbr==1.8.1 -numpydoc==0.4 -sphinx==1.6.4 +numpydoc==0.9.1 +sphinx==1.8.5 testresources>=0.2.7 jsonschema==2.6.0 configparser==3.5.0 From 9c9906e0b59212f7cebca1cdc8338e44aed160db Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Fri, 30 Aug 2019 16:03:56 +0100 Subject: [PATCH 12/24] PYCBC-642: Release Notes Generator Generates release notes by querying JIRA using the official JIRA Python Client. To be used to produce preliminary release notes for human editing and review only. Change-Id: Ia89eb749a34fd941f8874abd7747724e2da59173 Reviewed-on: http://review.couchbase.org/113883 Tested-by: Ellis Breen Reviewed-by: Mike Goldsmith (cherry picked from commit 656321bded0d19159045a9da58a7c104c60ef0e9) Reviewed-on: http://review.couchbase.org/114078 Tested-by: Build Bot --- dev_requirements.txt | 2 ++ docs/source/relnotes.py | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 docs/source/relnotes.py diff --git a/dev_requirements.txt b/dev_requirements.txt index c8feda753..aa8f39a2b 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -14,3 +14,5 @@ configparser2 parameterized==0.6.1 utilspie==0.1.0 python-vagrant>=0.5.15 +beautifulsoup4==4.7.1 +jira==2.0.0 diff --git a/docs/source/relnotes.py b/docs/source/relnotes.py new file mode 100644 index 000000000..f0b37fc11 --- /dev/null +++ b/docs/source/relnotes.py @@ -0,0 +1,72 @@ +from collections import defaultdict +from bs4 import BeautifulSoup +import requests +import os.path +import re +from jira import JIRA +import argparse +import datetime + +server = "https://issues.couchbase.com" +options = dict(server=server) +jira = JIRA(options) +project_code = "PYCBC" +project = jira.project(project_code) +print("got project {}".format(project.versions)) +parser = argparse.ArgumentParser(description="Generate release notes in Asciidoc format") +parser.add_argument('version',type=str) +args=parser.parse_args() +ver_num = args.version +project_version = next(iter(filter(lambda x: x.name == ver_num, project.versions)), None) +relnotes_raw = requests.get( + "{}/secure/ReleaseNote.jspa?projectId={}&version={}".format(server, project.id, + project_version.id)) +soup = BeautifulSoup(relnotes_raw.text, 'html.parser') +content = soup.find("section", class_="aui-page-panel-content") +outputdir = os.path.join("build") + +date = datetime.date.today().strftime("%B {day} %Y").format(day=datetime.date.today().day) +try: + os.makedirs(outputdir) +except: + pass +with open(os.path.join(outputdir, "relnotes.adoc"), "w+") as outputfile: + section_type = None + result = defaultdict(lambda: []) + mapping = {"Enhancements": r'(Task|Improvement).*', + "Fixes": r'(Bugs).*'} + version = re.match(r'^(.*)Version ([0-9]+\.[0-9]+\.[0-9]+).*$', content.title.text).group(2) + print("got version {}".format(version)) + for entry in content.body.find_all(): + if re.match(r'h[0-9]+', entry.name): + section_type = mapping.get(entry.text.strip(), "Enhancements") + if re.match("Edit/Copy Release Notes", entry.text): + break + else: + if section_type: + items = entry.find_all('li') + output = [] + for item in items: + link = item.find('a') + output.append(dict(link=link.get('href'), issuenumber=link.text, + description=re.sub(r'^(.*?)- ', '', item.text))) + result[section_type] += output + + output = """ +== Python SDK {version} Release Notes ({date}) + +[source,bash] +---- +pip install couchbase=={version} +---- + +*API Docs:* http://docs.couchbase.com/sdk-api/couchbase-python-client-{version}/ + +{contents} +""".format(version=version, date=date, contents='\n'.join( + "=== {type}\n\n{value}".format(type=type, value='\n'.join( + """* {link}[{issuenumber}]: +{description}\n""".format(**item) for item in value)) for type, value in result.items())) + + print(output) + outputfile.write(output) From bbdd7d4e63da06c5447ec77e960536299ec44f5c Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 3 Sep 2019 12:06:56 +0100 Subject: [PATCH 13/24] PYCBC-618: Make Ping status codes human-readable and document them Change-Id: I2fccad65a36f246dcd2ebe169dd494aa21300f06 Reviewed-on: http://review.couchbase.org/114165 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Mike Goldsmith --- couchbase/bucket.py | 19 ++++++++++++++++--- couchbase/tests/cases/diag_t.py | 4 +++- setup.py | 7 +++++-- src/constants.c | 7 +++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/couchbase/bucket.py b/couchbase/bucket.py index b16c022a7..9a70cf84a 100644 --- a/couchbase/bucket.py +++ b/couchbase/bucket.py @@ -36,10 +36,14 @@ import json from couchbase.analytics import AnalyticsRequest, AnalyticsQuery from couchbase.connstr import ConnectionString - +from enum import IntEnum ### Private constants. This is to avoid imposing a dependency requirement ### For simple flags: +class PingStatus(IntEnum): + OK=_LCB.LCB_PINGSTATUS_OK + TIMEOUT=_LCB.LCB_PINGSTATUS_TIMEOUT + ERROR=_LCB.LCB_PINGSTATUS_ERROR def _depr(fn, usage, stacklevel=3): """Internal convenience function for deprecation warnings""" @@ -933,8 +937,17 @@ def ping(self): :raise: :exc:`.CouchbaseNetworkError` - :return: `dict` where keys are stat keys and values are - host-value pairs + :return: `dict` where keys are service types and values are + lists of dictionaries, each one describing a single + node. + + The 'status' entry of each value corresponds to an integer enum: + + PingStatus.OK(0) = ping responded in time + + PingStatus.TIMEOUT(1) = ping timed out + + PingStatus.ERROR(2) = there was some other error while trying to ping the host. Ping cluster (works on couchbase buckets):: diff --git a/couchbase/tests/cases/diag_t.py b/couchbase/tests/cases/diag_t.py index 61c0bdb80..23644f167 100644 --- a/couchbase/tests/cases/diag_t.py +++ b/couchbase/tests/cases/diag_t.py @@ -21,6 +21,7 @@ import jsonschema import re import couchbase._libcouchbase as LCB +from couchbase.bucket import PingStatus # For Python 2/3 compatibility try: @@ -32,7 +33,8 @@ "properties": {"details": {"type": "string"}, "latency": {"anyOf": [{"type": "number"}, {"type": "string"}]}, "server": {"type": "string"}, - "status": {"type": "number"} + "status": {"type": "number", + "enum": list(PingStatus)} }, "required": ["latency", "server", "status"]} diff --git a/setup.py b/setup.py index a4a648e7d..f4d275c87 100644 --- a/setup.py +++ b/setup.py @@ -190,6 +190,9 @@ def comp_option_pattern(prefix): conan_and_cmake_deps = (['conan', 'cmake>=3.0.2'] if cmake_build and sys.platform.startswith('darwin') else []) +gen_reqs = ["typing; python_version<'3.7'", + "enum34; python_version<'3.5'"] + setup( name = 'couchbase', version = pkgversion, @@ -230,8 +233,8 @@ def comp_option_pattern(prefix): 'acouchbase.py34only' ] if sys.version_info >= (3, 4) else []), package_data=pkgdata, - setup_requires=["typing; python_version<'3.7'"] + conan_and_cmake_deps, - install_requires=["typing; python_version<'3.7'"] + pip_not_on_win_python_lt_3, + setup_requires=gen_reqs + conan_and_cmake_deps, + install_requires=gen_reqs + pip_not_on_win_python_lt_3, tests_require=['nose', 'testresources>=0.2.7', 'basictracer==2.2.0'], test_suite='couchbase.tests.test_sync', **setup_kw diff --git a/src/constants.c b/src/constants.c index 5d96dfb7f..ecabe39c6 100644 --- a/src/constants.c +++ b/src/constants.c @@ -238,6 +238,13 @@ do_all_constants(PyObject *module, pycbc_constant_handler handler) ADD_MACRO(LCBCRYPTO_KEY_ENCRYPT); ADD_MACRO(LCBCRYPTO_KEY_DECRYPT); +#define PYCBC_PING_STATUS(X) ADD_MACRO(LCB_PINGSTATUS_##X); +#define PYCBC_PP_PING_STATUS(X) \ + X(OK) \ + X(TIMEOUT) \ + X(ERROR) + + PYCBC_PP_PING_STATUS(PYCBC_PING_STATUS) LCB_CONSTANT(VERSION); ADD_MACRO(PYCBC_CRYPTO_VERSION); #ifdef PYCBC_TRACING From 206738cef730eed9ea34e9bfac59831b522d1bb9 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 3 Sep 2019 10:55:57 +0100 Subject: [PATCH 14/24] PYCBC-642: Release note fixes Change-Id: Ic484821d3d433958af3d707bc4f19fcbc06e9e8a Reviewed-on: http://review.couchbase.org/114164 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Mike Goldsmith --- dev_requirements.txt | 2 +- docs/source/relnotes.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index aa8f39a2b..8000cc478 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,6 +1,6 @@ requests nose==1.3.0 -pbr==1.8.1 +pbr==3.0.0 numpydoc==0.9.1 sphinx==1.8.5 testresources>=0.2.7 diff --git a/docs/source/relnotes.py b/docs/source/relnotes.py index f0b37fc11..2524742f0 100644 --- a/docs/source/relnotes.py +++ b/docs/source/relnotes.py @@ -33,13 +33,13 @@ with open(os.path.join(outputdir, "relnotes.adoc"), "w+") as outputfile: section_type = None result = defaultdict(lambda: []) - mapping = {"Enhancements": r'(Task|Improvement).*', - "Fixes": r'(Bugs).*'} + mapping = {"Task": "Enhancements", + "Bugs": "Fixes"} version = re.match(r'^(.*)Version ([0-9]+\.[0-9]+\.[0-9]+).*$', content.title.text).group(2) print("got version {}".format(version)) for entry in content.body.find_all(): if re.match(r'h[0-9]+', entry.name): - section_type = mapping.get(entry.text.strip(), "Enhancements") + section_type = mapping.get(entry.text.strip().replace('Improvement', 'Task'), "Enhancements") if re.match("Edit/Copy Release Notes", entry.text): break else: From 16b5044d1b01f5296df65d054a6398587140085e Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 1 Oct 2019 14:22:38 +0100 Subject: [PATCH 15/24] PYCBC-659: Cover new services in Ping command Change-Id: I0ded190af21ab5d2165a674e02a13da011e677ce Reviewed-on: http://review.couchbase.org/115677 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Brett Lawson --- src/miscops.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miscops.c b/src/miscops.c index e14fc3a6e..8cdae76ef 100644 --- a/src/miscops.c +++ b/src/miscops.c @@ -397,7 +397,7 @@ TRACED_FUNCTION_WRAPPER(_ping,LCBTRACE_OP_REQUEST_ENCODING,Bucket) struct pycbc_common_vars cv = PYCBC_COMMON_VARS_STATIC_INIT; lcb_CMDPING cmd = {0}; cmd.services = LCB_PINGSVC_F_KV | LCB_PINGSVC_F_N1QL | LCB_PINGSVC_F_VIEWS | - LCB_PINGSVC_F_FTS; + LCB_PINGSVC_F_FTS | LCB_PINGSVC_F_ANALYTICS; cmd.options = LCB_PINGOPT_F_JSON | LCB_PINGOPT_F_JSONPRETTY; if (1) { cmd.options |= LCB_PINGOPT_F_JSONDETAILS; From e5a30e1f9969339b15fa05337a110814ba544dae Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Thu, 14 Nov 2019 14:58:54 +0000 Subject: [PATCH 16/24] PYCBC-653: Fix Python 2.7 pip niggle with conditional reqs Change-Id: I957621b3ca679ad73e8c4ffbc1d39ddda1d41ce9 Reviewed-on: http://review.couchbase.org/114550 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: David Kelly --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index f4d275c87..61acf8846 100644 --- a/setup.py +++ b/setup.py @@ -183,15 +183,15 @@ def comp_option_pattern(prefix): # Dummy dependency to prevent installation of Python < 3 package on Windows. pip_not_on_win_python_lt_3 = ( - ["pip>=9.0; (sys_platform != 'win32' and python_version >= '2.7') or (python_version >= '3.0')"] + ['pip>=9.0; (sys_platform != "win32" and python_version >= "2.7") or (python_version >= "3.0")'] if pip.__version__ >= "9.0.0" else []) conan_and_cmake_deps = (['conan', 'cmake>=3.0.2'] if cmake_build and sys.platform.startswith('darwin') else []) -gen_reqs = ["typing; python_version<'3.7'", - "enum34; python_version<'3.5'"] +gen_reqs = ['typing; python_version<"3.7"', + 'enum34; python_version<"3.5"'] setup( name = 'couchbase', From f6f6ffc82499807a73974b88913362e0a744830d Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 17 Dec 2019 21:53:09 +0000 Subject: [PATCH 17/24] PYCBC-724: Fix version detection code. Change-Id: Ibb27e940abe80f19106656d7d69abe35c287b483 Reviewed-on: http://review.couchbase.org/119518 Tested-by: Ellis Breen Reviewed-by: David Kelly Tested-by: Build Bot --- couchbase_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchbase_version.py b/couchbase_version.py index f0411617d..e0869a9e6 100755 --- a/couchbase_version.py +++ b/couchbase_version.py @@ -20,7 +20,7 @@ class MalformedGitTag(Exception): pass -RE_XYZ = re.compile(r'(\d+)\.(\d+)\.(\d)(?:-(.*))?') +RE_XYZ = re.compile(r'(\d+)\.(\d+)\.(\d+)(?:-(.*))?') VERSION_FILE = os.path.join( os.path.dirname(__file__), From 3ad59d6437503409360977180658139fb145833c Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Tue, 31 Dec 2019 11:43:56 +0000 Subject: [PATCH 18/24] PYCBC-702: allow custom SSL cert for all auth types Change-Id: Icd22e5bca452d0a242fe5f88a784f65d1d6d3e52 Reviewed-on: http://review.couchbase.org/119851 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: David Kelly --- couchbase/cluster.py | 77 ++++++++++++++++++++++-------- couchbase/tests/cases/cluster_t.py | 44 +++++++++++++++++ dev_requirements.txt | 1 + 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/couchbase/cluster.py b/couchbase/cluster.py index e727390e2..e4d987162 100644 --- a/couchbase/cluster.py +++ b/couchbase/cluster.py @@ -344,7 +344,29 @@ def n1ql_query(self, query, *args, **kwargs): raise NoBucketError('Must have at least one active bucket for query') +def _recursive_creds_merge(base, overlay): + for k, v in overlay.items(): + base_k = base.get(k, None) + if not base_k: + base[k] = v + continue + if isinstance(v, dict): + if isinstance(base_k, dict): + base[k] = _recursive_creds_merge(base_k, v) + else: + raise Exception("Cannot merge dict and {}".format(v)) + else: + raise Exception("Cannot merge non dicts") + return base + + class Authenticator(object): + def __init__(self, cert_path = None): + """ + :param cert_path: Path for SSL certificate (last in chain if multiple) + """ + self._cert_path = cert_path + def get_credentials(self, bucket=None): """ Gets the credentials for a specified bucket. If bucket is @@ -381,23 +403,27 @@ def get_unique_creds_dict(cls): """ return {} - def get_cred_bucket(self, bucket): + def _base_options(self, bucket, overlay): + base_dict = {'options': {'cert_path': self._cert_path} if self._cert_path else {}} + return _recursive_creds_merge(base_dict, overlay) + + def get_cred_bucket(self, bucket, **overlay): """ :param bucket: :return: returns the non-unique parts of the credentials for bucket authentication, as a dictionary of functions, e.g.: 'options': {'username': self.username}, 'scheme': 'couchbases'} """ - raise NotImplementedError() + return self._base_options(bucket, overlay) - def get_cred_not_bucket(self): + def get_cred_not_bucket(self, **overlay): """ :param bucket: :return: returns the non-unique parts of the credentials for admin access as a dictionary of functions, e.g.: {'options':{'password': self.password}} """ - raise NotImplementedError() + return self._base_options(None, overlay) def get_auto_credentials(self, bucket): """ @@ -415,7 +441,7 @@ def get_auto_credentials(self, bucket): class PasswordAuthenticator(Authenticator): - def __init__(self, username, password): + def __init__(self, username, password, cert_path=None): """ This class uses a single credential pair of username and password, and is designed to be used either with cluster management operations or @@ -426,19 +452,23 @@ def __init__(self, username, password): :param username: :param password: + :param cert_path: + Path of the CA key .. warning:: This functionality is experimental both in API and implementation. """ + super(PasswordAuthenticator,self).__init__(cert_path=cert_path) self.username = username self.password = password - def get_cred_bucket(self, *unused): - return {'options': {'username': self.username, 'password': self.password}} + def get_cred_bucket(self, bucket, **overlay): + return self.get_cred_not_bucket(**overlay) - def get_cred_not_bucket(self): - return self.get_cred_bucket() + def get_cred_not_bucket(self, **overlay): + merged = _recursive_creds_merge({'options': {'username': self.username, 'password': self.password}}, overlay) + return super(PasswordAuthenticator, self).get_cred_not_bucket(**merged) @classmethod def unwanted_keys(cls): @@ -448,7 +478,8 @@ def unwanted_keys(cls): class ClassicAuthenticator(Authenticator): def __init__(self, cluster_username=None, cluster_password=None, - buckets=None): + buckets=None, + cert_path=None): """ Classic authentication mechanism. :param cluster_username: @@ -457,16 +488,20 @@ def __init__(self, cluster_username=None, Global cluster password. Only required for management operations :param buckets: A dictionary of `{bucket_name: bucket_password}`. + :param cert_path: + Path of the CA key """ + super(ClassicAuthenticator, self).__init__(cert_path=cert_path) self.username = cluster_username self.password = cluster_password self.buckets = buckets if buckets else {} def get_cred_not_bucket(self): - return {'options': {'username': self.username, 'password': self.password}} + return super(ClassicAuthenticator, self).get_cred_not_bucket(**{'options': {'username': self.username, 'password': self.password}}) - def get_cred_bucket(self, bucket): - return {'options': {'password': self.buckets.get(bucket)}} + def get_cred_bucket(self, bucket, **overlay): + merged=_recursive_creds_merge({'options': {'password': self.buckets.get(bucket)}}, overlay) + return super(ClassicAuthenticator, self).get_cred_bucket(bucket, **merged) class CertAuthenticator(Authenticator): @@ -486,22 +521,26 @@ def __init__(self, cert_path=None, key_path=None, trust_store_path=None, cluster :param trust_store_path: Path of the certificate trust store. """ + super(CertAuthenticator, self).__init__(cert_path=cert_path) + self.username = cluster_username self.password = cluster_password - self.certpath = cert_path self.keypath = key_path self.trust_store_path = trust_store_path @classmethod def get_unique_creds_dict(clazz): - return {'certpath': lambda self: self.certpath, 'keypath': lambda self: self.keypath, - 'truststorepath': lambda self: self.trust_store_path} + return { 'keypath': lambda self: self.keypath, + 'truststorepath': lambda self: self.trust_store_path} - def get_cred_bucket(self, *unused): - return {'options': {'username': self.username}, 'scheme': 'couchbases'} + def get_cred_bucket(self, bucket, **overlay): + merged = _recursive_creds_merge( + {'options': {'username': self.username}, 'scheme': 'couchbases'}, + overlay) + return super(CertAuthenticator, self).get_cred_bucket(bucket, **merged) def get_cred_not_bucket(self): - return {'options': {'password': self.password}} + return super(CertAuthenticator, self).get_cred_not_bucket(**{'options': {'password': self.password}}) @classmethod def unwanted_keys(cls): diff --git a/couchbase/tests/cases/cluster_t.py b/couchbase/tests/cases/cluster_t.py index b8fe292aa..ccde2565d 100644 --- a/couchbase/tests/cases/cluster_t.py +++ b/couchbase/tests/cases/cluster_t.py @@ -17,10 +17,15 @@ from unittest import SkipTest +from couchbase.exceptions import CouchbaseFatalError, CouchbaseInputError, CouchbaseNetworkError from couchbase.tests.base import CouchbaseTestCase from couchbase.connstr import ConnectionString from couchbase.cluster import Cluster, ClassicAuthenticator,PasswordAuthenticator, NoBucketError, MixedAuthError, CertAuthenticator import gc +import os +import warnings + +CERT_PATH = os.getenv("PYCBC_CERT_PATH") class ClusterTest(CouchbaseTestCase): @@ -170,3 +175,42 @@ def test_can_authenticate_with_username_password(self): bucket = cluster.open_bucket(bucket_name) self.assertIsNotNone(bucket) + + def _test_allow_cert_path_with_SSL_mock_errors(self, func, *args, **kwargs): + try: + func(*args,**kwargs) + except Exception as e: + if self.is_realserver and CERT_PATH: + raise + try: + raise e + except CouchbaseNetworkError as f: + self.assertRegex(str(e),r'.*(refused the connection).*') + except CouchbaseFatalError as f: + self.assertRegex(str(e),r'.*(SSL subsystem).*') + except CouchbaseInputError as f: + self.assertRegex(str(e),r'.*(not supported).*') + warnings.warn("Got exception {} but acceptable error for Mock with SSL+cert_path tests".format(str(e))) + + def test_can_authenticate_with_cert_path_and_username_password_via_PasswordAuthenticator(self): + cluster = Cluster( + 'couchbases://{host}?certpath={certpath}'.format(host=self.cluster_info.host, certpath=CERT_PATH)) + authenticator = PasswordAuthenticator(self.cluster_info.admin_username, self.cluster_info.admin_password) + cluster.authenticate(authenticator) + self._test_allow_cert_path_with_SSL_mock_errors(cluster.open_bucket, self.cluster_info.bucket_name) + + def test_can_authenticate_with_cert_path_and_username_password_via_ClassicAuthenticator(self): + cluster = Cluster( + 'couchbases://{host}?certpath={certpath}'.format(host=self.cluster_info.host, certpath=CERT_PATH)) + authenticator = ClassicAuthenticator(buckets={self.cluster_info.bucket_name: self.cluster_info.bucket_password}, + cluster_username=self.cluster_info.admin_username, + cluster_password=self.cluster_info.admin_password) + cluster.authenticate(authenticator) + self._test_allow_cert_path_with_SSL_mock_errors(cluster.open_bucket, self.cluster_info.bucket_name) + + def test_can_authenticate_with_cert_path_and_username_password_via_kwargs(self): + cluster = Cluster( + 'couchbases://{host}?certpath={certpath}'.format(host=self.cluster_info.host, certpath=CERT_PATH)) + self._test_allow_cert_path_with_SSL_mock_errors(cluster.open_bucket, self.cluster_info.bucket_name, + username=self.cluster_info.admin_username, + password=self.cluster_info.admin_password) diff --git a/dev_requirements.txt b/dev_requirements.txt index 8000cc478..be6eb9414 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -16,3 +16,4 @@ utilspie==0.1.0 python-vagrant>=0.5.15 beautifulsoup4==4.7.1 jira==2.0.0 +flaky>=3.6.1 \ No newline at end of file From fad98183a9891ec57a8fa16b383be36c3de6bae9 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Wed, 19 Feb 2020 13:06:35 +0000 Subject: [PATCH 19/24] PYCBC-812: Update libcouchbase minimum version to 2.10.0 Change-Id: Ia212a9e03a05cd2fea22d978ac579acf6df04ad9 Reviewed-on: http://review.couchbase.org/122506 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: Charles Dixon --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c1d1eb567..f67a52098 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ version by issuing the following incantation ~~~~~~~~~~~~~ Prerequisites ~~~~~~~~~~~~~ -.. |libcouchbase_version| replace:: 2.9.0 +.. |libcouchbase_version| replace:: 2.10.0 - Couchbase Server (http://couchbase.com/download) - libcouchbase_. version |libcouchbase_version| or greater (Bundled in Windows installer) From 9e148e5b8886a060c073093727c2d4e4babbd7b3 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Mon, 4 May 2020 10:57:00 +0100 Subject: [PATCH 20/24] PYCBC-914: Support older Python 2.7/PIP installations Some very old Python distributions (such as those in Centos 7 base) come with pip/setuptools that can't properly handle Environment Markers. This is a workaround for this. Change-Id: I3a6b442ff18b4e2ada61b520a83e3a817f6e8101 Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/127204 Reviewed-by: David Kelly Tested-by: Build Bot --- couchbase/tests/cases/touch_t.py | 4 ++++ dev_requirements.txt | 3 ++- setup.py | 8 +++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/couchbase/tests/cases/touch_t.py b/couchbase/tests/cases/touch_t.py index a69eb8cf2..37e9ffa97 100644 --- a/couchbase/tests/cases/touch_t.py +++ b/couchbase/tests/cases/touch_t.py @@ -16,6 +16,7 @@ # import time +from flaky import flaky from nose.plugins.attrib import attr from couchbase.tests.base import ConnectionTestCase @@ -28,6 +29,7 @@ def setUp(self): super(TouchTest, self).setUp() self.cb = self.make_connection() + @flaky(10, 1) def test_trivial_touch(self): key = self.gen_key("trivial_touch") self.cb.upsert(key, "value", ttl=1) @@ -44,6 +46,7 @@ def test_trivial_touch(self): self.assertFalse(rv.success) self.assertTrue(E.NotFoundError._can_derive(rv.rc)) + @flaky(10, 1) def test_trivial_multi_touch(self): kv = self.gen_kv_dict(prefix="trivial_multi_touch") self.cb.upsert_multi(kv, ttl=1) @@ -61,6 +64,7 @@ def test_trivial_multi_touch(self): rvs = self.cb.get_multi(kv.keys(), quiet=True) self.assertFalse(rvs.all_ok) + @flaky(10, 1) def test_dict_touch_multi(self): k_missing = self.gen_key("dict_touch_multi_missing") k_existing = self.gen_key("dict_touch_multi_existing") diff --git a/dev_requirements.txt b/dev_requirements.txt index be6eb9414..58e718376 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -16,4 +16,5 @@ utilspie==0.1.0 python-vagrant>=0.5.15 beautifulsoup4==4.7.1 jira==2.0.0 -flaky>=3.6.1 \ No newline at end of file +flaky>=3.6.1 +coverage>=4.5.4,<5.0.0 diff --git a/setup.py b/setup.py index 61acf8846..fe00ebd00 100644 --- a/setup.py +++ b/setup.py @@ -189,9 +189,11 @@ def comp_option_pattern(prefix): conan_and_cmake_deps = (['conan', 'cmake>=3.0.2'] if cmake_build and sys.platform.startswith('darwin') else []) - -gen_reqs = ['typing; python_version<"3.7"', - 'enum34; python_version<"3.5"'] +gen_reqs = [] +if sys.version_info < (3, 7): + gen_reqs += ['typing'] +if sys.version_info < (3, 5): + gen_reqs += ['enum34'] setup( name = 'couchbase', From e4106db9f6ca60db7e6993475cd668c0eab98292 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 29 Apr 2020 13:07:03 -0600 Subject: [PATCH 21/24] PYCBC-714 - Intermittent SystemError Motivation ========== We have a customer seeing this on 2.x, and we see it ourselves from time to time in 3.x. Oddly, I couldn't reproduce this at one point in 3.x, but certainly now we can. Let's fix it. Modification ============ It appears that if we modify the exception object (returned by calls to PyErr_Fetch, set by PyErr_Restore) we occasionally trigger this. I assumed it was just a ref count issue, but bumping the ref count didn't help. Maybe undocumented behavior? In any case, once we put it back in via PyErr_Restore, no need to change anything, so I moved that code up a bit, problem solved. Results ======= Cannot reproduce this with script in PYCBC-714. Change-Id: Iec4ea42201220441ccf98eb1a647c6f2a725f11f Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/126977 Tested-by: Build Bot Reviewed-by: Ellis Breen (cherry picked from commit 6b6cc02465a426d64dc356ac1b9695ae52ed4e42) Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/126953 --- src/multiresult.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/multiresult.c b/src/multiresult.c index a0c0d068c..5da48df64 100644 --- a/src/multiresult.c +++ b/src/multiresult.c @@ -320,6 +320,10 @@ int pycbc_multiresult_maybe_raise2(pycbc_MultiResult *self, PyObject_SetAttrString(value, "result", (PyObject*)res); } + if (PyObject_IsInstance(value, pycbc_helpers.default_exception)) { + PyObject_SetAttrString(value, "all_results", (PyObject*)self); + Py_XDECREF(self->exceptions); + } PyErr_Restore(type, value, traceback); /** @@ -327,11 +331,6 @@ int pycbc_multiresult_maybe_raise2(pycbc_MultiResult *self, * a reference to ourselves. If we don't free the original exception, * then we'll be stuck with a circular reference */ - - if (PyObject_IsInstance(value, pycbc_helpers.default_exception)) { - PyObject_SetAttrString(value, "all_results", (PyObject*)self); - Py_XDECREF(self->exceptions); - } Py_XDECREF(self->errop); self->exceptions = NULL; self->errop = NULL; From d08d2ca1ae8dbcb1a667ce922b579eac227402d9 Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Mon, 4 May 2020 20:50:36 +0100 Subject: [PATCH 22/24] PYCBC-917: Fallback for detecting older pip versions Change-Id: I75ae2e8fbe644e9135f88f7e7600f5ceb2c96c77 Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/127247 Reviewed-by: David Kelly Tested-by: Build Bot --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index fe00ebd00..f4c563980 100644 --- a/setup.py +++ b/setup.py @@ -181,10 +181,13 @@ def comp_option_pattern(prefix): cmake_build=os.environ.get("PYCBC_CMAKE_BUILD") # Dummy dependency to prevent installation of Python < 3 package on Windows. - +try: + pipversion = pip.__version__ +except: + pipversion = "0.0.0" pip_not_on_win_python_lt_3 = ( ['pip>=9.0; (sys_platform != "win32" and python_version >= "2.7") or (python_version >= "3.0")'] - if pip.__version__ >= "9.0.0" + if pipversion >= "9.0.0" else []) conan_and_cmake_deps = (['conan', 'cmake>=3.0.2'] if From 2126242121ba9e6f2985f046359b58246796aaef Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 29 Apr 2020 15:03:21 -0600 Subject: [PATCH 23/24] PYCBC-714 Intermittent System Error Motivation ========== Already fixed, this just adjusts the position of the comment and some ancillary code to make more sense Modification ============ Moved comment plus housekeeping code to make more sense. Result ====== Nothing new, still cannot repro the issue. Change-Id: Ib9231b57442f1aac8fbd9137a31bff266b1c5338 Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/126987 Tested-by: Build Bot Reviewed-by: Ellis Breen (cherry picked from commit 33f4edeea78cffd4c68f27663614a405b5068990) Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/127369 --- src/multiresult.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/multiresult.c b/src/multiresult.c index 5da48df64..4fce74609 100644 --- a/src/multiresult.c +++ b/src/multiresult.c @@ -320,21 +320,21 @@ int pycbc_multiresult_maybe_raise2(pycbc_MultiResult *self, PyObject_SetAttrString(value, "result", (PyObject*)res); } - if (PyObject_IsInstance(value, pycbc_helpers.default_exception)) { - PyObject_SetAttrString(value, "all_results", (PyObject*)self); - Py_XDECREF(self->exceptions); - } - PyErr_Restore(type, value, traceback); - /** * This is needed since the exception object will later contain * a reference to ourselves. If we don't free the original exception, * then we'll be stuck with a circular reference */ + if (PyObject_IsInstance(value, pycbc_helpers.default_exception)) { + PyObject_SetAttrString(value, "all_results", (PyObject*)self); + Py_XDECREF(self->exceptions); + } Py_XDECREF(self->errop); self->exceptions = NULL; self->errop = NULL; + PyErr_Restore(type, value, traceback); + return 1; } From 7e004ddd13038ddd6e99a87d332e1be3c3772adc Mon Sep 17 00:00:00 2001 From: Ellis Breen Date: Fri, 1 May 2020 15:04:20 +0100 Subject: [PATCH 24/24] PYCBC-844: Fix release notes generator Change-Id: Ia4cda61711f9997e0ec1400df41a2f7a860bdb69 Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/127097 Tested-by: Build Bot Tested-by: Ellis Breen Reviewed-by: David Kelly Reviewed-on: http://review.couchbase.org/c/couchbase-python-client/+/127243 --- docs/source/relnotes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/relnotes.py b/docs/source/relnotes.py index 2524742f0..dbf1c86d9 100644 --- a/docs/source/relnotes.py +++ b/docs/source/relnotes.py @@ -34,12 +34,15 @@ section_type = None result = defaultdict(lambda: []) mapping = {"Task": "Enhancements", - "Bugs": "Fixes"} + "Improvement": "Enhancements", + "New Feature": "Enhancements", + "Bug": "Fixes"} version = re.match(r'^(.*)Version ([0-9]+\.[0-9]+\.[0-9]+).*$', content.title.text).group(2) print("got version {}".format(version)) for entry in content.body.find_all(): if re.match(r'h[0-9]+', entry.name): - section_type = mapping.get(entry.text.strip().replace('Improvement', 'Task'), "Enhancements") + print("Got section :{}".format(entry.text)) + section_type = mapping.get(entry.text.strip(), None) if re.match("Edit/Copy Release Notes", entry.text): break else: