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.
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/PKG-INFO +3 -64
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/README.md +2 -63
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/pyproject.toml +1 -1
- salesforce_data_customcode-0.1.19/src/datacustomcode/auth.py +257 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/cli.py +29 -32
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/credentials.py +28 -32
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/LICENSE.txt +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/__init__.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/client.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/cmd.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/config.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/config.yaml +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/deploy.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/__init__.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/base.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/path/__init__.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/file/path/default.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/__init__.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/base.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/reader/__init__.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/reader/base.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/reader/query_api.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/__init__.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/base.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/csv.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/io/writer/print.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/mixin.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/py.typed +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/run.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/scan.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/spark/__init__.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/spark/base.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/spark/default.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/template.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/Dockerfile +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/README.md +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/account.ipynb +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/jupyterlab.sh +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/payload/config.json +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/function/requirements.txt +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/Dockerfile +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/README.md +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/account.ipynb +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/payload/config.json +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
- {salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/templates/script/requirements.txt +0 -0
- {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.
|
|
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
|
-
| `
|
|
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
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
|
@@ -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
|
+
)
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/cli.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
120
|
+
client_secret=section["client_secret"],
|
|
139
121
|
# OAuth Tokens fields
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
171
|
+
client_secret=os.environ["SFDC_CLIENT_SECRET"],
|
|
189
172
|
# OAuth Tokens fields
|
|
190
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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 [
|
|
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}]")
|
|
File without changes
|
|
File without changes
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/client.py
RENAMED
|
File without changes
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/cmd.py
RENAMED
|
File without changes
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/deploy.py
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
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/mixin.py
RENAMED
|
File without changes
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/py.typed
RENAMED
|
File without changes
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/run.py
RENAMED
|
File without changes
|
{salesforce_data_customcode-0.1.18 → salesforce_data_customcode-0.1.19}/src/datacustomcode/scan.py
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|