diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 07b7645f..568e881f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,4 +15,4 @@ jobs: id-token: write steps: - id: deployment - uses: sphinx-notes/pages@v3 \ No newline at end of file + uses: sphinx-notes/pages@v3 diff --git a/docs/examples.rst b/docs/examples.rst index 4f8dee84..8d7cbf3c 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,155 +1,8 @@ Examples =============== -Basic example -^^^^^^^^^^^^^ - -All examples in this section are parts of `basic example `_. - -For deeper upderstanding it is better to read the whole example. - -Create table ------------- - -.. code-block:: python - - def create_tables(pool: ydb.QuerySessionPool): - print("\nCreating table series...") - pool.execute_with_retries( - """ - CREATE table `series` ( - `series_id` Int64, - `title` Utf8, - `series_info` Utf8, - `release_date` Date, - PRIMARY KEY (`series_id`) - ) - """ - ) - - print("\nCreating table seasons...") - pool.execute_with_retries( - """ - CREATE table `seasons` ( - `series_id` Int64, - `season_id` Int64, - `title` Utf8, - `first_aired` Date, - `last_aired` Date, - PRIMARY KEY (`series_id`, `season_id`) - ) - """ - ) - - print("\nCreating table episodes...") - pool.execute_with_retries( - """ - CREATE table `episodes` ( - `series_id` Int64, - `season_id` Int64, - `episode_id` Int64, - `title` Utf8, - `air_date` Date, - PRIMARY KEY (`series_id`, `season_id`, `episode_id`) - ) - """ - ) - - -Upsert Simple -------------- - -.. code-block:: python - - def upsert_simple(pool: ydb.QuerySessionPool): - print("\nPerforming UPSERT into episodes...") - - pool.execute_with_retries( - """ - UPSERT INTO episodes (series_id, season_id, episode_id, title) VALUES (2, 6, 1, "TBD"); - """ - ) - - -Simple Select ----------- - -.. code-block:: python - - def select_simple(pool: ydb.QuerySessionPool): - print("\nCheck series table...") - result_sets = pool.execute_with_retries( - """ - SELECT - series_id, - title, - release_date - FROM series - WHERE series_id = 1; - """, - ) - first_set = result_sets[0] - for row in first_set.rows: - print( - "series, id: ", - row.series_id, - ", title: ", - row.title, - ", release date: ", - row.release_date, - ) - - return first_set - -Select With Parameters ----------------------- - -.. code-block:: python - - def select_with_parameters(pool: ydb.QuerySessionPool, series_id, season_id, episode_id): - result_sets = pool.execute_with_retries( - """ - DECLARE $seriesId AS Int64; - DECLARE $seasonId AS Int64; - DECLARE $episodeId AS Int64; - - SELECT - title, - air_date - FROM episodes - WHERE series_id = $seriesId AND season_id = $seasonId AND episode_id = $episodeId; - """, - { - "$seriesId": series_id, # could be defined implicit - "$seasonId": (season_id, ydb.PrimitiveType.Int64), # could be defined via tuple - "$episodeId": ydb.TypedValue(episode_id, ydb.PrimitiveType.Int64), # could be defined via special class - }, - ) - - print("\n> select_with_parameters:") - first_set = result_sets[0] - for row in first_set.rows: - print("episode title:", row.title, ", air date:", row.air_date) - - return first_set - -Huge Select ------------ - -.. code-block:: python - - def huge_select(pool: ydb.QuerySessionPool): - def callee(session: ydb.QuerySessionSync): - query = """SELECT * from episodes;""" - - with session.transaction().execute( - query, - commit_tx=True, - ) as result_sets: - print("\n> Huge SELECT call") - for result_set in result_sets: - for row in result_set.rows: - print("episode title:", row.title, ", air date:", row.air_date) - - return pool.retry_operation_sync(callee) +.. toctree:: + :maxdepth: 3 + examples/basic_example + examples/authentication \ No newline at end of file diff --git a/docs/examples/authentication.rst b/docs/examples/authentication.rst new file mode 100644 index 00000000..9f489172 --- /dev/null +++ b/docs/examples/authentication.rst @@ -0,0 +1,104 @@ +Authentication +============== + +There are several ways to authenticate through YDB Python SDK. + +Anonymous Credentials +--------------------- + +Full executable example `here `_. + +.. code-block:: python + + driver = ydb.Driver( + endpoint=os.getenv("YDB_ENDPOINT"), + database=os.getenv("YDB_DATABASE"), + credentials=ydb.AnonymousCredentials(), + ) + + +Access Token Credentials +------------------------ + +Full executable example `here `_. + +.. code-block:: python + + driver = ydb.Driver( + endpoint=os.getenv("YDB_ENDPOINT"), + database=os.getenv("YDB_DATABASE"), + credentials=ydb.AccessTokenCredentials(os.getenv("YDB_ACCESS_TOKEN_CREDENTIALS")), + ) + + +Static Credentials +--------------------------- + +Full executable example `here `_. + + +.. code-block:: python + + driver_config = ydb.DriverConfig( + endpoint=endpoint, + database=database, + credentials=ydb.StaticCredentials.from_user_password(user, password), + ) + + driver = ydb.Driver(driver_config=driver_config) + + +Service Account Credentials +---------------------------- + +Full executable example `here `_. + +.. code-block:: python + + driver = ydb.Driver( + endpoint=os.getenv("YDB_ENDPOINT"), + database=os.getenv("YDB_DATABASE"), + credentials=ydb.iam.ServiceAccountCredentials.from_file( + os.getenv("SA_KEY_FILE"), + ), + ) + + +OAuth 2.0 Token Exchange Credentials +------------------------------------ + +Full executable example `here `_. + +.. code-block:: python + + driver = ydb.Driver( + endpoint=args.endpoint, + database=args.database, + root_certificates=ydb.load_ydb_root_certificate(), + credentials=ydb.oauth2_token_exchange.Oauth2TokenExchangeCredentials( + token_endpoint=args.token_endpoint, + audience=args.audience, + subject_token_source=ydb.oauth2_token_exchange.JwtTokenSource( + signing_method="RS256", + private_key_file=args.private_key_file, + key_id=args.key_id, + issuer=args.issuer, + subject=args.subject, + audience=args.audience, + ), + ), + ) + + +Metadata Credentials +-------------------- + +Full executable example `here `_. + +.. code-block:: python + + driver = ydb.Driver( + endpoint=os.getenv("YDB_ENDPOINT"), + database=os.getenv("YDB_DATABASE"), + credentials=ydb.iam.MetadataUrlCredentials(), + ) diff --git a/docs/examples/basic_example.rst b/docs/examples/basic_example.rst new file mode 100644 index 00000000..e7f38737 --- /dev/null +++ b/docs/examples/basic_example.rst @@ -0,0 +1,152 @@ +Basic example +============= + +All examples in this section are parts of `basic example `_. + +For deeper upderstanding it is better to read the whole example. + +Create table +------------ + +.. code-block:: python + + def create_tables(pool: ydb.QuerySessionPool): + print("\nCreating table series...") + pool.execute_with_retries( + """ + CREATE table `series` ( + `series_id` Int64, + `title` Utf8, + `series_info` Utf8, + `release_date` Date, + PRIMARY KEY (`series_id`) + ) + """ + ) + + print("\nCreating table seasons...") + pool.execute_with_retries( + """ + CREATE table `seasons` ( + `series_id` Int64, + `season_id` Int64, + `title` Utf8, + `first_aired` Date, + `last_aired` Date, + PRIMARY KEY (`series_id`, `season_id`) + ) + """ + ) + + print("\nCreating table episodes...") + pool.execute_with_retries( + """ + CREATE table `episodes` ( + `series_id` Int64, + `season_id` Int64, + `episode_id` Int64, + `title` Utf8, + `air_date` Date, + PRIMARY KEY (`series_id`, `season_id`, `episode_id`) + ) + """ + ) + + +Upsert Simple +------------- + +.. code-block:: python + + def upsert_simple(pool: ydb.QuerySessionPool): + print("\nPerforming UPSERT into episodes...") + + pool.execute_with_retries( + """ + UPSERT INTO episodes (series_id, season_id, episode_id, title) VALUES (2, 6, 1, "TBD"); + """ + ) + + +Simple Select +---------- + +.. code-block:: python + + def select_simple(pool: ydb.QuerySessionPool): + print("\nCheck series table...") + result_sets = pool.execute_with_retries( + """ + SELECT + series_id, + title, + release_date + FROM series + WHERE series_id = 1; + """, + ) + first_set = result_sets[0] + for row in first_set.rows: + print( + "series, id: ", + row.series_id, + ", title: ", + row.title, + ", release date: ", + row.release_date, + ) + + return first_set + +Select With Parameters +---------------------- + +.. code-block:: python + + def select_with_parameters(pool: ydb.QuerySessionPool, series_id, season_id, episode_id): + result_sets = pool.execute_with_retries( + """ + DECLARE $seriesId AS Int64; + DECLARE $seasonId AS Int64; + DECLARE $episodeId AS Int64; + + SELECT + title, + air_date + FROM episodes + WHERE series_id = $seriesId AND season_id = $seasonId AND episode_id = $episodeId; + """, + { + "$seriesId": series_id, # could be defined implicit + "$seasonId": (season_id, ydb.PrimitiveType.Int64), # could be defined via tuple + "$episodeId": ydb.TypedValue(episode_id, ydb.PrimitiveType.Int64), # could be defined via special class + }, + ) + + print("\n> select_with_parameters:") + first_set = result_sets[0] + for row in first_set.rows: + print("episode title:", row.title, ", air date:", row.air_date) + + return first_set + +Huge Select +----------- + +.. code-block:: python + + def huge_select(pool: ydb.QuerySessionPool): + def callee(session: ydb.QuerySessionSync): + query = """SELECT * from episodes;""" + + with session.transaction().execute( + query, + commit_tx=True, + ) as result_sets: + print("\n> Huge SELECT call") + for result_set in result_sets: + for row in result_set.rows: + print("episode title:", row.title, ", air date:", row.air_date) + + return pool.retry_operation_sync(callee) + diff --git a/examples/static-credentials/__main__.py b/examples/static-credentials/__main__.py new file mode 100644 index 00000000..900396ed --- /dev/null +++ b/examples/static-credentials/__main__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +import argparse +from example import run + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description="""\033[92mStatic Credentials example.\x1b[0m\n""", + ) + parser.add_argument("-e", "--endpoint", help="Endpoint url to use", default="grpc://localhost:2136") + parser.add_argument("-d", "--database", help="Name of the database to use", default="/local") + parser.add_argument("-u", "--user", help="User to auth with", default="root") + parser.add_argument("-p", "--password", help="Password from user to auth with", default="1234") + + args = parser.parse_args() + + run( + endpoint=args.endpoint, + database=args.database, + user=args.user, + password=args.password, + ) diff --git a/examples/static-credentials/example.py b/examples/static-credentials/example.py new file mode 100644 index 00000000..71409f5c --- /dev/null +++ b/examples/static-credentials/example.py @@ -0,0 +1,23 @@ +import ydb + + +def test_driver_works(driver: ydb.Driver): + driver.wait(5) + pool = ydb.QuerySessionPool(driver) + result = pool.execute_with_retries("SELECT 1 as cnt") + assert result[0].rows[0].cnt == 1 + + +def auth_with_static_credentials(endpoint: str, database: str, user: str, password: str): + driver_config = ydb.DriverConfig( + endpoint=endpoint, + database=database, + credentials=ydb.StaticCredentials.from_user_password(user, password), + ) + + with ydb.Driver(driver_config=driver_config) as driver: + test_driver_works(driver) + + +def run(endpoint: str, database: str, user: str, password: str): + auth_with_static_credentials(endpoint, database, user, password) diff --git a/tests/auth/test_static_credentials.py b/tests/auth/test_static_credentials.py new file mode 100644 index 00000000..a9239f2a --- /dev/null +++ b/tests/auth/test_static_credentials.py @@ -0,0 +1,47 @@ +import pytest +import ydb + + +USERNAME = "root" +PASSWORD = "1234" + + +def check_driver_works(driver): + driver.wait(timeout=15) + pool = ydb.QuerySessionPool(driver) + result = pool.execute_with_retries("SELECT 1 as cnt") + assert result[0].rows[0].cnt == 1 + + +def test_static_credentials_default(endpoint, database): + driver_config = ydb.DriverConfig( + endpoint, + database, + ) + credentials = ydb.StaticCredentials(driver_config, USERNAME, PASSWORD) + + with ydb.Driver(driver_config=driver_config, credentials=credentials) as driver: + check_driver_works(driver) + + +def test_static_credentials_classmethod(endpoint, database): + driver_config = ydb.DriverConfig( + endpoint, + database, + credentials=ydb.StaticCredentials.from_user_password(USERNAME, PASSWORD), + ) + + with ydb.Driver(driver_config=driver_config) as driver: + check_driver_works(driver) + + +def test_static_credentials_wrong_creds(endpoint, database): + driver_config = ydb.DriverConfig( + endpoint, + database, + credentials=ydb.StaticCredentials.from_user_password(USERNAME, PASSWORD * 2), + ) + + with pytest.raises(ydb.ConnectionFailure): + with ydb.Driver(driver_config=driver_config) as driver: + driver.wait(5, fail_fast=True) diff --git a/ydb/credentials.py b/ydb/credentials.py index ab502798..ab721d0b 100644 --- a/ydb/credentials.py +++ b/ydb/credentials.py @@ -45,6 +45,9 @@ def get_auth_token(self) -> str: return token return "" + def _update_driver_config(self, driver_config): + pass + class OneToManyValue(object): def __init__(self): @@ -185,11 +188,23 @@ def _wrap_static_credentials_response(rpc_state, response): class StaticCredentials(AbstractExpiringTokenCredentials): def __init__(self, driver_config, user, password="", tracer=None): super(StaticCredentials, self).__init__(tracer) - self.driver_config = driver_config + + from .driver import DriverConfig + + if driver_config is not None: + self.driver_config = DriverConfig( + endpoint=driver_config.endpoint, + database=driver_config.database, + root_certificates=driver_config.root_certificates, + ) self.user = user self.password = password self.request_timeout = 10 + @classmethod + def from_user_password(cls, user: str, password: str, tracer=None): + return cls(None, user, password, tracer) + def _make_token_request(self): conn = connection.Connection.ready_factory(self.driver_config.endpoint, self.driver_config) assert conn is not None, "Failed to establish connection in to %s" % self.driver_config.endpoint @@ -205,6 +220,15 @@ def _make_token_request(self): conn.close() return {"expires_in": 30 * 60, "access_token": result.token} + def _update_driver_config(self, driver_config): + from .driver import DriverConfig + + self.driver_config = DriverConfig( + endpoint=driver_config.endpoint, + database=driver_config.database, + root_certificates=driver_config.root_certificates, + ) + class AnonymousCredentials(Credentials): @staticmethod diff --git a/ydb/driver.py b/ydb/driver.py index ecd3319e..1559b0d0 100644 --- a/ydb/driver.py +++ b/ydb/driver.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- +import grpc +import logging +import os +from typing import Any # noqa + from . import credentials as credentials_impl, table, scheme, pool from . import tracing -import os -import grpc from . import iam from . import _utilities -from typing import Any # noqa + +logger = logging.getLogger(__name__) class RPCCompression: @@ -172,7 +176,7 @@ def default_from_endpoint_and_database(cls, endpoint, database, root_certificate database, credentials=default_credentials(credentials), root_certificates=root_certificates, - **kwargs + **kwargs, ) @classmethod @@ -183,13 +187,22 @@ def default_from_connection_string(cls, connection_string, root_certificates=Non database, credentials=default_credentials(credentials), root_certificates=root_certificates, - **kwargs + **kwargs, ) def set_grpc_keep_alive_timeout(self, timeout): self.grpc_keep_alive_timeout = timeout return self + def _update_attrs_by_kwargs(self, **kwargs): + for key, value in kwargs.items(): + if value is not None: + if getattr(self, key) is not None: + logger.warning( + f"Arg {key} was used in both DriverConfig and Driver. Value from Driver will be used." + ) + setattr(self, key, value) + ConnectionParams = DriverConfig @@ -202,7 +215,7 @@ def get_config( root_certificates=None, credentials=None, config_class=DriverConfig, - **kwargs + **kwargs, ): if driver_config is None: if connection_string is not None: @@ -213,7 +226,17 @@ def get_config( driver_config = config_class.default_from_endpoint_and_database( endpoint, database, root_certificates, credentials, **kwargs ) - return driver_config + else: + kwargs["endpoint"] = endpoint + kwargs["database"] = database + kwargs["root_certificates"] = root_certificates + kwargs["credentials"] = credentials + + driver_config._update_attrs_by_kwargs(**kwargs) + + if driver_config.credentials is not None: + driver_config.credentials._update_driver_config(driver_config) + return driver_config @@ -228,7 +251,7 @@ def __init__( database=None, root_certificates=None, credentials=None, - **kwargs + **kwargs, ): """ Constructs a driver instance to be used in table and scheme clients.