fullscript.spellbook 3.0.1__tar.gz → 3.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/PKG-INFO +19 -10
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/README.md +18 -9
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/documentation/db_magic.md +4 -2
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/PKG-INFO +19 -10
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/scm_version.json +2 -2
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/db_magic.py +58 -8
- fullscript_spellbook-3.0.2/spellbook_config.yaml +55 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/tests/test_db_magic.py +88 -0
- fullscript_spellbook-3.0.1/spellbook_config.yaml +0 -54
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/.gitignore +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/.gitlab-ci.yml +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/.python-version +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/AGENTS.md +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/CLAUDE.md +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/LICENSE +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/MANIFEST.in +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/documentation/drive_magic.md +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/documentation/sheet_magic.md +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/documentation/snowflake_key_pair_setup.md +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/documentation/stats_magic.md +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/SOURCES.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/dependency_links.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/requires.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/scm_file_list.json +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/top_level.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/pyproject.toml +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/requirements.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/setup.cfg +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/__init__.py +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/drive_magic.py +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/exceptions.py +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/py.typed +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/sheet_magic.py +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/stats_magic.py +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/utils.py +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/__init__.py +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard1.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard10.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard11.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard12.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard2.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard3.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard4.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard5.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard6.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard7.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard8.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/spellbook/wizards/wizard9.txt +0 -0
- {fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fullscript.spellbook
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: A simple package to make your life easier.
|
|
5
5
|
Author-email: Meinhard Mare <meinhard.mare@fullscript.com>, Thomas Edwards <thomas.edwards@fullscript.com>, Gui Chagas <gui.chagas@fullscript.com>, David King <david.king@fullscript.com>, Karsten Melchers <karsten.melchers@fullscript.com>
|
|
6
6
|
License: MIT
|
|
@@ -177,25 +177,34 @@ An example of the `spellbook_config.yaml` file is shown below:
|
|
|
177
177
|
```yaml
|
|
178
178
|
databases:
|
|
179
179
|
# Snowflake Database Connection
|
|
180
|
-
- name:
|
|
180
|
+
- name: snowflake_example
|
|
181
181
|
type: snowflake
|
|
182
|
-
account:
|
|
183
|
-
user:
|
|
184
|
-
password:
|
|
185
|
-
private_key:
|
|
186
|
-
private_key_passphrase:
|
|
182
|
+
account: ${SNOWFLAKE_ACCOUNT}
|
|
183
|
+
user: ${SNOWFLAKE_USER}
|
|
184
|
+
password: ${SNOWFLAKE_PASSWORD} # Optional when a private key is given
|
|
185
|
+
private_key: ${SNOW_PK} # Base64-encoded; used in preference to the password
|
|
186
|
+
private_key_passphrase: ${SNOW_PK_PASSPHRASE} # Optional
|
|
187
187
|
role: snowflake_role
|
|
188
188
|
warehouse: snowflake_warehouse
|
|
189
189
|
database: snowflake_database
|
|
190
|
-
schema: snowflake_schema
|
|
190
|
+
schema: snowflake_schema # Optional, defaults to 'public'
|
|
191
|
+
|
|
192
|
+
# MySQL Database Connection using a stored password
|
|
193
|
+
- name: mysql_example
|
|
194
|
+
type: mysql
|
|
195
|
+
host: your_host
|
|
196
|
+
port: 3306
|
|
197
|
+
user: mysql_user_name
|
|
198
|
+
password: ${MYSQL_PASSWORD}
|
|
199
|
+
database: mysql_database
|
|
191
200
|
|
|
192
201
|
# MySQL Database Connection using AWS RDS IAM authentication (no stored password).
|
|
193
202
|
# Fullscript internal only: requires the `rx` CLI. See documentation/db_magic.md.
|
|
194
|
-
-
|
|
203
|
+
# No `user` is needed - the IAM token names the user it was issued for.
|
|
204
|
+
- name: mysql_iam_example
|
|
195
205
|
type: mysql
|
|
196
206
|
host: your_host.rds.amazonaws.com
|
|
197
207
|
port: 3306
|
|
198
|
-
user: ${DB_USER}
|
|
199
208
|
iam_auth: true
|
|
200
209
|
database: ${MYSQL_DATABASE}
|
|
201
210
|
|
|
@@ -54,25 +54,34 @@ An example of the `spellbook_config.yaml` file is shown below:
|
|
|
54
54
|
```yaml
|
|
55
55
|
databases:
|
|
56
56
|
# Snowflake Database Connection
|
|
57
|
-
- name:
|
|
57
|
+
- name: snowflake_example
|
|
58
58
|
type: snowflake
|
|
59
|
-
account:
|
|
60
|
-
user:
|
|
61
|
-
password:
|
|
62
|
-
private_key:
|
|
63
|
-
private_key_passphrase:
|
|
59
|
+
account: ${SNOWFLAKE_ACCOUNT}
|
|
60
|
+
user: ${SNOWFLAKE_USER}
|
|
61
|
+
password: ${SNOWFLAKE_PASSWORD} # Optional when a private key is given
|
|
62
|
+
private_key: ${SNOW_PK} # Base64-encoded; used in preference to the password
|
|
63
|
+
private_key_passphrase: ${SNOW_PK_PASSPHRASE} # Optional
|
|
64
64
|
role: snowflake_role
|
|
65
65
|
warehouse: snowflake_warehouse
|
|
66
66
|
database: snowflake_database
|
|
67
|
-
schema: snowflake_schema
|
|
67
|
+
schema: snowflake_schema # Optional, defaults to 'public'
|
|
68
|
+
|
|
69
|
+
# MySQL Database Connection using a stored password
|
|
70
|
+
- name: mysql_example
|
|
71
|
+
type: mysql
|
|
72
|
+
host: your_host
|
|
73
|
+
port: 3306
|
|
74
|
+
user: mysql_user_name
|
|
75
|
+
password: ${MYSQL_PASSWORD}
|
|
76
|
+
database: mysql_database
|
|
68
77
|
|
|
69
78
|
# MySQL Database Connection using AWS RDS IAM authentication (no stored password).
|
|
70
79
|
# Fullscript internal only: requires the `rx` CLI. See documentation/db_magic.md.
|
|
71
|
-
-
|
|
80
|
+
# No `user` is needed - the IAM token names the user it was issued for.
|
|
81
|
+
- name: mysql_iam_example
|
|
72
82
|
type: mysql
|
|
73
83
|
host: your_host.rds.amazonaws.com
|
|
74
84
|
port: 3306
|
|
75
|
-
user: ${DB_USER}
|
|
76
85
|
iam_auth: true
|
|
77
86
|
database: ${MYSQL_DATABASE}
|
|
78
87
|
|
|
@@ -81,16 +81,18 @@ tokens expire after about 15 minutes:
|
|
|
81
81
|
type: mysql
|
|
82
82
|
host: ${MYSQL_HOST} # e.g. your-instance.abc123.us-east-1.rds.amazonaws.com
|
|
83
83
|
port: 3306
|
|
84
|
-
user: ${DB_USER}
|
|
85
84
|
iam_auth: true
|
|
86
85
|
rds_db_name: your_rds_instance_name # Optional, defaults to the value of `name`
|
|
87
86
|
database: ${MYSQL_DATABASE}
|
|
88
87
|
```
|
|
89
88
|
|
|
89
|
+
No `user` is needed: the IAM token names the user it was issued for, and Spellbook reads it
|
|
90
|
+
from the token. Set `user` only if you want to pin it, in which case it must match the token.
|
|
91
|
+
|
|
90
92
|
| Field | Required | Notes |
|
|
91
93
|
| --- | --- | --- |
|
|
92
94
|
| `iam_auth` | No | `true` switches the database to IAM tokens. Omitting it keeps password authentication. |
|
|
93
|
-
| `user` |
|
|
95
|
+
| `user` | No | Read from the token's `DBUser`. Set it only to pin a specific user; a value that disagrees with the token is reported as a config error. |
|
|
94
96
|
| `rds_db_name` | No | The remote database as named by `rx rds list`. Defaults to `name`, so set it whenever your config alias differs from the RDS name. |
|
|
95
97
|
| `ssl_ca` | No | Not needed in normal use; see below. |
|
|
96
98
|
| `password` | No | Ignored when `iam_auth` is true. |
|
{fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fullscript.spellbook
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: A simple package to make your life easier.
|
|
5
5
|
Author-email: Meinhard Mare <meinhard.mare@fullscript.com>, Thomas Edwards <thomas.edwards@fullscript.com>, Gui Chagas <gui.chagas@fullscript.com>, David King <david.king@fullscript.com>, Karsten Melchers <karsten.melchers@fullscript.com>
|
|
6
6
|
License: MIT
|
|
@@ -177,25 +177,34 @@ An example of the `spellbook_config.yaml` file is shown below:
|
|
|
177
177
|
```yaml
|
|
178
178
|
databases:
|
|
179
179
|
# Snowflake Database Connection
|
|
180
|
-
- name:
|
|
180
|
+
- name: snowflake_example
|
|
181
181
|
type: snowflake
|
|
182
|
-
account:
|
|
183
|
-
user:
|
|
184
|
-
password:
|
|
185
|
-
private_key:
|
|
186
|
-
private_key_passphrase:
|
|
182
|
+
account: ${SNOWFLAKE_ACCOUNT}
|
|
183
|
+
user: ${SNOWFLAKE_USER}
|
|
184
|
+
password: ${SNOWFLAKE_PASSWORD} # Optional when a private key is given
|
|
185
|
+
private_key: ${SNOW_PK} # Base64-encoded; used in preference to the password
|
|
186
|
+
private_key_passphrase: ${SNOW_PK_PASSPHRASE} # Optional
|
|
187
187
|
role: snowflake_role
|
|
188
188
|
warehouse: snowflake_warehouse
|
|
189
189
|
database: snowflake_database
|
|
190
|
-
schema: snowflake_schema
|
|
190
|
+
schema: snowflake_schema # Optional, defaults to 'public'
|
|
191
|
+
|
|
192
|
+
# MySQL Database Connection using a stored password
|
|
193
|
+
- name: mysql_example
|
|
194
|
+
type: mysql
|
|
195
|
+
host: your_host
|
|
196
|
+
port: 3306
|
|
197
|
+
user: mysql_user_name
|
|
198
|
+
password: ${MYSQL_PASSWORD}
|
|
199
|
+
database: mysql_database
|
|
191
200
|
|
|
192
201
|
# MySQL Database Connection using AWS RDS IAM authentication (no stored password).
|
|
193
202
|
# Fullscript internal only: requires the `rx` CLI. See documentation/db_magic.md.
|
|
194
|
-
-
|
|
203
|
+
# No `user` is needed - the IAM token names the user it was issued for.
|
|
204
|
+
- name: mysql_iam_example
|
|
195
205
|
type: mysql
|
|
196
206
|
host: your_host.rds.amazonaws.com
|
|
197
207
|
port: 3306
|
|
198
|
-
user: ${DB_USER}
|
|
199
208
|
iam_auth: true
|
|
200
209
|
database: ${MYSQL_DATABASE}
|
|
201
210
|
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import re
|
|
5
5
|
import subprocess
|
|
6
6
|
from typing import Any
|
|
7
|
-
from urllib.parse import quote_plus
|
|
7
|
+
from urllib.parse import parse_qs, quote_plus
|
|
8
8
|
|
|
9
9
|
import pandas as pd
|
|
10
10
|
from cryptography.hazmat.backends import default_backend
|
|
@@ -49,13 +49,48 @@ SUPPORTED_DB_TYPES = ('snowflake', 'mysql')
|
|
|
49
49
|
# Settings a connection genuinely cannot be made without, checked only for the database
|
|
50
50
|
# being used. Anything absent from here is optional and may resolve to an empty value:
|
|
51
51
|
# `private_key_passphrase`, `schema`, `port`, `ssl_ca` and `rds_db_name` all have defaults
|
|
52
|
-
# or meaningful empty behaviour.
|
|
52
|
+
# or meaningful empty behaviour. See required_fields() for the per-database adjustments.
|
|
53
53
|
REQUIRED_FIELDS = {
|
|
54
54
|
'mysql': ('host', 'user', 'database'),
|
|
55
55
|
'snowflake': ('account', 'user', 'role', 'warehouse', 'database'),
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def required_fields(db_config: dict[str, Any]) -> tuple[str, ...]:
|
|
60
|
+
"""
|
|
61
|
+
Work out which settings this particular database cannot do without.
|
|
62
|
+
|
|
63
|
+
MySQL needs a `password` unless it authenticates with an IAM token, and conversely an
|
|
64
|
+
IAM database does not need a `user`, because the token says who it was issued to.
|
|
65
|
+
|
|
66
|
+
:param db_config: The configuration block for the database.
|
|
67
|
+
:return: The names of the settings that must be present and non-empty.
|
|
68
|
+
"""
|
|
69
|
+
fields = REQUIRED_FIELDS.get(db_config.get('type'), ())
|
|
70
|
+
|
|
71
|
+
if db_config.get('type') == 'mysql':
|
|
72
|
+
if db_config.get('iam_auth'):
|
|
73
|
+
return tuple(field for field in fields if field != 'user')
|
|
74
|
+
return fields + ('password',)
|
|
75
|
+
|
|
76
|
+
return fields
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def db_user_from_token(token: str) -> str | None:
|
|
80
|
+
"""
|
|
81
|
+
Read the database user out of an AWS RDS IAM token.
|
|
82
|
+
|
|
83
|
+
The token is a presigned URL and carries the user it was issued for as its `DBUser`
|
|
84
|
+
parameter, so the user does not have to be repeated in the configuration file.
|
|
85
|
+
|
|
86
|
+
:param token: The token returned by `rx rds password`.
|
|
87
|
+
:return: The database user, or None if the token does not name one.
|
|
88
|
+
"""
|
|
89
|
+
_, _, query = (token or '').partition('?')
|
|
90
|
+
users = parse_qs(query).get('DBUser') or []
|
|
91
|
+
return users[0] or None if users else None
|
|
92
|
+
|
|
93
|
+
|
|
59
94
|
def _strip_ansi(text_value: str | None) -> str:
|
|
60
95
|
"""
|
|
61
96
|
Remove ANSI colour and cursor codes from CLI output.
|
|
@@ -80,11 +115,7 @@ def _validate_db_config(db_name: str, db_config: dict[str, Any]) -> dict[str, An
|
|
|
80
115
|
"""
|
|
81
116
|
unresolved = db_config.pop('_unresolved_env', {})
|
|
82
117
|
|
|
83
|
-
|
|
84
|
-
if db_config.get('type') == 'mysql' and not db_config.get('iam_auth'):
|
|
85
|
-
required = required + ('password',)
|
|
86
|
-
|
|
87
|
-
for key in required:
|
|
118
|
+
for key in required_fields(db_config):
|
|
88
119
|
if db_config.get(key):
|
|
89
120
|
continue
|
|
90
121
|
if key in unresolved:
|
|
@@ -270,11 +301,30 @@ def mysql_connect(db_name: str) -> Connection:
|
|
|
270
301
|
# RDS instance names rarely match the alias used in the config file, so allow an
|
|
271
302
|
# override and fall back to the alias when the two happen to line up.
|
|
272
303
|
token = get_rds_iam_token(db_config.get('rds_db_name', db_config['name']))
|
|
304
|
+
|
|
305
|
+
# The token names the user it was issued for, so `user` is optional in the config.
|
|
306
|
+
token_user = db_user_from_token(token)
|
|
307
|
+
user = db_config.get('user') or token_user
|
|
308
|
+
if not user:
|
|
309
|
+
raise ConfigError(
|
|
310
|
+
f"Database '{db_name}' has no 'user', and the token from 'rx rds password' "
|
|
311
|
+
f"did not name one either. Add a 'user' to {CONFIG_FILENAME}."
|
|
312
|
+
)
|
|
313
|
+
if token_user and db_config.get('user') and db_config['user'] != token_user:
|
|
314
|
+
# The token is signed for one user, so connecting as another is rejected by RDS
|
|
315
|
+
# with a bare access-denied. Say what is actually wrong instead.
|
|
316
|
+
raise ConfigError(
|
|
317
|
+
f"Database '{db_name}' is configured for user "
|
|
318
|
+
f"{db_config['user']!r}, but 'rx rds password' issued a token for "
|
|
319
|
+
f"{token_user!r}. Remove 'user' from {CONFIG_FILENAME} to use the token's "
|
|
320
|
+
"own user, or correct it to match."
|
|
321
|
+
)
|
|
322
|
+
|
|
273
323
|
# IAM tokens are URL shaped, so they have to be escaped inside the connection
|
|
274
324
|
# string. AWS also requires TLS, and only pymysql completes the token handshake.
|
|
275
325
|
ssl_ca = db_config.get('ssl_ca')
|
|
276
326
|
connection_string = (
|
|
277
|
-
f"mysql+pymysql://{quote_plus(
|
|
327
|
+
f"mysql+pymysql://{quote_plus(user)}:{quote_plus(token)}"
|
|
278
328
|
f"@{host}:{port}/{database}"
|
|
279
329
|
)
|
|
280
330
|
# A CA bundle gets the certificate verified too; without one we still insist on
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
databases:
|
|
2
|
+
# Snowflake connection, with values written straight into this file.
|
|
3
|
+
# Spellbook warns about hardcoded credentials, so prefer the next example.
|
|
4
|
+
- name: snowflake_example
|
|
5
|
+
type: snowflake
|
|
6
|
+
account: your_account
|
|
7
|
+
user: snowflake_user_name
|
|
8
|
+
password: snowflake_password # Optional when a private key is given
|
|
9
|
+
private_key: snowflake_pk # Base64-encoded; used in preference to the password
|
|
10
|
+
private_key_passphrase: snowflake_pk_phrase # Optional, omit if the key has none
|
|
11
|
+
role: snowflake_role
|
|
12
|
+
warehouse: snowflake_warehouse
|
|
13
|
+
database: snowflake_database
|
|
14
|
+
schema: snowflake_schema # Optional, defaults to 'public'
|
|
15
|
+
|
|
16
|
+
# The same connection, reading its sensitive values from environment variables.
|
|
17
|
+
- name: snowflake_env_example
|
|
18
|
+
type: snowflake
|
|
19
|
+
account: ${SNOWFLAKE_ACCOUNT}
|
|
20
|
+
user: ${SNOWFLAKE_USER}
|
|
21
|
+
password: ${SNOWFLAKE_PASSWORD}
|
|
22
|
+
private_key: ${SNOW_PK}
|
|
23
|
+
private_key_passphrase: ${SNOW_PK_PASSPHRASE} # Optional
|
|
24
|
+
role: snowflake_role
|
|
25
|
+
warehouse: snowflake_warehouse
|
|
26
|
+
database: snowflake_database
|
|
27
|
+
schema: snowflake_schema
|
|
28
|
+
|
|
29
|
+
# MySQL connection using a stored password.
|
|
30
|
+
- name: mysql_example
|
|
31
|
+
type: mysql
|
|
32
|
+
host: your_host
|
|
33
|
+
port: 3306 # Optional, defaults to 3306
|
|
34
|
+
user: mysql_user_name
|
|
35
|
+
password: ${MYSQL_PASSWORD}
|
|
36
|
+
database: mysql_database
|
|
37
|
+
|
|
38
|
+
# MySQL connection using AWS RDS IAM authentication, so no password is stored.
|
|
39
|
+
# Spellbook fetches a temporary token on every connection, because they expire
|
|
40
|
+
# after about 15 minutes. Requires the `rx` CLI, which is Fullscript internal tooling.
|
|
41
|
+
- name: mysql_iam_example
|
|
42
|
+
type: mysql
|
|
43
|
+
host: ${MYSQL_HOST} # e.g. your-instance.abc123.us-east-1.rds.amazonaws.com
|
|
44
|
+
port: 3306
|
|
45
|
+
iam_auth: true # Use an AWS RDS IAM token instead of a password
|
|
46
|
+
rds_db_name: your_rds_instance_name # Optional, as named by `rx rds list`; defaults to `name`
|
|
47
|
+
database: ${MYSQL_DATABASE}
|
|
48
|
+
|
|
49
|
+
google_accounts:
|
|
50
|
+
# Google Sheets Service Account 1
|
|
51
|
+
- name: google_sheets_service_account_1
|
|
52
|
+
service_account: "your_base64_encoded_service_account_credentials_here" # Add your Base64-encoded service account JSON here
|
|
53
|
+
# Google Sheets Service Account 2
|
|
54
|
+
- name: google_sheets_service_account_2
|
|
55
|
+
service_account: "your_base64_encoded_service_account_credentials_here"
|
|
@@ -211,6 +211,94 @@ def test_iam_auth_fetches_a_fresh_token_per_connection(monkeypatch, config, engi
|
|
|
211
211
|
assert 'token-two' in engines[1][0]
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
# --- The IAM token names its own user ----------------------------------------------------
|
|
215
|
+
# An RDS IAM token is a presigned URL carrying DBUser, so `user` need not be configured.
|
|
216
|
+
|
|
217
|
+
@pytest.mark.parametrize('token, expected', [
|
|
218
|
+
(FAKE_TOKEN, 'iam_user'),
|
|
219
|
+
('host:3306/?Action=connect&DBUser=first.last&X-Amz-Signature=x', 'first.last'),
|
|
220
|
+
('host:3306/?DBUser=needs%40escaping', 'needs@escaping'),
|
|
221
|
+
('host:3306/?Action=connect&X-Amz-Signature=x', None), # no DBUser at all
|
|
222
|
+
('host:3306/?DBUser=', None), # present but empty
|
|
223
|
+
('not-a-url-at-all', None),
|
|
224
|
+
('', None),
|
|
225
|
+
])
|
|
226
|
+
def test_db_user_is_read_from_the_token(token, expected):
|
|
227
|
+
assert db_magic.db_user_from_token(token) == expected
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_user_is_derived_from_the_token_when_not_configured(monkeypatch, config, engines):
|
|
231
|
+
without_user = {key: value for key, value in IAM_DB.items() if key != 'user'}
|
|
232
|
+
config(without_user)
|
|
233
|
+
monkeypatch.setattr(subprocess, 'run', fake_rx(stdout=FAKE_TOKEN))
|
|
234
|
+
|
|
235
|
+
db_magic.mysql_connect('reporting_db')
|
|
236
|
+
|
|
237
|
+
# FAKE_TOKEN carries DBUser=iam_user
|
|
238
|
+
assert engines[0][0].startswith('mysql+pymysql://iam_user:')
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_a_configured_user_is_still_honoured(monkeypatch, config, engines):
|
|
242
|
+
config(IAM_DB)
|
|
243
|
+
monkeypatch.setattr(subprocess, 'run', fake_rx(stdout=FAKE_TOKEN))
|
|
244
|
+
|
|
245
|
+
db_magic.mysql_connect('reporting_db')
|
|
246
|
+
|
|
247
|
+
assert engines[0][0].startswith('mysql+pymysql://iam_user:')
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_a_user_that_disagrees_with_the_token_is_reported_clearly(
|
|
251
|
+
monkeypatch, config, engines,
|
|
252
|
+
):
|
|
253
|
+
"""RDS rejects a mismatch with a bare access-denied, so catch it with a real message."""
|
|
254
|
+
config({**IAM_DB, 'user': 'someone.else'})
|
|
255
|
+
monkeypatch.setattr(subprocess, 'run', fake_rx(stdout=FAKE_TOKEN))
|
|
256
|
+
|
|
257
|
+
with pytest.raises(ConfigError) as error:
|
|
258
|
+
db_magic.mysql_connect('reporting_db')
|
|
259
|
+
|
|
260
|
+
message = str(error.value)
|
|
261
|
+
assert 'someone.else' in message
|
|
262
|
+
assert 'iam_user' in message
|
|
263
|
+
assert engines == [], 'no connection should be attempted'
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_a_token_without_a_user_and_no_config_user_raises(monkeypatch, config, engines):
|
|
267
|
+
without_user = {key: value for key, value in IAM_DB.items() if key != 'user'}
|
|
268
|
+
config(without_user)
|
|
269
|
+
monkeypatch.setattr(subprocess, 'run', fake_rx(
|
|
270
|
+
stdout='host:3306/?Action=connect&X-Amz-Signature=x',
|
|
271
|
+
))
|
|
272
|
+
|
|
273
|
+
with pytest.raises(ConfigError, match="did not name one either"):
|
|
274
|
+
db_magic.mysql_connect('reporting_db')
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_user_is_not_required_in_config_for_iam_databases(config_file):
|
|
278
|
+
config_file({'name': 'db', 'type': 'mysql', 'host': 'h',
|
|
279
|
+
'database': 'd', 'iam_auth': True})
|
|
280
|
+
|
|
281
|
+
assert db_magic.get_db_config('db').get('user') is None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_user_is_still_required_for_password_databases(config_file):
|
|
285
|
+
config_file({'name': 'db', 'type': 'mysql', 'host': 'h',
|
|
286
|
+
'database': 'd', 'password': 'p'})
|
|
287
|
+
|
|
288
|
+
with pytest.raises(ConfigError, match="required 'user'"):
|
|
289
|
+
db_magic.get_db_config('db')
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@pytest.mark.parametrize('db_config, expected', [
|
|
293
|
+
({'type': 'mysql', 'iam_auth': True}, ('host', 'database')),
|
|
294
|
+
({'type': 'mysql'}, ('host', 'user', 'database', 'password')),
|
|
295
|
+
({'type': 'snowflake'}, ('account', 'user', 'role', 'warehouse', 'database')),
|
|
296
|
+
({'type': 'postgres'}, ()),
|
|
297
|
+
])
|
|
298
|
+
def test_required_fields_per_database_type(db_config, expected):
|
|
299
|
+
assert db_magic.required_fields(db_config) == expected
|
|
300
|
+
|
|
301
|
+
|
|
214
302
|
# --- Literal '%' must survive on the pymysql path ----------------------------------------
|
|
215
303
|
# pymysql substitutes whenever args is not None, so the empty parameter set pandas passes
|
|
216
304
|
# would turn `like '%x%'` into a format string. mysql-connector guards on `if params:`,
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
databases:
|
|
2
|
-
# Snowflake Database Connection
|
|
3
|
-
- name: alias_of_your_connection
|
|
4
|
-
type: snowflake
|
|
5
|
-
account: your_account
|
|
6
|
-
user: snowflake_user_name # Optional for private key auth
|
|
7
|
-
password: snowflake_password # Optional, used if private key is not present
|
|
8
|
-
private_key: snowflake_pk # Base64-encoded private key
|
|
9
|
-
private_key_passphrase: snowflake_pk_phrase # Optional
|
|
10
|
-
role: snowflake_role
|
|
11
|
-
warehouse: snowflake_warehouse
|
|
12
|
-
database: snowflake_database
|
|
13
|
-
schema: snowflake_schema
|
|
14
|
-
|
|
15
|
-
- name: alias_of_your_connection_2
|
|
16
|
-
type: snowflake
|
|
17
|
-
account: ${SNOWFLAKE_ACCOUNT}
|
|
18
|
-
user: ${SNOWFLAKE_USER} # Example of using environment variables
|
|
19
|
-
password: ${SNOWFLAKE_PASSWORD} # Example of using environment variables
|
|
20
|
-
private_key: snowflake_pk # Example of using environment variables
|
|
21
|
-
private_key_passphrase: snowflake_pk_phrase # Optional
|
|
22
|
-
role: snowflake_role
|
|
23
|
-
warehouse: snowflake_warehouse
|
|
24
|
-
database: snowflake_database
|
|
25
|
-
schema: snowflake_schema
|
|
26
|
-
|
|
27
|
-
# MySQL Database Connection
|
|
28
|
-
- name: alias_of_your_connection
|
|
29
|
-
type: mysql
|
|
30
|
-
host: your_host
|
|
31
|
-
port: 3306
|
|
32
|
-
user: mysql_user_name
|
|
33
|
-
password: mysql_password
|
|
34
|
-
database: mysql_database
|
|
35
|
-
|
|
36
|
-
# MySQL Database Connection using AWS RDS IAM authentication.
|
|
37
|
-
# Requires the `rx` CLI, which is Fullscript internal tooling.
|
|
38
|
-
- name: alias_of_your_connection_3
|
|
39
|
-
type: mysql
|
|
40
|
-
host: ${MYSQL_HOST} # e.g. your-instance.abc123.us-east-1.rds.amazonaws.com
|
|
41
|
-
port: 3306
|
|
42
|
-
user: ${DB_USER} # Your IAM database user
|
|
43
|
-
iam_auth: true # Use an AWS RDS IAM token instead of a password
|
|
44
|
-
rds_db_name: your_rds_instance_name # Optional, as named by `rx rds list`; defaults to `name`
|
|
45
|
-
database: ${MYSQL_DATABASE}
|
|
46
|
-
|
|
47
|
-
google_accounts:
|
|
48
|
-
# Google Sheets Service Account 1
|
|
49
|
-
- name: google_sheets_service_account_1
|
|
50
|
-
service_account: "your_base64_encoded_service_account_credentials_here" # Add your Base64-encoded service account JSON here
|
|
51
|
-
# Google Sheets Service Account 2
|
|
52
|
-
- name: google_sheets_service_account_2
|
|
53
|
-
service_account: "your_base64_encoded_service_account_credentials_here"
|
|
54
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/documentation/snowflake_key_pair_setup.md
RENAMED
|
File without changes
|
|
File without changes
|
{fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{fullscript_spellbook-3.0.1 → fullscript_spellbook-3.0.2}/fullscript.spellbook.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|