salesforce-data-customcode 0.1.18__tar.gz → 0.1.19__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.
Files changed (61) hide show
  1. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/PKG-INFO +3 -64
  2. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/README.md +2 -63
  3. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/pyproject.toml +1 -1
  4. salesforce_data_customcode-0.1.19/src/datacustomcode/auth.py +257 -0
  5. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/cli.py +29 -32
  6. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/credentials.py +28 -32
  7. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/LICENSE.txt +0 -0
  8. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/__init__.py +0 -0
  9. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/client.py +0 -0
  10. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/cmd.py +0 -0
  11. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/config.py +0 -0
  12. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/config.yaml +0 -0
  13. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/deploy.py +0 -0
  14. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/__init__.py +0 -0
  15. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/base.py +0 -0
  16. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/path/__init__.py +0 -0
  17. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/path/default.py +0 -0
  18. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/__init__.py +0 -0
  19. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/base.py +0 -0
  20. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/reader/__init__.py +0 -0
  21. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/reader/base.py +0 -0
  22. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/reader/query_api.py +0 -0
  23. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/__init__.py +0 -0
  24. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/base.py +0 -0
  25. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/csv.py +0 -0
  26. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/print.py +0 -0
  27. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/mixin.py +0 -0
  28. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/py.typed +0 -0
  29. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/run.py +0 -0
  30. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/scan.py +0 -0
  31. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/spark/__init__.py +0 -0
  32. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/spark/base.py +0 -0
  33. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/spark/default.py +0 -0
  34. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/template.py +0 -0
  35. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
  36. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/Dockerfile +0 -0
  37. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
  38. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/README.md +0 -0
  39. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/account.ipynb +0 -0
  40. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
  41. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/examples/employee_hierarchy/employee_data.csv +0 -0
  42. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/examples/employee_hierarchy/entrypoint.py +0 -0
  43. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/jupyterlab.sh +0 -0
  44. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/payload/config.json +0 -0
  45. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
  46. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
  47. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/requirements.txt +0 -0
  48. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
  49. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/Dockerfile +0 -0
  50. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
  51. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/README.md +0 -0
  52. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/account.ipynb +0 -0
  53. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
  54. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
  55. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
  56. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
  57. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/payload/config.json +0 -0
  58. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
  59. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
  60. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/requirements.txt +0 -0
  61. {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: salesforce-data-customcode
3
- Version: 0.1.18
3
+ Version: 0.1.19
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -238,7 +238,7 @@ Instead of using `datacustomcode configure`, you can also set credentials via en
238
238
  |----------|-------------|
239
239
  | `SFDC_CLIENT_SECRET` | External Client App Client Secret |
240
240
  | `SFDC_REFRESH_TOKEN` | OAuth refresh token |
241
- | `SFDC_CORE_TOKEN` | (Optional) OAuth core/access token |
241
+ | `SFDC_ACCESS_TOKEN` | (Optional) OAuth core/access token |
242
242
 
243
243
  Example usage:
244
244
  ```bash
@@ -391,68 +391,7 @@ You now have all fields necessary for the `datacustomcode configure` command.
391
391
 
392
392
  ### Obtaining Refresh Token and Core Token
393
393
 
394
- If you're using OAuth Tokens authentication (instead of Username/Password), follow these steps to obtain your refresh token and core token (access token).
395
-
396
- #### Step 1: Note External Client App Details
397
-
398
- From your External Client App, note down the following:
399
- - **Client ID**
400
- - **Client Secret**
401
- - **Callback URL** (e.g., `http://localhost:55555/callback`)
402
-
403
- #### Step 2: Obtain Authorization Code
404
-
405
- 1. Open a browser and navigate to the following URL (replace placeholders with your values):
406
-
407
- ```
408
- <LOGIN_URL>/services/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>
409
- ```
410
-
411
- 2. After authenticating, you'll be redirected to your callback URL. The redirected URL will be in the form:
412
- ```
413
- <CALLBACK_URL>?code=<CODE>
414
- ```
415
-
416
- 3. Extract the `<CODE>` from the address bar. If the address bar doesn't show it, check the **Network tab** in your browser's developer tools.
417
-
418
- #### Step 3: Exchange Code for Tokens
419
-
420
- Make a POST request to exchange the authorization code for tokens. You can use `curl` or Postman:
421
-
422
- ```bash
423
- curl --location --request POST '<LOGIN_URL>/services/oauth2/token' \
424
- --header 'Content-Type: application/x-www-form-urlencoded' \
425
- --data-urlencode 'grant_type=authorization_code' \
426
- --data-urlencode 'code=<CODE>' \
427
- --data-urlencode 'client_id=<CLIENT_ID>' \
428
- --data-urlencode 'client_secret=<CLIENT_SECRET>' \
429
- --data-urlencode 'redirect_uri=<CALLBACK_URL>'
430
- ```
431
-
432
- The response will be a JSON object containing:
433
-
434
- ```json
435
- {
436
- "access_token": "<access_token>",
437
- "refresh_token": "<refresh_token>",
438
- "signature": "<signature>",
439
- "scope": "refresh_token cdp_query_api api cdp_profile_api cdp_api full",
440
- "id_token": "<id_token>",
441
- "instance_url": "https://your-instance.my.salesforce.com",
442
- "id": "https://login.salesforce.com/id/00DSB.../005SB...",
443
- "token_type": "Bearer",
444
- "issued_at": "1767743916187"
445
- }
446
- ```
447
-
448
- The key fields you need are:
449
- | Field | Description |
450
- |-------|-------------|
451
- | `access_token` | The **core token** (also called access token) |
452
- | `refresh_token` | The **refresh token** for obtaining new access tokens |
453
- | `instance_url` | Your Salesforce instance URL |
454
-
455
- Use the `refresh_token` value when running `datacustomcode configure` with OAuth Tokens authentication.
394
+ If you're using OAuth Tokens authentication, the initial configure will retrieve and store tokens. Run `datacustomcode auth` to refresh these when they expire.
456
395
 
457
396
  ## Other docs
458
397
 
@@ -214,7 +214,7 @@ Instead of using `datacustomcode configure`, you can also set credentials via en
214
214
  |----------|-------------|
215
215
  | `SFDC_CLIENT_SECRET` | External Client App Client Secret |
216
216
  | `SFDC_REFRESH_TOKEN` | OAuth refresh token |
217
- | `SFDC_CORE_TOKEN` | (Optional) OAuth core/access token |
217
+ | `SFDC_ACCESS_TOKEN` | (Optional) OAuth core/access token |
218
218
 
219
219
  Example usage:
220
220
  ```bash
@@ -367,68 +367,7 @@ You now have all fields necessary for the `datacustomcode configure` command.
367
367
 
368
368
  ### Obtaining Refresh Token and Core Token
369
369
 
370
- If you're using OAuth Tokens authentication (instead of Username/Password), follow these steps to obtain your refresh token and core token (access token).
371
-
372
- #### Step 1: Note External Client App Details
373
-
374
- From your External Client App, note down the following:
375
- - **Client ID**
376
- - **Client Secret**
377
- - **Callback URL** (e.g., `http://localhost:55555/callback`)
378
-
379
- #### Step 2: Obtain Authorization Code
380
-
381
- 1. Open a browser and navigate to the following URL (replace placeholders with your values):
382
-
383
- ```
384
- <LOGIN_URL>/services/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>
385
- ```
386
-
387
- 2. After authenticating, you'll be redirected to your callback URL. The redirected URL will be in the form:
388
- ```
389
- <CALLBACK_URL>?code=<CODE>
390
- ```
391
-
392
- 3. Extract the `<CODE>` from the address bar. If the address bar doesn't show it, check the **Network tab** in your browser's developer tools.
393
-
394
- #### Step 3: Exchange Code for Tokens
395
-
396
- Make a POST request to exchange the authorization code for tokens. You can use `curl` or Postman:
397
-
398
- ```bash
399
- curl --location --request POST '<LOGIN_URL>/services/oauth2/token' \
400
- --header 'Content-Type: application/x-www-form-urlencoded' \
401
- --data-urlencode 'grant_type=authorization_code' \
402
- --data-urlencode 'code=<CODE>' \
403
- --data-urlencode 'client_id=<CLIENT_ID>' \
404
- --data-urlencode 'client_secret=<CLIENT_SECRET>' \
405
- --data-urlencode 'redirect_uri=<CALLBACK_URL>'
406
- ```
407
-
408
- The response will be a JSON object containing:
409
-
410
- ```json
411
- {
412
- "access_token": "<access_token>",
413
- "refresh_token": "<refresh_token>",
414
- "signature": "<signature>",
415
- "scope": "refresh_token cdp_query_api api cdp_profile_api cdp_api full",
416
- "id_token": "<id_token>",
417
- "instance_url": "https://your-instance.my.salesforce.com",
418
- "id": "https://login.salesforce.com/id/00DSB.../005SB...",
419
- "token_type": "Bearer",
420
- "issued_at": "1767743916187"
421
- }
422
- ```
423
-
424
- The key fields you need are:
425
- | Field | Description |
426
- |-------|-------------|
427
- | `access_token` | The **core token** (also called access token) |
428
- | `refresh_token` | The **refresh token** for obtaining new access tokens |
429
- | `instance_url` | Your Salesforce instance URL |
430
-
431
- Use the `refresh_token` value when running `datacustomcode configure` with OAuth Tokens authentication.
370
+ If you're using OAuth Tokens authentication, the initial configure will retrieve and store tokens. Run `datacustomcode auth` to refresh these when they expire.
432
371
 
433
372
  ## Other docs
434
373
 
@@ -18,7 +18,7 @@ license = "Apache-2.0"
18
18
  name = "salesforce-data-customcode"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.10,<3.12"
21
- version = "0.1.18"
21
+ version = "0.1.19"
22
22
 
23
23
  [tool.black]
24
24
  exclude = '''
@@ -0,0 +1,257 @@
1
+ # Copyright (c) 2025, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import http.server
16
+ import queue
17
+ import socketserver
18
+ import threading
19
+ import time
20
+ from typing import Any
21
+ from urllib.parse import parse_qs, urlparse
22
+ import webbrowser
23
+
24
+ import click
25
+ import requests
26
+
27
+
28
+ class OAuthCallbackHandler(http.server.SimpleHTTPRequestHandler):
29
+ """HTTP request handler to capture OAuth callback."""
30
+
31
+ def __init__(self, *args, auth_code_queue=None, **kwargs):
32
+ self.auth_code_queue = auth_code_queue
33
+ super().__init__(*args, **kwargs)
34
+
35
+ def do_GET(self):
36
+ """Handle GET request from OAuth callback."""
37
+ parsed_path = urlparse(self.path)
38
+ query_params = parse_qs(parsed_path.query)
39
+
40
+ if "code" in query_params:
41
+ auth_code = query_params["code"][0]
42
+ self.auth_code_queue.put(auth_code)
43
+ self.send_response(200)
44
+ self.send_header("Content-type", "text/html")
45
+ self.end_headers()
46
+ self.wfile.write(
47
+ b"<html><body><h1>Authentication successful!</h1>"
48
+ b"<p>You can close this window and return to the terminal.</p>"
49
+ b"</body></html>"
50
+ )
51
+ elif "error" in query_params:
52
+ error = query_params["error"][0]
53
+ error_description = query_params.get("error_description", [""])[0]
54
+ self.auth_code_queue.put(f"ERROR:{error}:{error_description}")
55
+ self.send_response(400)
56
+ self.send_header("Content-type", "text/html")
57
+ self.end_headers()
58
+ self.wfile.write(
59
+ f"<html><body><h1>Authentication failed</h1>"
60
+ f"<p>Error: {error}</p>"
61
+ f"<p>{error_description}</p></body></html>".encode()
62
+ )
63
+ else:
64
+ self.send_response(400)
65
+ self.send_header("Content-type", "text/html")
66
+ self.end_headers()
67
+ self.wfile.write(b"<html><body><h1>Invalid callback</h1></body></html>")
68
+
69
+ def log_message(self, format, *args):
70
+ """Suppress default logging."""
71
+
72
+
73
+ def _run_oauth_callback_server(
74
+ redirect_uri: str, auth_code_queue: "queue.Queue[str]"
75
+ ) -> tuple[socketserver.TCPServer, int]:
76
+ """Start a local HTTP server to catch OAuth callback.
77
+
78
+ Args:
79
+ redirect_uri: The redirect URI configured in the OAuth app
80
+ auth_code_queue: Queue to put the authorization code in
81
+
82
+ Returns:
83
+ Tuple of (server instance, actual port number)
84
+ """
85
+ parsed_uri = urlparse(redirect_uri)
86
+ host = parsed_uri.hostname
87
+ port = parsed_uri.port
88
+ if not host or not port:
89
+ raise ValueError(f"Invalid redirect URI: {redirect_uri}")
90
+
91
+ # Create a custom handler factory
92
+ def handler_factory(*args, **kwargs):
93
+ return OAuthCallbackHandler(*args, auth_code_queue=auth_code_queue, **kwargs)
94
+
95
+ server = socketserver.TCPServer((host, port), handler_factory)
96
+ server.allow_reuse_address = True
97
+
98
+ def serve():
99
+ server.serve_forever()
100
+
101
+ server_thread = threading.Thread(target=serve, daemon=True)
102
+ server_thread.start()
103
+
104
+ # Wait a moment for server to start
105
+ time.sleep(0.5)
106
+
107
+ return server, port
108
+
109
+
110
+ def _exchange_code_for_tokens(
111
+ login_url: str,
112
+ client_id: str,
113
+ client_secret: str,
114
+ redirect_uri: str,
115
+ auth_code: str,
116
+ ) -> Any:
117
+ """Exchange authorization code for access and refresh tokens.
118
+
119
+ Args:
120
+ login_url: Salesforce login URL
121
+ client_id: OAuth client ID
122
+ client_secret: OAuth client secret
123
+ redirect_uri: Redirect URI used in authorization
124
+ auth_code: Authorization code from callback
125
+
126
+ Returns:
127
+ Dictionary containing access_token and refresh_token
128
+
129
+ Raises:
130
+ click.ClickException: If token exchange fails
131
+ """
132
+ token_url = f"{login_url.rstrip('/')}/services/oauth2/token"
133
+ data = {
134
+ "grant_type": "authorization_code",
135
+ "code": auth_code,
136
+ "client_id": client_id,
137
+ "client_secret": client_secret,
138
+ "redirect_uri": redirect_uri,
139
+ }
140
+
141
+ try:
142
+ response = requests.post(token_url, data=data, timeout=30)
143
+ response.raise_for_status()
144
+ return response.json()
145
+ except requests.exceptions.RequestException as e:
146
+ raise click.ClickException(
147
+ f"Failed to exchange authorization code for tokens: {e}"
148
+ ) from e
149
+
150
+
151
+ def do_oauth_browser_flow(
152
+ login_url: str, client_id: str, client_secret: str, redirect_uri: str
153
+ ) -> tuple[str, str]:
154
+ """Perform OAuth browser flow to obtain tokens.
155
+
156
+ Args:
157
+ login_url: Salesforce login URL
158
+ client_id: OAuth client ID
159
+ client_secret: OAuth client secret
160
+ redirect_uri: Redirect URI configured in OAuth app
161
+
162
+ Returns:
163
+ Tuple of (refresh_token, access_token)
164
+
165
+ Raises:
166
+ click.ClickException: If OAuth flow fails
167
+ """
168
+ # Create queue for communication between server and main thread
169
+ auth_code_queue: queue.Queue[str] = queue.Queue()
170
+
171
+ # Start callback server
172
+ click.echo(f"\nStarting local callback server on {redirect_uri}...")
173
+ server, actual_port = _run_oauth_callback_server(redirect_uri, auth_code_queue)
174
+
175
+ # Build authorization URL with final redirect_uri
176
+ auth_url = (
177
+ f"{login_url.rstrip('/')}/services/oauth2/authorize"
178
+ f"?response_type=code"
179
+ f"&client_id={client_id}"
180
+ f"&redirect_uri={redirect_uri}"
181
+ )
182
+
183
+ # Open browser
184
+ click.echo("Opening browser for authentication...")
185
+ click.echo(f"If the browser doesn't open automatically, visit:\n{auth_url}\n")
186
+ webbrowser.open(auth_url)
187
+
188
+ # Wait for callback (with timeout)
189
+ click.echo("Waiting for authentication...")
190
+ try:
191
+ result = auth_code_queue.get(timeout=60) # 1 minute timeout
192
+ except queue.Empty:
193
+ server.shutdown()
194
+ raise click.ClickException(
195
+ "Authentication timeout. Please try again."
196
+ ) from None
197
+
198
+ # Shutdown server
199
+ server.shutdown()
200
+
201
+ # Check for errors
202
+ if result.startswith("ERROR:"):
203
+ _, error, error_description = result.split(":", 2)
204
+ raise click.ClickException(f"OAuth error: {error}. {error_description}")
205
+
206
+ auth_code = result
207
+
208
+ # Exchange code for tokens
209
+ click.echo("Exchanging authorization code for tokens...")
210
+ token_response = _exchange_code_for_tokens(
211
+ login_url, client_id, client_secret, redirect_uri, auth_code
212
+ )
213
+
214
+ refresh_token = token_response.get("refresh_token")
215
+ access_token = token_response.get("access_token")
216
+
217
+ if not refresh_token:
218
+ raise click.ClickException(
219
+ "No refresh_token in response. Please check your OAuth app configuration."
220
+ )
221
+
222
+ return refresh_token, access_token
223
+
224
+
225
+ def configure_oauth_tokens(
226
+ login_url: str,
227
+ client_id: str,
228
+ client_secret: str,
229
+ redirect_uri: str,
230
+ profile: str,
231
+ ) -> None:
232
+ """Configure credentials for OAuth Tokens authentication."""
233
+ from datacustomcode.credentials import AuthType, Credentials
234
+
235
+ # Perform OAuth browser flow
236
+ try:
237
+ refresh_token, access_token = do_oauth_browser_flow(
238
+ login_url, client_id, client_secret, redirect_uri
239
+ )
240
+ except click.ClickException as e:
241
+ click.secho(f"Error: {e}", fg="red")
242
+ raise click.Abort() from None
243
+
244
+ credentials = Credentials(
245
+ login_url=login_url,
246
+ client_id=client_id,
247
+ auth_type=AuthType.OAUTH_TOKENS,
248
+ client_secret=client_secret,
249
+ refresh_token=refresh_token,
250
+ access_token=access_token,
251
+ redirect_uri=redirect_uri,
252
+ )
253
+ credentials.update_ini(profile=profile)
254
+ click.secho(
255
+ f"OAuth Tokens credentials saved to profile '{profile}' successfully",
256
+ fg="green",
257
+ )
@@ -21,6 +21,8 @@ from typing import List, Union
21
21
  import click
22
22
  from loguru import logger
23
23
 
24
+ from datacustomcode import AuthType
25
+ from datacustomcode.auth import configure_oauth_tokens
24
26
  from datacustomcode.scan import find_base_directory, get_package_type
25
27
 
26
28
 
@@ -45,37 +47,6 @@ def version():
45
47
  click.echo("Version information not available")
46
48
 
47
49
 
48
- def _configure_oauth_tokens(
49
- login_url: str,
50
- client_id: str,
51
- profile: str,
52
- ) -> None:
53
- """Configure credentials for OAuth Tokens authentication."""
54
- from datacustomcode.credentials import AuthType, Credentials
55
-
56
- client_secret = click.prompt("Client Secret")
57
- refresh_token = click.prompt("Refresh Token")
58
- core_token = click.prompt(
59
- "Core Token (optional, press Enter to skip)",
60
- default="",
61
- show_default=False,
62
- )
63
-
64
- credentials = Credentials(
65
- login_url=login_url,
66
- client_id=client_id,
67
- auth_type=AuthType.OAUTH_TOKENS,
68
- client_secret=client_secret,
69
- refresh_token=refresh_token,
70
- core_token=core_token if core_token else None,
71
- )
72
- credentials.update_ini(profile=profile)
73
- click.secho(
74
- f"OAuth Tokens credentials saved to profile '{profile}' successfully",
75
- fg="green",
76
- )
77
-
78
-
79
50
  def _configure_client_credentials(
80
51
  login_url: str,
81
52
  client_id: str,
@@ -123,11 +94,37 @@ def configure(profile: str, auth_type: str) -> None:
123
94
 
124
95
  # Route to appropriate handler based on auth type
125
96
  if auth_type == AuthType.OAUTH_TOKENS.value:
126
- _configure_oauth_tokens(login_url, client_id, profile)
97
+ client_secret = click.prompt("Client Secret", hide_input=True)
98
+ redirect_uri = click.prompt("Redirect URI")
99
+ configure_oauth_tokens(
100
+ login_url, client_id, client_secret, redirect_uri, profile
101
+ )
127
102
  elif auth_type == AuthType.CLIENT_CREDENTIALS.value:
128
103
  _configure_client_credentials(login_url, client_id, profile)
129
104
 
130
105
 
106
+ @cli.command()
107
+ @click.option("--profile", default="default", help="Credential profile name")
108
+ def auth(profile: str):
109
+ from datacustomcode.credentials import Credentials
110
+
111
+ credentials = Credentials.from_available(profile=profile)
112
+ if not credentials.redirect_uri:
113
+ click.secho(
114
+ "Error: Redirect URI is required for OAuth Tokens authentication",
115
+ fg="red",
116
+ )
117
+ raise click.Abort()
118
+ if credentials.auth_type == AuthType.OAUTH_TOKENS:
119
+ configure_oauth_tokens(
120
+ login_url=credentials.login_url,
121
+ client_id=credentials.client_id,
122
+ client_secret=credentials.client_secret,
123
+ redirect_uri=credentials.redirect_uri,
124
+ profile=profile,
125
+ )
126
+
127
+
131
128
  @cli.command()
132
129
  @click.argument("path", default="payload")
133
130
  @click.option("--network", default="default")
@@ -32,23 +32,6 @@ class AuthType(str, Enum):
32
32
  CLIENT_CREDENTIALS = "client_credentials"
33
33
 
34
34
 
35
- # Environment variable mappings for each auth type
36
- ENV_CREDENTIALS_COMMON = {
37
- "login_url": "SFDC_LOGIN_URL",
38
- "client_id": "SFDC_CLIENT_ID",
39
- }
40
-
41
- ENV_CREDENTIALS_OAUTH_TOKENS = {
42
- "client_secret": "SFDC_CLIENT_SECRET",
43
- "refresh_token": "SFDC_REFRESH_TOKEN",
44
- "core_token": "SFDC_CORE_TOKEN",
45
- }
46
-
47
- ENV_CREDENTIALS_CLIENT_CREDENTIALS = {
48
- "client_secret": "SFDC_CLIENT_SECRET",
49
- }
50
-
51
-
52
35
  @dataclass
53
36
  class Credentials:
54
37
  """Flexible credentials supporting multiple authentication methods.
@@ -61,14 +44,13 @@ class Credentials:
61
44
  # Required for all auth types
62
45
  login_url: str
63
46
  client_id: str
47
+ client_secret: str
64
48
  auth_type: AuthType = field(default=AuthType.OAUTH_TOKENS)
65
49
 
66
- # Common field
67
- client_secret: Optional[str] = None
68
-
69
50
  # OAuth Tokens flow fields
70
- core_token: Optional[str] = None
51
+ access_token: Optional[str] = None
71
52
  refresh_token: Optional[str] = None
53
+ redirect_uri: Optional[str] = None
72
54
 
73
55
  def __post_init__(self):
74
56
  """Validate credentials based on auth_type."""
@@ -135,10 +117,11 @@ class Credentials:
135
117
  login_url=section["login_url"],
136
118
  client_id=section["client_id"],
137
119
  auth_type=auth_type,
138
- client_secret=section.get("client_secret"),
120
+ client_secret=section["client_secret"],
139
121
  # OAuth Tokens fields
140
- core_token=section.get("core_token"),
122
+ access_token=section.get("access_token"),
141
123
  refresh_token=section.get("refresh_token"),
124
+ redirect_uri=section.get("redirect_uri"),
142
125
  )
143
126
 
144
127
  @classmethod
@@ -154,7 +137,7 @@ class Credentials:
154
137
  For oauth_tokens (default):
155
138
  SFDC_CLIENT_SECRET: External Client App client secret
156
139
  SFDC_REFRESH_TOKEN: OAuth refresh token
157
- SFDC_CORE_TOKEN: OAuth core/access token (optional)
140
+ SFDC_ACCESS_TOKEN: OAuth access token (optional)
158
141
 
159
142
  Returns:
160
143
  Credentials instance loaded from environment variables
@@ -185,10 +168,11 @@ class Credentials:
185
168
  login_url=login_url,
186
169
  client_id=client_id,
187
170
  auth_type=auth_type,
188
- client_secret=os.environ.get("SFDC_CLIENT_SECRET"),
171
+ client_secret=os.environ["SFDC_CLIENT_SECRET"],
189
172
  # OAuth Tokens fields
190
- core_token=os.environ.get("SFDC_CORE_TOKEN"),
173
+ access_token=os.environ.get("SFDC_ACCESS_TOKEN"),
191
174
  refresh_token=os.environ.get("SFDC_REFRESH_TOKEN"),
175
+ redirect_uri=os.environ.get("SFDC_REDIRECT_URI"),
192
176
  )
193
177
 
194
178
  @classmethod
@@ -245,24 +229,36 @@ class Credentials:
245
229
  config[profile]["auth_type"] = self.auth_type.value
246
230
  config[profile]["login_url"] = self.login_url
247
231
  config[profile]["client_id"] = self.client_id
248
-
232
+ config[profile]["client_secret"] = self.client_secret
249
233
  # Save fields based on auth type
250
234
  if self.auth_type == AuthType.OAUTH_TOKENS:
251
- config[profile]["client_secret"] = self.client_secret or ""
252
235
  config[profile]["refresh_token"] = self.refresh_token or ""
253
- if self.core_token:
254
- config[profile]["core_token"] = self.core_token
236
+ config[profile]["redirect_uri"] = self.redirect_uri or ""
237
+ if self.access_token:
238
+ config[profile]["access_token"] = self.access_token
255
239
  # Remove fields from other auth types
256
240
  for key in ["username", "password"]:
257
241
  config[profile].pop(key, None)
258
242
 
259
243
  elif self.auth_type == AuthType.CLIENT_CREDENTIALS:
260
- config[profile]["client_secret"] = self.client_secret or ""
261
244
  # Remove fields from other auth types
262
- for key in ["username", "password", "refresh_token", "core_token"]:
245
+ for key in [
246
+ "username",
247
+ "password",
248
+ "refresh_token",
249
+ "access_token",
250
+ "redirect_uri",
251
+ ]:
263
252
  config[profile].pop(key, None)
264
253
 
265
254
  with open(expanded_ini_file, "w") as f:
266
255
  config.write(f)
267
256
 
257
+ # Set secure file permissions (0o600 - readable/writable by owner only)
258
+ try:
259
+ os.chmod(expanded_ini_file, 0o600)
260
+ except OSError:
261
+ # Ignore errors if we can't set file permissions (e.g., on Windows)
262
+ pass
263
+
268
264
  logger.debug(f"Saved credentials to {expanded_ini_file} [{profile}]")