fivetran-cli 0.1.0__tar.gz → 0.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fivetran-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Command line wrapper for the Fivetran REST API
5
5
  Author: Fivetran
6
6
  License-Expression: MIT
@@ -22,10 +22,10 @@ Thin command line wrapper around the Fivetran REST API.
22
22
  fivetran [global-options] <resource> <operation> [arguments] [options]
23
23
  ```
24
24
 
25
- Credentials default to `FIVETRAN_API_KEY` and `FIVETRAN_API_SECRET`.
25
+ Credentials default to `FIVETRAN_API_KEY`. Set it to the single Base64-encoded API key shown by Fivetran.
26
26
 
27
27
  ```bash
28
- fivetran --api-key "$FIVETRAN_API_KEY" --api-secret "$FIVETRAN_API_SECRET" connection list
28
+ fivetran --api-key "$FIVETRAN_API_KEY" connection list
29
29
  fivetran connection create --data-file connection.json
30
30
  ```
31
31
 
@@ -34,7 +34,7 @@ fivetran connection create --data-file connection.json
34
34
  Global options may be placed before or after the resource command.
35
35
 
36
36
  ```bash
37
- fivetran connection list --api-key "$FIVETRAN_API_KEY" --api-secret "$FIVETRAN_API_SECRET"
37
+ fivetran connection list --api-key "$FIVETRAN_API_KEY"
38
38
  fivetran connection-schema get connection_id
39
39
  fivetran connector-metadata get google_ads
40
40
  fivetran public-connector list
@@ -6,10 +6,10 @@ Thin command line wrapper around the Fivetran REST API.
6
6
  fivetran [global-options] <resource> <operation> [arguments] [options]
7
7
  ```
8
8
 
9
- Credentials default to `FIVETRAN_API_KEY` and `FIVETRAN_API_SECRET`.
9
+ Credentials default to `FIVETRAN_API_KEY`. Set it to the single Base64-encoded API key shown by Fivetran.
10
10
 
11
11
  ```bash
12
- fivetran --api-key "$FIVETRAN_API_KEY" --api-secret "$FIVETRAN_API_SECRET" connection list
12
+ fivetran --api-key "$FIVETRAN_API_KEY" connection list
13
13
  fivetran connection create --data-file connection.json
14
14
  ```
15
15
 
@@ -18,7 +18,7 @@ fivetran connection create --data-file connection.json
18
18
  Global options may be placed before or after the resource command.
19
19
 
20
20
  ```bash
21
- fivetran connection list --api-key "$FIVETRAN_API_KEY" --api-secret "$FIVETRAN_API_SECRET"
21
+ fivetran connection list --api-key "$FIVETRAN_API_KEY"
22
22
  fivetran connection-schema get connection_id
23
23
  fivetran connector-metadata get google_ads
24
24
  fivetran public-connector list
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fivetran-cli"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Command line wrapper for the Fivetran REST API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -18,7 +18,6 @@ from .output import emit_output
18
18
  Handler = Callable[[argparse.Namespace, FivetranClient], Any]
