Skip to content

Commit 09a6eeb

Browse files
authored
feat: Service account support (#68)
1 parent 71d69cb commit 09a6eeb

File tree

8 files changed

+62
-3
lines changed

8 files changed

+62
-3
lines changed

.github/workflows/nightly.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ jobs:
5656
env:
5757
USER_NAME: ${{ secrets.FIREBOLT_USERNAME }}
5858
PASSWORD: ${{ secrets.FIREBOLT_PASSWORD }}
59+
SERVICE_ID: ${{ secrets.SERVICE_ID }}
60+
SERVICE_SECRET: ${{ secrets.SERVICE_SECRET }}
5961
DATABASE_NAME: ${{ steps.setup.outputs.database_name }}
6062
ENGINE_NAME: ${{ steps.setup.outputs.engine_name }}
6163
ENGINE_URL: ${{ steps.setup.outputs.engine_url }}

.github/workflows/python-integration-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ jobs:
3333
env:
3434
USER_NAME: ${{ secrets.FIREBOLT_USERNAME }}
3535
PASSWORD: ${{ secrets.FIREBOLT_PASSWORD }}
36+
SERVICE_ID: ${{ secrets.SERVICE_ID }}
37+
SERVICE_SECRET: ${{ secrets.SERVICE_SECRET }}
3638
DATABASE_NAME: ${{ steps.setup.outputs.database_name }}
3739
ENGINE_NAME: ${{ steps.setup.outputs.engine_name }}
3840
ENGINE_URL: ${{ steps.setup.outputs.engine_url }}

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040
env:
4141
USER_NAME: ${{ secrets.FIREBOLT_USERNAME }}
4242
PASSWORD: ${{ secrets.FIREBOLT_PASSWORD }}
43+
SERVICE_ID: ${{ secrets.SERVICE_ID }}
44+
SERVICE_SECRET: ${{ secrets.SERVICE_SECRET }}
4345
DATABASE_NAME: ${{ steps.setup.outputs.database_name }}
4446
ENGINE_NAME: ${{ steps.setup.outputs.engine_name }}
4547
ENGINE_URL: ${{ steps.setup.outputs.engine_url }}

.legitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tests/unit/** # Ignore everything in the unit test directory

src/firebolt_db/firebolt_dialect.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import firebolt.db as dbapi
77
import sqlalchemy.types as sqltypes
8-
from firebolt.client.auth import Auth, UsernamePassword
8+
from firebolt.client.auth import Auth, ServiceAccount, UsernamePassword
99
from firebolt.db import Cursor
1010
from sqlalchemy.engine import Connection as AlchemyConnection
1111
from sqlalchemy.engine import ExecutionContext, default
@@ -111,9 +111,14 @@ def create_connect_args(self, url: URL) -> Tuple[List, Dict]:
111111
# parameters are all passed as a string, we need to convert
112112
# bool flag to boolean for SDK compatibility
113113
token_cache_flag = bool(strtobool(parameters.pop("use_token_cache", "True")))
114+
auth = (
115+
ServiceAccount(url.username, url.password, token_cache_flag)
116+
if "@" not in url.username
117+
else UsernamePassword(url.username, url.password, token_cache_flag)
118+
)
114119
kwargs: Dict[str, Union[str, Auth, Dict[str, Any], None]] = {
115120
"database": url.host or None,
116-
"auth": UsernamePassword(url.username, url.password, token_cache_flag),
121+
"auth": auth,
117122
"engine_name": url.database,
118123
"additional_parameters": {},
119124
}

tests/integration/conftest.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
DATABASE_NAME_ENV = "DATABASE_NAME"
1414
USERNAME_ENV = "USER_NAME"
1515
PASSWORD_ENV = "PASSWORD"
16+
SERVICE_ID = "SERVICE_ID"
17+
SERVICE_SECRET = "SERVICE_SECRET"
1618

1719

1820
def must_env(var_name: str) -> str:
@@ -41,6 +43,16 @@ def password() -> str:
4143
return must_env(PASSWORD_ENV)
4244

4345

46+
@fixture(scope="session")
47+
def service_id() -> str:
48+
return must_env(SERVICE_ID)
49+
50+
51+
@fixture(scope="session")
52+
def service_secret() -> str:
53+
return must_env(SERVICE_SECRET)
54+
55+
4456
@fixture(scope="session")
4557
def engine(
4658
username: str, password: str, database_name: str, engine_name: str
@@ -50,12 +62,27 @@ def engine(
5062
)
5163

5264

65+
@fixture(scope="session")
66+
def engine_service_account(
67+
service_id: str, service_secret: str, database_name: str, engine_name: str
68+
) -> Engine:
69+
return create_engine(
70+
f"firebolt://{service_id}:{service_secret}@{database_name}/{engine_name}"
71+
)
72+
73+
5374
@fixture(scope="session")
5475
def connection(engine: Engine) -> Connection:
5576
with engine.connect() as c:
5677
yield c
5778

5879

80+
@fixture(scope="session")
81+
def connection_service_account(engine_service_account: Engine) -> Connection:
82+
with engine_service_account.connect() as c:
83+
yield c
84+
85+
5986
@fixture(scope="session")
6087
def event_loop():
6188
loop = asyncio.get_event_loop()

tests/integration/test_sqlalchemy_integration.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,7 @@ def test_get_columns(self, engine: Engine, fact_table_name: str):
111111
assert row_keys[1] == "type"
112112
assert row_keys[2] == "nullable"
113113
assert row_keys[3] == "default"
114+
115+
def test_service_account_connect(self, connection_service_account: Connection):
116+
result = connection_service_account.execute("SELECT 1")
117+
assert result.fetchall() == [(1,)]

tests/unit/test_firebolt_dialect.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,25 @@ def test_create_dialect(self, dialect: FireboltDialect):
3030
assert isinstance(dialect.type_compiler, FireboltTypeCompiler)
3131
assert dialect.context == {}
3232

33+
def test_create_connect_args_service_account(self, dialect: FireboltDialect):
34+
u = url.make_url(
35+
"test_engine://test-sa-user-key:test_password@test_db_name/test_engine_name"
36+
)
37+
with mock.patch.dict(os.environ, {"FIREBOLT_BASE_URL": "test_url"}):
38+
result_list, result_dict = dialect.create_connect_args(u)
39+
assert result_dict["engine_name"] == "test_engine_name"
40+
assert result_dict["auth"].client_id == "test-sa-user-key"
41+
assert result_dict["auth"].client_secret == "test_password"
42+
assert result_dict["auth"]._use_token_cache is True
43+
assert result_dict["database"] == "test_db_name"
44+
assert result_dict["api_endpoint"] == "test_url"
45+
assert "username" not in result_dict
46+
assert "password" not in result_dict
47+
assert result_list == []
48+
3349
def test_create_connect_args(self, dialect: FireboltDialect):
3450
connection_url = (
35-
"test_engine://test_user@email:test_password@test_db_name/test_engine_name"
51+
"test_engine://test_user@email:test_password@test_db_name/test_engine_name?"
3652
)
3753
u = url.make_url(connection_url)
3854
with mock.patch.dict(os.environ, {"FIREBOLT_BASE_URL": "test_url"}):

0 commit comments

Comments
 (0)