From 4455889174229ee2fad91b005489522c252f7df9 Mon Sep 17 00:00:00 2001 From: Christophe Bornet Date: Tue, 1 Oct 2024 14:59:18 +0200 Subject: [PATCH] Add method to get session and keyspace without setting them globally (#168) * Add method to get session and keyspace without setting them globally * add 0.1.9 to changesfile --------- Co-authored-by: Stefano Lottini --- CHANGES.txt | 9 + src/cassio/config/__init__.py | 236 +++++++++++++++------------ tests/integration/test_init_astra.py | 7 +- 3 files changed, 142 insertions(+), 110 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4ee729c..e34547a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,12 @@ +v 0.1.10 +======== +Add method to get session and keyspace from parameters. +Calling `cassio.init()` with insufficient arguments now raises an exception. + +v 0.1.9 +======= +introduce default proto version 4 for Astra (w/ fallback); alias ASTRA_DB_ID (#164) + v 0.1.8 ======= Add support for arbitrary CQL type in the 'body_blob' column (body_type init parameter) diff --git a/src/cassio/config/__init__.py b/src/cassio/config/__init__.py index 86bb2da..9875743 100644 --- a/src/cassio/config/__init__.py +++ b/src/cassio/config/__init__.py @@ -23,7 +23,7 @@ default_keyspace: Optional[str] = None -def init( +def get_session_and_keyspace( auto: bool = False, session: Optional[Session] = None, secure_connect_bundle: Optional[str] = None, @@ -38,88 +38,10 @@ def init( tempfile_basedir: Optional[str] = None, bundle_url_template: Optional[str] = None, cloud_kwargs: Optional[Dict[str, Any]] = None, -) -> None: - """ - Globally set the default Cassandra connection (/keyspace) for CassIO. - This default will be used by all other db-requiring CassIO instantiations, - unless passed to the respective classes' __init__. - - There are various ways to achieve this, depending on the passed parameters. - Broadly speaking, this method allows to pass one's own ready Session, - or to have it created in the method. For this second case, both Astra DB - and a regular Cassandra cluster can be targeted. - One can also simply call cassio.init(auto=True) and let it figure out - what to do based on standard environment variables. - - CASSANDRA - If one passes `contact_points`, it is assumed that this is Cassandra. - In that case, only the following arguments will be used: - `contact_points`, `keyspace`, `username`, `password`, `cluster_kwargs` - Note that when passing a `session` all other parameters are ignored. - - ASTRA DB: - If `contact_points` is not passed, one of several methods to connect to - Astra should be supplied for the connection to Astra. These overlap: - see below for their precedence. - Note that when passing a `session` all other parameters are ignored. - - AUTO: - If passing auto=True, no other parameter is allowed except for - `tempfile_basedir`. The rest, including choosing Astra DB / Cassandra, - is autofilled by inspecting the following environment variables: - CASSANDRA_CONTACT_POINTS - CASSANDRA_USERNAME - CASSANDRA_PASSWORD - CASSANDRA_KEYSPACE - ASTRA_DB_APPLICATION_TOKEN - ASTRA_DB_INIT_STRING - ASTRA_DB_SECURE_BUNDLE_PATH - ASTRA_DB_KEYSPACE - ASTRA_DB_DATABASE_ID / ASTRA_DB_ID (equivalently) - - PARAMETERS: - `auto`: (bool = False), whether to auto-guess all connection params. - `session` (optional cassandra.cluster.Session), an established connection. - `secure_connect_bundle` (optional str), full path to a Secure Bundle. - `init_string` (optional str), a stand-alone "db init string" credential - (which can optionally contain keyspace and/or token). - `token` (optional str), the Astra DB "AstraCS:..." token. - `database_id` (optional str), the Astra DB ID. Used only for Astra - connections with no `secure_connect_bundle` parameter passed. - `keyspace` (optional str), the keyspace to work. - `contact_points` (optional List[str]), for Cassandra connection - `username` (optional str), username for Cassandra connection - `password` (optional str), password for Cassandra connection - `cluster_kwargs` (optional dict), additional arguments to `Cluster(...)`. - `tempfile_basedir` (optional str), where to create temporary work directories. - `bundle_url_template` (optional str), url template for getting the database - secure bundle. The "databaseId" variable is resolved with the actual value. - Default (for Astra DB): - "https://api.astra.datastax.com/v2/databases/{database_id}/secureBundleURL" - `cloud_kwargs` (optional dict), additional arguments to `Cluster(cloud={...})` - (i.e. additional key-value pairs within the passed `cloud` dict). - - ASTRA DB: - The Astra-related parameters are arranged in a chain of fallbacks. - In case redundant information is supplied, these are the precedences: - session > secure_connect_bundle > init_string - token > (from init_string if any) - keyspace > (from init_string if any) > (from bundle if any) - If a secure-connect-bundle is needed and not passed, it will be downloaded: - this requires `database_id` to be passed, suitable token permissions. - Constraints and caveats: - `secure_connect_bundle` requires `token`. - `session` does not make use of `cluster_kwargs` and will ignore it. - - The Session is created at `init` time and kept around, available to any - subsequent table creation. If calling `init` a second time, a new Session - will be made available replacing the previous one. - """ - global default_session - global default_keyspace +) -> tuple[Session, Optional[str]]: + result_session: Optional[Session] temp_dir_created: bool = False temp_dir: Optional[str] = None - direct_session: Optional[Session] = None bundle_from_is: Optional[str] = None bundle_from_arg: Optional[str] = None bundle_from_download: Optional[str] = None @@ -185,9 +107,6 @@ def init( ) keyspace_from_is = options_from_is.get("keyspace") token_from_is = options_from_is.get("token") - # for the session - if session: - direct_session = session if secure_connect_bundle: if not token: raise ValueError( @@ -197,16 +116,9 @@ def init( bundle_from_arg = secure_connect_bundle token_from_arg = token keyspace_from_arg = keyspace - can_be_astra = any( - [ - secure_connect_bundle is not None, - init_string is not None, - token is not None, - ] - ) # resolution of priority among args - if direct_session: - default_session = direct_session + if session: + result_session = session else: # first determine if Cassandra or Astra is_cassandra = all( @@ -217,12 +129,13 @@ def init( contact_points is not None, ] ) - if is_cassandra: - is_astra_db = False - else: - # determine if Astra DB - is_astra_db = can_be_astra - # + can_be_astra = any( + [ + secure_connect_bundle is not None, + init_string is not None, + token is not None, + ] + ) if is_cassandra: # need to take care of: # contact_points, username, password, cluster_kwargs @@ -263,8 +176,8 @@ def init( auth_provider=chosen_auth_provider, **(cluster_kwargs if cluster_kwargs is not None else {}), ) - default_session = cluster.connect() - elif is_astra_db: + result_session = cluster.connect() + elif can_be_astra: # Astra DB chosen_token = _first_valid(token_from_arg, token_from_is) if chosen_token is None: @@ -323,7 +236,7 @@ def init( } ), ) - default_session = cluster.connect() + result_session = cluster.connect() except NoHostAvailable: # this is how a mismatched explicitly-set protocol version # manifests itself. If the V4 guess fails, retry w/out version @@ -338,19 +251,132 @@ def init( ), **(cluster_kwargs if cluster_kwargs is not None else {}), ) - default_session = cluster.connect() + result_session = cluster.connect() else: raise ValueError("No secure-connect-bundle was available.") + else: + raise ValueError("Insufficient parameters to connect") # keyspace to be resolved in any case chosen_keyspace = _first_valid( keyspace_from_arg, keyspace_from_is, keyspace_from_bundle ) - default_keyspace = chosen_keyspace + return result_session, chosen_keyspace finally: if temp_dir_created and temp_dir is not None: shutil.rmtree(temp_dir) +def init( + auto: bool = False, + session: Optional[Session] = None, + secure_connect_bundle: Optional[str] = None, + init_string: Optional[str] = None, + token: Optional[str] = None, + database_id: Optional[str] = None, + keyspace: Optional[str] = None, + contact_points: Optional[Union[str, List[str]]] = None, + username: Optional[str] = None, + password: Optional[str] = None, + cluster_kwargs: Optional[Dict[str, Any]] = None, + tempfile_basedir: Optional[str] = None, + bundle_url_template: Optional[str] = None, + cloud_kwargs: Optional[Dict[str, Any]] = None, +) -> None: + """ + Globally set the default Cassandra connection (/keyspace) for CassIO. + This default will be used by all other db-requiring CassIO instantiations, + unless passed to the respective classes' __init__. + + There are various ways to achieve this, depending on the passed parameters. + Broadly speaking, this method allows to pass one's own ready Session, + or to have it created in the method. For this second case, both Astra DB + and a regular Cassandra cluster can be targeted. + One can also simply call cassio.init(auto=True) and let it figure out + what to do based on standard environment variables. + + CASSANDRA + If one passes `contact_points`, it is assumed that this is Cassandra. + In that case, only the following arguments will be used: + `contact_points`, `keyspace`, `username`, `password`, `cluster_kwargs` + Note that when passing a `session` all other parameters are ignored. + + ASTRA DB: + If `contact_points` is not passed, one of several methods to connect to + Astra should be supplied for the connection to Astra. These overlap: + see below for their precedence. + Note that when passing a `session` all other parameters are ignored. + + AUTO: + If passing auto=True, no other parameter is allowed except for + `tempfile_basedir`. The rest, including choosing Astra DB / Cassandra, + is autofilled by inspecting the following environment variables: + CASSANDRA_CONTACT_POINTS + CASSANDRA_USERNAME + CASSANDRA_PASSWORD + CASSANDRA_KEYSPACE + ASTRA_DB_APPLICATION_TOKEN + ASTRA_DB_INIT_STRING + ASTRA_DB_SECURE_BUNDLE_PATH + ASTRA_DB_KEYSPACE + ASTRA_DB_DATABASE_ID / ASTRA_DB_ID (equivalently) + + PARAMETERS: + `auto`: (bool = False), whether to auto-guess all connection params. + `session` (optional cassandra.cluster.Session), an established connection. + `secure_connect_bundle` (optional str), full path to a Secure Bundle. + `init_string` (optional str), a stand-alone "db init string" credential + (which can optionally contain keyspace and/or token). + `token` (optional str), the Astra DB "AstraCS:..." token. + `database_id` (optional str), the Astra DB ID. Used only for Astra + connections with no `secure_connect_bundle` parameter passed. + `keyspace` (optional str), the keyspace to work. + `contact_points` (optional List[str]), for Cassandra connection + `username` (optional str), username for Cassandra connection + `password` (optional str), password for Cassandra connection + `cluster_kwargs` (optional dict), additional arguments to `Cluster(...)`. + `tempfile_basedir` (optional str), where to create temporary work directories. + `bundle_url_template` (optional str), url template for getting the database + secure bundle. The "databaseId" variable is resolved with the actual value. + Default (for Astra DB): + "https://api.astra.datastax.com/v2/databases/{database_id}/secureBundleURL" + `cloud_kwargs` (optional dict), additional arguments to `Cluster(cloud={...})` + (i.e. additional key-value pairs within the passed `cloud` dict). + + ASTRA DB: + The Astra-related parameters are arranged in a chain of fallbacks. + In case redundant information is supplied, these are the precedences: + session > secure_connect_bundle > init_string + token > (from init_string if any) + keyspace > (from init_string if any) > (from bundle if any) + If a secure-connect-bundle is needed and not passed, it will be downloaded: + this requires `database_id` to be passed, suitable token permissions. + Constraints and caveats: + `secure_connect_bundle` requires `token`. + `session` does not make use of `cluster_kwargs` and will ignore it. + + The Session is created at `init` time and kept around, available to any + subsequent table creation. If calling `init` a second time, a new Session + will be made available replacing the previous one. + """ + global default_session, default_keyspace + default_session, default_keyspace = get_session_and_keyspace( + auto=auto, + session=session, + secure_connect_bundle=secure_connect_bundle, + init_string=init_string, + token=token, + database_id=database_id, + keyspace=keyspace, + contact_points=contact_points, + username=username, + password=password, + cluster_kwargs=cluster_kwargs, + tempfile_basedir=tempfile_basedir, + bundle_url_template=bundle_url_template, + cloud_kwargs=cloud_kwargs, + ) + + def resolve_session(arg_session: Optional[Session] = None) -> Optional[Session]: """Utility to fall back to the global defaults when null args are passed.""" if arg_session is not None: @@ -383,7 +409,7 @@ def check_resolve_keyspace(arg_keyspace: Optional[str] = None) -> str: return s -def _first_valid(*pargs: Any) -> Union[Any, None]: +def _first_valid(*pargs: Optional[str]) -> Optional[str]: for entry in pargs: if entry is not None: return entry diff --git a/tests/integration/test_init_astra.py b/tests/integration/test_init_astra.py index ff8ca25..1b7a52e 100644 --- a/tests/integration/test_init_astra.py +++ b/tests/integration/test_init_astra.py @@ -66,11 +66,8 @@ def test_init_noop(self) -> None: assert resolve_keyspace() is None assert resolve_session("s") == "s" assert resolve_keyspace("k") == "k" - cassio.init() - assert resolve_session() is None - assert resolve_keyspace() is None - assert resolve_session("s") == "s" - assert resolve_keyspace("k") == "k" + with pytest.raises(ValueError, match="Insufficient parameters to connect"): + cassio.init() def test_init_session(self) -> None: _reset_cassio_globals()