19
19
  GLOBAL_OPTIONS_WITH_VALUES = {
20
20
  "--api-key",
21
- "--api-secret",
22
21
  "--profile",
23
22
  "--base-url",
24
23
  "--format",
@@ -26,7 +25,7 @@ GLOBAL_OPTIONS_WITH_VALUES = {
26
25
  }
27
26
  GLOBAL_FLAG_OPTIONS = {"--quiet", "--verbose"}
28
27
  OPERATION_EPILOG = (
29
- "Credentials default to FIVETRAN_API_KEY and FIVETRAN_API_SECRET. "
28
+ "Credentials default to FIVETRAN_API_KEY, which should contain the Base64-encoded API key. "
30
29
  "FIVETRAN_BASE_URL overrides the default API base URL. "
31
30
  "Global options may be placed before or after the resource command."
32
31
  )
@@ -121,10 +120,9 @@ def build_parser() -> argparse.ArgumentParser:
121
120
 
122
121
 
123
122
  def _add_global_options(parser: argparse.ArgumentParser) -> None:
124
- parser.add_argument("--api-key", help="Fivetran API key. Defaults to FIVETRAN_API_KEY.")
125
123
  parser.add_argument(
126
- "--api-secret",
127
- help="Fivetran API secret. Defaults to FIVETRAN_API_SECRET.",
124
+ "--api-key",
125
+ help="Base64-encoded Fivetran API key. Defaults to FIVETRAN_API_KEY.",
128
126
  )
129
127
  parser.add_argument("--profile", help="Named credential profile.")
130
128
  parser.add_argument(
@@ -1413,11 +1411,6 @@ def _confirm_destructive(args: argparse.Namespace) -> None:
1413
1411
  def resolve_config(args: argparse.Namespace) -> ClientConfig:
1414
1412
  profile = _load_profile(args.profile) if args.profile else {}
1415
1413
  api_key = args.api_key or profile.get("api_key") or os.environ.get("FIVETRAN_API_KEY")
1416
- api_secret = (
1417
- args.api_secret
1418
- or profile.get("api_secret")
1419
- or os.environ.get("FIVETRAN_API_SECRET")
1420
- )
1421
1414
  base_url = (
1422
1415
  args.base_url
1423
1416
  or profile.get("base_url")
@@ -1426,7 +1419,6 @@ def resolve_config(args: argparse.Namespace) -> ClientConfig:
1426
1419
  )
1427
1420
  return ClientConfig(
1428
1421
  api_key=api_key,
1429
- api_secret=api_secret,
1430
1422
  base_url=base_url,
1431
1423
  verbose=args.verbose,
1432
1424
  )
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import base64
4
3
  import json
5
4
  import urllib.error
6
5
  import urllib.parse
@@ -19,7 +18,6 @@ class CliError(Exception):
19
18
  @dataclass(frozen=True)
20
19
  class ClientConfig:
21
20
  api_key: str | None
22
- api_secret: str | None
23
21
  base_url: str = DEFAULT_BASE_URL
24
22
  verbose: bool = False
25
23
 
@@ -38,10 +36,10 @@ class FivetranClient:
38
36
  body: Any = None,
39
37
  auth_required: bool = True,
40
38
  ) -> Any:
41
- if auth_required and (not self.config.api_key or not self.config.api_secret):
39
+ if auth_required and not self.config.api_key:
42
40
  raise CliError(
43
- "Missing Fivetran API credentials. Set FIVETRAN_API_KEY and "
44
- "FIVETRAN_API_SECRET, pass --api-key/--api-secret, or use --profile."
41
+ "Missing Fivetran API key. Set FIVETRAN_API_KEY to the "
42
+ "Base64-encoded API key, pass --api-key, or use --profile."
45
43
  )
46
44
 
47
45
  url = self._url(path, query)
@@ -52,8 +50,7 @@ class FivetranClient:
52
50
  headers["Content-Type"] = "application/json"
53
51
 
54
52
  if auth_required:
55
- token = f"{self.config.api_key}:{self.config.api_secret}".encode("utf-8")
56
- headers["Authorization"] = "Basic " + base64.b64encode(token).decode("ascii")
53
+ headers["Authorization"] = _authorization_header(self.config.api_key)
57
54
 
58
55
  request = urllib.request.Request(url, data=data, headers=headers, method=method)
59
56
 
@@ -157,3 +154,12 @@ def _next_cursor(page: Any) -> str | None:
157
154
  cursor = data.get("next_cursor")
158
155
  return str(cursor) if cursor else None
159
156
  return None
157
+
158
+
159
+ def _authorization_header(api_key: str | None) -> str:
160
+ if api_key is None:
161
+ raise CliError("Missing Fivetran API key.")
162
+ api_key = api_key.strip()
163
+ if api_key.lower().startswith("basic "):
164
+ return api_key
165
+ return f"Basic {api_key}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fivetran-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Command line wrapper for the Fivetran REST API
5
5
  Author: Fivetran
6
6
  License-Expression: MIT
@@ -22,10 +22,10 @@ Thin command line wrapper around the Fivetran REST API.
22
22
  fivetran [global-options] <resource> <operation> [arguments] [options]
23
23
  ```
24
24
 
25
- Credentials default to `FIVETRAN_API_KEY` and `FIVETRAN_API_SECRET`.
25
+ Credentials default to `FIVETRAN_API_KEY`. Set it to the single Base64-encoded API key shown by Fivetran.
26
26
 
27
27
  ```bash
28
- fivetran --api-key "$FIVETRAN_API_KEY" --api-secret "$FIVETRAN_API_SECRET" connection list
28
+ fivetran --api-key "$FIVETRAN_API_KEY" connection list
29
29
  fivetran connection create --data-file connection.json
30
30
  ```
31
31
 
@@ -34,7 +34,7 @@ fivetran connection create --data-file connection.json
34
34
  Global options may be placed before or after the resource command.
35
35
 
36
36
  ```bash
37
- fivetran connection list --api-key "$FIVETRAN_API_KEY" --api-secret "$FIVETRAN_API_SECRET"
37
+ fivetran connection list --api-key "$FIVETRAN_API_KEY"
38
38
  fivetran connection-schema get connection_id
39
39
  fivetran connector-metadata get google_ads
40
40
  fivetran public-connector list
@@ -51,8 +51,6 @@ class CliTests(unittest.TestCase):
51
51
  [
52
52
  "--api-key",
53
53
  "key",
54
- "--api-secret",
55
- "secret",
56
54
  "connection",
57
55
  "list",
58
56
  "--group-id",
@@ -88,8 +86,6 @@ class CliTests(unittest.TestCase):
88
86
  [
89
87
  "--api-key",
90
88
  "key",
91
- "--api-secret",
92
- "secret",
93
89
  "connection",
94
90
  "create",
95
91
  "--data",
@@ -113,8 +109,6 @@ class CliTests(unittest.TestCase):
113
109
  "connection_id",
114
110
  "--api-key",
115
111
  "key",
116
- "--api-secret",
117
- "secret",
118
112
  "--format",
119
113
  "table",
120
114
  ]
@@ -129,8 +123,6 @@ class CliTests(unittest.TestCase):
129
123
  [
130
124
  "--api-key",
131
125
  "key",
132
- "--api-secret",
133
- "secret",
134
126
  "group",
135
127
  "delete",
136
128
  "group_id",
@@ -146,8 +138,6 @@ class CliTests(unittest.TestCase):
146
138
  [
147
139
  "--api-key",
148
140
  "key",
149
- "--api-secret",
150
- "secret",
151
141
  "group",
152
142
  "delete",
153
143
  "group_id",
@@ -162,7 +152,7 @@ class CliTests(unittest.TestCase):
162
152
  with tempfile.TemporaryDirectory() as tmpdir:
163
153
  config_path = Path(tmpdir) / "config.toml"
164
154
  config_path.write_text(
165
- '[profiles.dev]\napi_key = "profile-key"\napi_secret = "profile-secret"\nbase_url = "https://example.test"\n',
155
+ '[profiles.dev]\napi_key = "profile-key"\nbase_url = "https://example.test"\n',
166
156
  encoding="utf-8",
167
157
  )
168
158
  with patch.dict(os.environ, {"FIVETRAN_CONFIG": str(config_path)}, clear=False):
@@ -170,7 +160,6 @@ class CliTests(unittest.TestCase):
170
160
 
171
161
  self.assertEqual(code, 0, stderr)
172
162
  self.assertEqual(client.config.api_key, "profile-key")
173
- self.assertEqual(client.config.api_secret, "profile-secret")
174
163
  self.assertEqual(client.config.base_url, "https://example.test")
175
164
  self.assertEqual(client.calls[0][0:2], ("GET", "/v1/account/info"))
176
165
 
@@ -179,8 +168,6 @@ class CliTests(unittest.TestCase):
179
168
  [
180
169
  "--api-key",
181
170
  "key",
182
- "--api-secret",
183
- "secret",
184
171
  "user",
185
172
  "connection",
186
173
  "update",
@@ -208,8 +195,6 @@ class CliTests(unittest.TestCase):
208
195
  [
209
196
  "--api-key",
210
197
  "key",
211
- "--api-secret",
212
- "secret",
213
198
  "team",
214
199
  "connection",
215
200
  "add",
@@ -228,8 +213,6 @@ class CliTests(unittest.TestCase):
228
213
  [
229
214
  "--api-key",
230
215
  "key",
231
- "--api-secret",
232
- "secret",
233
216
  "system-key",
234
217
  "rotate",
235
218
  "key_id",
@@ -245,8 +228,6 @@ class CliTests(unittest.TestCase):
245
228
  [
246
229
  "--api-key",
247
230
  "key",
248
- "--api-secret",
249
- "secret",
250
231
  "group",
251
232
  "public-key",
252
233
  "get",
@@ -262,8 +243,6 @@ class CliTests(unittest.TestCase):
262
243
  [
263
244
  "--api-key",
264
245
  "key",
265
- "--api-secret",
266
- "secret",
267
246
  "--output",
268
247
  "/missing-directory/output.json",
269
248
  "account",
@@ -280,8 +259,6 @@ class CliTests(unittest.TestCase):
280
259
  [
281
260
  "--api-key",
282
261
  "key",
283
- "--api-secret",
284
- "secret",
285
262
  "connection-schema",
286
263
  "column",
287
264
  "drop-blocked",
@@ -309,8 +286,6 @@ class CliTests(unittest.TestCase):
309
286
  [
310
287
  "--api-key",
311
288
  "key",
312
- "--api-secret",
313
- "secret",
314
289
  "connection-schema",
315
290
  "column",
316
291
  "update",
@@ -337,8 +312,6 @@ class CliTests(unittest.TestCase):
337
312
  [
338
313
  "--api-key",
339
314
  "key",
340
- "--api-secret",
341
- "secret",
342
315
  "connector-metadata",
343
316
  "get",
344
317
  "google_ads",
@@ -360,8 +333,6 @@ class CliTests(unittest.TestCase):
360
333
  [
361
334
  "--api-key",
362
335
  "key",
363
- "--api-secret",
364
- "secret",
365
336
  "hybrid-deployment-agent",
366
337
  "list",
367
338
  "--group-id",
@@ -388,8 +359,6 @@ class CliTests(unittest.TestCase):
388
359
  [
389
360
  "--api-key",
390
361
  "key",
391
- "--api-secret",
392
- "secret",
393
362
  "proxy-agent",
394
363
  "connection",
395
364
  "list",
@@ -405,8 +374,6 @@ class CliTests(unittest.TestCase):
405
374
  [
406
375
  "--api-key",
407
376
  "key",
408
- "--api-secret",
409
- "secret",
410
377
  "private-link",
411
378
  "update",
412
379
  "private_link_id",
@@ -424,8 +391,6 @@ class CliTests(unittest.TestCase):
424
391
  [
425
392
  "--api-key",
426
393
  "key",
427
- "--api-secret",
428
- "secret",
429
394
  "transformation",
430
395
  "list",
431
396
  "--all",
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import unittest
4
+ from unittest.mock import MagicMock, patch
4
5
 
5
6
  from fivetran_cli.client import CliError, ClientConfig, FivetranClient
6
7
 
7
8
 
8
9
  class PagingClient(FivetranClient):
9
10
  def __init__(self) -> None:
10
- super().__init__(ClientConfig(api_key="key", api_secret="secret"))
11
+ super().__init__(ClientConfig(api_key="encoded-key"))
11
12
  self.calls = []
12
13
 
13
14
  def request(self, method, path, *, query=None, body=None, auth_required=True):
@@ -37,16 +38,44 @@ class ClientTests(unittest.TestCase):
37
38
  )
38
39
 
39
40
  def test_request_requires_credentials_for_authenticated_endpoints(self):
40
- client = FivetranClient(ClientConfig(api_key=None, api_secret=None))
41
+ client = FivetranClient(ClientConfig(api_key=None))
41
42
 
42
43
  with self.assertRaises(CliError) as context:
43
44
  client.request("GET", "/v1/connections")
44
45
 
45
- self.assertIn("Missing Fivetran API credentials", str(context.exception))
46
+ self.assertIn("Missing Fivetran API key", str(context.exception))
47
+
48
+ def test_request_uses_encoded_api_key_directly_in_basic_auth_header(self):
49
+ client = FivetranClient(
50
+ ClientConfig(api_key="already-base64", base_url="https://api.example.test")
51
+ )
52
+ response = MagicMock()
53
+ response.read.return_value = b'{"data":{"ok":true}}'
54
+ response.__enter__.return_value = response
55
+
56
+ with patch("urllib.request.urlopen", return_value=response) as urlopen:
57
+ client.request("GET", "/v1/account/info")
58
+
59
+ request = urlopen.call_args.args[0]
60
+ self.assertEqual(request.headers["Authorization"], "Basic already-base64")
61
+
62
+ def test_request_accepts_api_key_with_basic_prefix(self):
63
+ client = FivetranClient(
64
+ ClientConfig(api_key="Basic already-base64", base_url="https://api.example.test")
65
+ )
66
+ response = MagicMock()
67
+ response.read.return_value = b'{"data":{"ok":true}}'
68
+ response.__enter__.return_value = response
69
+
70
+ with patch("urllib.request.urlopen", return_value=response) as urlopen:
71
+ client.request("GET", "/v1/account/info")
72
+
73
+ request = urlopen.call_args.args[0]
74
+ self.assertEqual(request.headers["Authorization"], "Basic already-base64")
46
75
 
47
76
  def test_url_omits_empty_query_values(self):
48
77
  client = FivetranClient(
49
- ClientConfig(api_key="key", api_secret="secret", base_url="https://api.example.test/")
78
+ ClientConfig(api_key="encoded-key", base_url="https://api.example.test/")
50
79
  )
51
80
 
52
81
  self.assertEqual(
File without changes
File without changes
File without changes