launchpy 0.5.2__tar.gz → 0.5.3__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.
- {launchpy-0.5.2 → launchpy-0.5.3}/PKG-INFO +25 -1
- {launchpy-0.5.2 → launchpy-0.5.3}/README.md +24 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/cli.md +43 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/getstarted.md +55 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/main.md +11 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/releases.md +8 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/__init__.py +7 -3
- launchpy-0.5.3/launchpy/__version__.py +1 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/cli/__main__.py +21 -16
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/configs.py +33 -5
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/connector.py +6 -1
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy.egg-info/PKG-INFO +25 -1
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy.egg-info/SOURCES.txt +2 -1
- {launchpy-0.5.2 → launchpy-0.5.3}/tests/test_connector.py +1 -0
- launchpy-0.5.3/tests/test_proxies.py +337 -0
- launchpy-0.5.2/launchpy/__version__.py +0 -1
- {launchpy-0.5.2 → launchpy-0.5.3}/LICENSE +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/admin.md +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/library.md +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/property.md +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/synchronizer.md +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/docs/translator.md +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/admin.py +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/cli/__init__.py +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/config.py +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/launchpy.py +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/library.py +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/property.py +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy/synchronizer.py +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy.egg-info/dependency_links.txt +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy.egg-info/entry_points.txt +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy.egg-info/requires.txt +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/launchpy.egg-info/top_level.txt +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/pyproject.toml +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/requirements.txt +0 -0
- {launchpy-0.5.2 → launchpy-0.5.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: launchpy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Adobe Launch API wrapper
|
|
5
5
|
Author-email: Julien Piccini <piccini.julien@gmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -71,6 +71,30 @@ Most of the documentation has been imported from the datanalyst website [here](.
|
|
|
71
71
|
|
|
72
72
|
You can find release information [here](./docs/releases.md).
|
|
73
73
|
|
|
74
|
+
## Proxy support
|
|
75
|
+
|
|
76
|
+
Pass a `proxy` URL to `launchpy.configure(...)`, or add `"proxy"` to
|
|
77
|
+
your JSON configuration file. The same proxy serves both HTTP and HTTPS.
|
|
78
|
+
Proxy settings apply to OAuth authentication,
|
|
79
|
+
API requests, and asynchronous property exports, with separate settings per
|
|
80
|
+
organization.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
launchpy.configure(
|
|
84
|
+
org_id="your-org@AdobeOrg",
|
|
85
|
+
client_id="your-client-id",
|
|
86
|
+
secret="your-secret",
|
|
87
|
+
scopes="your-scopes",
|
|
88
|
+
proxy="http://proxy.example.com:8080",
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The CLI accepts `--proxy URL` at startup and in the interactive `config`
|
|
93
|
+
command. An explicit CLI proxy overrides the file setting.
|
|
94
|
+
Without an explicit proxy, existing environment-based proxy behavior is unchanged.
|
|
95
|
+
See the [Python and JSON examples](./docs/getstarted.md#proxy-configuration)
|
|
96
|
+
and [CLI examples](./docs/cli.md#proxy-configuration).
|
|
97
|
+
|
|
74
98
|
## HTTP retries
|
|
75
99
|
|
|
76
100
|
The `AdobeRequest` connector always retries HTTP 429 responses for GET, POST,
|
|
@@ -40,6 +40,30 @@ Most of the documentation has been imported from the datanalyst website [here](.
|
|
|
40
40
|
|
|
41
41
|
You can find release information [here](./docs/releases.md).
|
|
42
42
|
|
|
43
|
+
## Proxy support
|
|
44
|
+
|
|
45
|
+
Pass a `proxy` URL to `launchpy.configure(...)`, or add `"proxy"` to
|
|
46
|
+
your JSON configuration file. The same proxy serves both HTTP and HTTPS.
|
|
47
|
+
Proxy settings apply to OAuth authentication,
|
|
48
|
+
API requests, and asynchronous property exports, with separate settings per
|
|
49
|
+
organization.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
launchpy.configure(
|
|
53
|
+
org_id="your-org@AdobeOrg",
|
|
54
|
+
client_id="your-client-id",
|
|
55
|
+
secret="your-secret",
|
|
56
|
+
scopes="your-scopes",
|
|
57
|
+
proxy="http://proxy.example.com:8080",
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The CLI accepts `--proxy URL` at startup and in the interactive `config`
|
|
62
|
+
command. An explicit CLI proxy overrides the file setting.
|
|
63
|
+
Without an explicit proxy, existing environment-based proxy behavior is unchanged.
|
|
64
|
+
See the [Python and JSON examples](./docs/getstarted.md#proxy-configuration)
|
|
65
|
+
and [CLI examples](./docs/cli.md#proxy-configuration).
|
|
66
|
+
|
|
43
67
|
## HTTP retries
|
|
44
68
|
|
|
45
69
|
The `AdobeRequest` connector always retries HTTP 429 responses for GET, POST,
|
|
@@ -10,6 +10,7 @@ This document provides an overview of the command line interface (CLI) for `laun
|
|
|
10
10
|
- [Instantiation](#instantiation)
|
|
11
11
|
- [create\_config\_file](#create_config_file)
|
|
12
12
|
- [config](#config)
|
|
13
|
+
- [Proxy configuration](#proxy-configuration)
|
|
13
14
|
- [change\_org](#change_org)
|
|
14
15
|
- [get\_properties](#get_properties)
|
|
15
16
|
- [create\_property](#create_property)
|
|
@@ -148,6 +149,7 @@ Arguments
|
|
|
148
149
|
`-cf`,`--config_file`: The path to the config file that contains your credentials and other necessary information.
|
|
149
150
|
`-p`,`--property` : The property name to be used and directly instantiated the property layer. This is an optional parameter.
|
|
150
151
|
`-on`,`--org_name`: The organization name to use when your `--config_file` is a multi-org config file (see [create_config_file](#create_config_file)). This is an optional parameter, it defaults to the first organization found in the file (or `default` when using individual parameters).
|
|
152
|
+
`--proxy`: Proxy URL for both HTTP and HTTPS destinations.
|
|
151
153
|
|
|
152
154
|
Example via parameters:
|
|
153
155
|
```bash
|
|
@@ -195,6 +197,47 @@ launchpy:client_a>
|
|
|
195
197
|
When in a non-multi-org environment, the prompt of the CLI will be `launchpy>`, and when in a multi-org environment, the prompt will be `launchpy:<org_name>`, where `<org_name>` is the name of the organization you are currently connected to.\
|
|
196
198
|
By default, the first organization found in the multi-org config file will be used if no `--org_name` is provided. You can switch between organizations using the `change_org` command.
|
|
197
199
|
|
|
200
|
+
#### Proxy configuration
|
|
201
|
+
|
|
202
|
+
The single `--proxy` option works both at startup and in the interactive `config` command,
|
|
203
|
+
with either a config file or individual credentials:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
python -m launchpy.cli --config_file config.json --proxy http://proxy.example.com:8080
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
(Cmd) config --config_file config.json --proxy http://proxy.example.com:8080
|
|
211
|
+
(Cmd) config --client_id <client_id> --secret <secret> --org_id <org_id> --scopes <scopes> --proxy http://proxy.example.com:8080
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
You can also store a single `proxy` URL in the JSON file:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"org_id": "your-org@AdobeOrg",
|
|
219
|
+
"client_id": "your-client-id",
|
|
220
|
+
"secret": "your-secret",
|
|
221
|
+
"scopes": "your-scopes",
|
|
222
|
+
"proxy": "http://proxy.example.com:8080"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The same proxy is used for both HTTP and HTTPS destinations. `--proxy` overrides
|
|
227
|
+
the file's proxy URL. For multi-org files, put `proxy` in each organization's
|
|
228
|
+
object; `--proxy` overrides it for every organization loaded from that file.
|
|
229
|
+
`change_org` preserves each organization's effective proxy settings.
|
|
230
|
+
|
|
231
|
+
The proxy covers authentication, token refresh, API calls, and property
|
|
232
|
+
exports. Existing files and commands without proxy options continue to work,
|
|
233
|
+
including environment-based proxy settings. Omitting `proxy` or setting it to
|
|
234
|
+
`null` in JSON does not disable environment proxies. Empty strings and non-string
|
|
235
|
+
values are rejected. TLS verification remains
|
|
236
|
+
enabled. For URL schemes, authentication, and Python configuration examples,
|
|
237
|
+
see [Proxy configuration in Getting Started](./getstarted.md#proxy-configuration).
|
|
238
|
+
Avoid putting proxy credentials on a shared terminal's command line or in
|
|
239
|
+
committed configuration files.
|
|
240
|
+
|
|
198
241
|
#### change_org
|
|
199
242
|
Switch the current CLI session to another organization that has already been loaded from a multi-org config file (via `config` or `--config_file` at startup). This avoids re-running `config` when you need to move between organizations.\
|
|
200
243
|
Arguments:
|
|
@@ -101,6 +101,61 @@ So your config file will look like this:
|
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
### Proxy configuration
|
|
105
|
+
|
|
106
|
+
To connect through a proxy, pass the optional `proxy` URL to `configure`.
|
|
107
|
+
The same proxy is used for both HTTP and HTTPS:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
lp.configure(
|
|
111
|
+
org_id="your-org@AdobeOrg",
|
|
112
|
+
client_id="your-client-id",
|
|
113
|
+
secret="your-secret",
|
|
114
|
+
scopes="your-scopes",
|
|
115
|
+
proxy="http://proxy.example.com:8080",
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Alternatively, add `proxy` to your configuration JSON and load it with
|
|
120
|
+
`lp.importConfigFile("myconfig.json")` as usual:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"org_id": "your-org@AdobeOrg",
|
|
125
|
+
"client_id": "your-client-id",
|
|
126
|
+
"secret": "your-secret",
|
|
127
|
+
"scopes": "your-scopes",
|
|
128
|
+
"proxy": "http://proxy.example.com:8080"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The proxy URL includes its scheme and port.
|
|
133
|
+
An HTTPS destination commonly uses an `http://` proxy
|
|
134
|
+
URL, which tunnels HTTPS with CONNECT; use `https://` only if the proxy itself
|
|
135
|
+
requires TLS. TLS certificate verification remains enabled.
|
|
136
|
+
|
|
137
|
+
For proxy authentication, use `http://username:password@proxy.example.com:8080`,
|
|
138
|
+
URL-encoding special characters in the username and password. Treat files and
|
|
139
|
+
command-line arguments containing proxy credentials as secrets.
|
|
140
|
+
|
|
141
|
+
In a multi-org JSON file, put `proxy` inside each organization's object.
|
|
142
|
+
Organizations can use different proxies, or omit the field independently.
|
|
143
|
+
To override the file's proxy for every imported organization, use
|
|
144
|
+
`lp.importConfigFile("myconfig.json", proxy="http://other-proxy:8080")`.
|
|
145
|
+
|
|
146
|
+
The settings apply to initial OAuth token acquisition, token refresh, all API
|
|
147
|
+
methods and their retries, and asynchronous rule-component downloads during
|
|
148
|
+
`extractProperty`. Configure before creating `Admin`, `Property`, `Library`,
|
|
149
|
+
or `Synchronizer` instances; existing connectors retain their configuration.
|
|
150
|
+
Calling `configure` again without `proxy` clears the previous explicit
|
|
151
|
+
proxy for that organization.
|
|
152
|
+
|
|
153
|
+
Omitting `proxy` or passing `None` preserves the HTTP libraries'
|
|
154
|
+
normal environment-based behavior (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`,
|
|
155
|
+
and `NO_PROXY`). This does **not** disable environment proxies.
|
|
156
|
+
Empty strings and non-string proxy values are rejected.
|
|
157
|
+
Existing configuration files without a proxy require no changes.
|
|
158
|
+
|
|
104
159
|
## 5. Instantiate the Admin class & get Company ID to retrieve properties
|
|
105
160
|
|
|
106
161
|
Once all of these setup steps are completed, you can instantiate the `Admin` class.
|
|
@@ -37,6 +37,11 @@ launchpy.importConfigFile('myConfig.json')
|
|
|
37
37
|
launchpy.config.config_object
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Configuration JSON files can include an optional `proxy` URL, independently
|
|
41
|
+
for each organization in multi-org files. `importConfigFile` also accepts
|
|
42
|
+
`proxy=...` to override the file's proxy for all imported organizations.
|
|
43
|
+
See [Proxy configuration](./getstarted.md#proxy-configuration) for examples.
|
|
44
|
+
|
|
40
45
|
### Configure method
|
|
41
46
|
|
|
42
47
|
The configure method enables you to configure the `config_object` directly from the python console with environment.\
|
|
@@ -56,6 +61,12 @@ lp.configure(
|
|
|
56
61
|
)
|
|
57
62
|
```
|
|
58
63
|
|
|
64
|
+
`configure` also accepts `proxy="http://proxy.example.com:8080"`.
|
|
65
|
+
This single URL is used for both HTTP and HTTPS destinations. These settings
|
|
66
|
+
apply to OAuth, API requests, and property exports. Omit the argument to retain
|
|
67
|
+
normal environment-based proxy behavior; configuring an organization again
|
|
68
|
+
without it clears that organization's previous explicit proxy.
|
|
69
|
+
|
|
59
70
|
## 1. The Admin class
|
|
60
71
|
|
|
61
72
|
The Admin methods helps your retrieving vital information to use the API later, such as the companyId.\
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
This page gathered the changes made between version of the launchpy module.\
|
|
4
4
|
This has been started after the 0.3.0 release.\
|
|
5
5
|
|
|
6
|
+
## 0.5.3
|
|
7
|
+
* Adding optional `proxy` support to `configure`, `importConfigFile`, `createConfigFile` and JSON configuration files. A single proxy URL is used for both HTTP and HTTPS, with separate settings per organization.
|
|
8
|
+
* Applying proxy settings to OAuth token acquisition and refresh, all API requests and their retries, and asynchronous rule-component downloads during `extractProperty`.
|
|
9
|
+
* Adding `--proxy` to CLI startup and the interactive `config` command. Explicit CLI and `importConfigFile` proxy arguments override file settings for all organizations loaded from the file.
|
|
10
|
+
* Preserving environment-based proxy behavior when no explicit proxy is supplied. Existing configuration files require no changes; empty strings and non-string proxy values are rejected.
|
|
11
|
+
* Fixing CLI configuration state and organization selection when `--org_name` is omitted. `change_org` now uses the shared connection setup and preserves each organization's proxy settings.
|
|
12
|
+
* Adding proxy configuration examples and documentation for Python, JSON, and CLI usage.
|
|
13
|
+
|
|
6
14
|
## 0.5.2
|
|
7
15
|
* Fixing recursive GET retries and always retrying HTTP 429 responses for GET, POST, PUT, PATCH, and DELETE.
|
|
8
16
|
* Respecting `Retry-After` seconds and HTTP dates, with exponential backoff when the header is absent or invalid.
|
|
@@ -5,6 +5,7 @@ May 15 12:04:49 2019
|
|
|
5
5
|
"""
|
|
6
6
|
from launchpy.__version__ import __version__
|
|
7
7
|
from launchpy.configs import *
|
|
8
|
+
from launchpy.configs import _validate_proxy
|
|
8
9
|
from launchpy.library import Library
|
|
9
10
|
from launchpy.admin import Admin
|
|
10
11
|
from launchpy.property import Property
|
|
@@ -42,10 +43,13 @@ async def __extractRuleComponents__(rule: dict, client: httpx.AsyncClient, folde
|
|
|
42
43
|
json.dump(component, f, indent=4)
|
|
43
44
|
|
|
44
45
|
# 3. Use an orchestrator instead of ThreadPoolExecutor
|
|
45
|
-
async def process_all_rules(rules_list, folder, header
|
|
46
|
+
async def process_all_rules(rules_list, folder, header,
|
|
47
|
+
proxy: str | None = None):
|
|
48
|
+
proxy = _validate_proxy(proxy)
|
|
46
49
|
timeout = httpx.Timeout(10.0, read=120.0)
|
|
47
50
|
limits = httpx.Limits(max_connections=100, max_keepalive_connections=50)
|
|
48
|
-
|
|
51
|
+
proxy_kwargs = {"proxy": proxy} if proxy is not None else {}
|
|
52
|
+
async with httpx.AsyncClient(timeout=timeout, limits=limits, **proxy_kwargs) as client:
|
|
49
53
|
# Create a list of "tasks" to run
|
|
50
54
|
tasks = [__extractRuleComponents__(rule, client, folder, header) for rule in rules_list]
|
|
51
55
|
# 'gather' runs them all concurrently
|
|
@@ -90,7 +94,7 @@ def extractProperty(property: dict | Property, publishedVersion: bool = False,fo
|
|
|
90
94
|
except Exception as e:
|
|
91
95
|
published_rules.append(rule)
|
|
92
96
|
rules = published_rules
|
|
93
|
-
asyncio.run(process_all_rules(rules, folder, property.header))
|
|
97
|
+
asyncio.run(process_all_rules(rules, folder, property.header, property.connector.proxy))
|
|
94
98
|
dataElements = property.getDataElements()
|
|
95
99
|
if publishedVersion:
|
|
96
100
|
published_data_elements = []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.3"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import launchpy
|
|
2
2
|
from launchpy.config import config_objects, headers
|
|
3
|
+
from launchpy.configs import _validate_proxy
|
|
3
4
|
import argparse, cmd, shlex, json
|
|
4
5
|
from functools import wraps
|
|
5
6
|
from rich.console import Console
|
|
@@ -961,6 +962,7 @@ class MainShell(cmd.Cmd):
|
|
|
961
962
|
|
|
962
963
|
def __init__(self, **kwargs:ParamSpecKwargs) -> None:
|
|
963
964
|
super().__init__()
|
|
965
|
+
proxy = _validate_proxy(kwargs.get("proxy"))
|
|
964
966
|
self.admin = None
|
|
965
967
|
self.cid = None
|
|
966
968
|
self.properties = None
|
|
@@ -988,12 +990,13 @@ class MainShell(cmd.Cmd):
|
|
|
988
990
|
with open(config_path, "rb") as f:
|
|
989
991
|
dict_config = json.load(f)
|
|
990
992
|
if type(dict_config) == dict:
|
|
991
|
-
config_key = kwargs.get("org_name"
|
|
993
|
+
config_key = kwargs.get("org_name") or "default"
|
|
992
994
|
self.config = {config_key: deepcopy(self.config_template)}
|
|
993
995
|
self.config[config_key]['secret'] = dict_config.get("secret", kwargs.get("secret"))
|
|
994
996
|
self.config[config_key]['org_id'] = dict_config.get("org_id", kwargs.get("org_id"))
|
|
995
997
|
self.config[config_key]['client_id'] = dict_config.get("client_id", dict_config.get("api_key", kwargs.get("client_id", kwargs.get("api_key"))))
|
|
996
998
|
self.config[config_key]['scopes'] = dict_config.get("scopes", kwargs.get("scopes"))
|
|
999
|
+
self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(dict_config.get("proxy"))
|
|
997
1000
|
config_objects.update(self.config)
|
|
998
1001
|
elif type(dict_config) == list:
|
|
999
1002
|
self.config = {}
|
|
@@ -1007,15 +1010,17 @@ class MainShell(cmd.Cmd):
|
|
|
1007
1010
|
self.config[config_key]['org_id'] = config.get("org_id", kwargs.get("org_id"))
|
|
1008
1011
|
self.config[config_key]['client_id'] = config.get("client_id", config.get("api_key", kwargs.get("client_id", kwargs.get("api_key"))))
|
|
1009
1012
|
self.config[config_key]['scopes'] = config.get("scopes", kwargs.get("scopes"))
|
|
1013
|
+
self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(config.get("proxy"))
|
|
1010
1014
|
config_objects.update(self.config)
|
|
1011
1015
|
config_key = dict_config[0].get("org_name", "default")
|
|
1012
1016
|
else:
|
|
1013
|
-
config_key = kwargs.get("org_name"
|
|
1017
|
+
config_key = kwargs.get("org_name") or "default"
|
|
1014
1018
|
self.config = {config_key: deepcopy(self.config_template)}
|
|
1015
1019
|
self.config[config_key]['secret'] = kwargs.get("secret")
|
|
1016
1020
|
self.config[config_key]['org_id'] = kwargs.get("org_id")
|
|
1017
1021
|
self.config[config_key]['client_id'] = kwargs.get("client_id",kwargs.get("api_key"))
|
|
1018
1022
|
self.config[config_key]['scopes'] = kwargs.get("scopes")
|
|
1023
|
+
self.config[config_key]['proxy'] = proxy
|
|
1019
1024
|
config_objects.update(self.config)
|
|
1020
1025
|
if self.config[config_key]['secret'] is not None and self.config[config_key]['org_id'] is not None and self.config[config_key]['client_id'] is not None and self.config[config_key]['scopes'] is not None:
|
|
1021
1026
|
myheaders = {}
|
|
@@ -1024,7 +1029,7 @@ class MainShell(cmd.Cmd):
|
|
|
1024
1029
|
myheaders[org_name]["x-gw-ims-org-id"] = config['org_id']
|
|
1025
1030
|
myheaders[org_name]["x-api-key"] = config['client_id']
|
|
1026
1031
|
headers.update(myheaders)
|
|
1027
|
-
config_key = kwargs.get("org_name"
|
|
1032
|
+
config_key = kwargs.get("org_name") or config_key
|
|
1028
1033
|
console.print("Configuring connection...", style="blue")
|
|
1029
1034
|
self._configure_connection(config_key)
|
|
1030
1035
|
if kwargs.get("property") is not None:
|
|
@@ -1051,13 +1056,15 @@ class MainShell(cmd.Cmd):
|
|
|
1051
1056
|
else:
|
|
1052
1057
|
config_key = list(self.config.keys())[0]
|
|
1053
1058
|
self.org_name = config_key
|
|
1054
|
-
|
|
1059
|
+
launchpy.configure(
|
|
1055
1060
|
secret=config_objects[config_key]['secret'],
|
|
1056
1061
|
org_id=config_objects[config_key]['org_id'],
|
|
1057
1062
|
client_id=config_objects[config_key]['client_id'],
|
|
1058
1063
|
scopes=config_objects[config_key]['scopes'],
|
|
1059
|
-
org_name=config_key
|
|
1064
|
+
org_name=config_key,
|
|
1065
|
+
proxy=config_objects[config_key].get("proxy")
|
|
1060
1066
|
)
|
|
1067
|
+
self.config = config_objects
|
|
1061
1068
|
self.admin = launchpy.Admin(org_name=config_key)
|
|
1062
1069
|
self.cid = self.admin.getCompanyId()
|
|
1063
1070
|
if config_key == 'default' or config_key is None:
|
|
@@ -1075,16 +1082,7 @@ class MainShell(cmd.Cmd):
|
|
|
1075
1082
|
if org_name not in config_objects.keys():
|
|
1076
1083
|
console.print(f"(!) Error: Organization '{org_name}' not found in the configuration.", style="red")
|
|
1077
1084
|
return
|
|
1078
|
-
self.
|
|
1079
|
-
secret=config_objects[org_name]['secret'],
|
|
1080
|
-
org_id=config_objects[org_name]['org_id'],
|
|
1081
|
-
client_id=config_objects[org_name]['client_id'],
|
|
1082
|
-
scopes=config_objects[org_name]['scopes'],
|
|
1083
|
-
org_name=org_name
|
|
1084
|
-
)
|
|
1085
|
-
self.admin = launchpy.Admin(org_name=org_name)
|
|
1086
|
-
self.cid = self.admin.getCompanyId()
|
|
1087
|
-
self.prompt = f"launchpy:{org_name}> "
|
|
1085
|
+
self._configure_connection(org_name)
|
|
1088
1086
|
console.print(f"Switched to organization '{org_name}'.", style="green")
|
|
1089
1087
|
except Exception as e:
|
|
1090
1088
|
console.print(f"(!) Error: {str(e)}", style="red")
|
|
@@ -1097,10 +1095,11 @@ class MainShell(cmd.Cmd):
|
|
|
1097
1095
|
parser = argparse.ArgumentParser(prog='create_config_file', add_help=True)
|
|
1098
1096
|
parser.add_argument("-fn", "--file_name", help="file name for your config file", default="launchpy_config.json",type=str)
|
|
1099
1097
|
parser.add_argument("-m","--multi_org", help="Enable multi-org configuration", action="store_true", default=False)
|
|
1098
|
+
parser.add_argument("-p","--proxy", help="Enable proxy for the connection", action="store_true", default=False)
|
|
1100
1099
|
try:
|
|
1101
1100
|
args = parser.parse_args(shlex.split(arg))
|
|
1102
1101
|
filename = args.file_name
|
|
1103
|
-
launchpy.createConfigFile(filename=filename,multi_org=args.multi_org)
|
|
1102
|
+
launchpy.createConfigFile(filename=filename,proxy=args.proxy,multi_org=args.multi_org)
|
|
1104
1103
|
filename_json = filename + ".json" if not filename.endswith(".json") else filename
|
|
1105
1104
|
console.print(f"Configuration file created at {Path.cwd() / Path(filename_json)}", style="green")
|
|
1106
1105
|
return
|
|
@@ -1121,8 +1120,10 @@ class MainShell(cmd.Cmd):
|
|
|
1121
1120
|
parser.add_argument("-cf", "--config_file", help="Path to config file", default=None)
|
|
1122
1121
|
parser.add_argument("-p", "--property", help="Property Name to auto-load on startup", default=None)
|
|
1123
1122
|
parser.add_argument("-on", "--org_name", help="Organization Name to use from config file", default=None)
|
|
1123
|
+
parser.add_argument("--proxy", help="Proxy URL for both HTTP and HTTPS")
|
|
1124
1124
|
try:
|
|
1125
1125
|
args = parser.parse_args(shlex.split(arg))
|
|
1126
|
+
proxy = _validate_proxy(args.proxy)
|
|
1126
1127
|
if args.config_file is not None:
|
|
1127
1128
|
config_path = Path(args.config_file)
|
|
1128
1129
|
if not config_path.is_absolute():
|
|
@@ -1136,6 +1137,7 @@ class MainShell(cmd.Cmd):
|
|
|
1136
1137
|
self.config[config_key]['org_id'] = dict_config.get("org_id", args.org_id)
|
|
1137
1138
|
self.config[config_key]['client_id'] = dict_config.get("client_id", dict_config.get("api_key", args.client_id))
|
|
1138
1139
|
self.config[config_key]['scopes'] = dict_config.get("scopes", args.scopes)
|
|
1140
|
+
self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(dict_config.get("proxy"))
|
|
1139
1141
|
config_objects.update(self.config)
|
|
1140
1142
|
elif type(dict_config) == list:
|
|
1141
1143
|
self.config = {}
|
|
@@ -1149,6 +1151,7 @@ class MainShell(cmd.Cmd):
|
|
|
1149
1151
|
self.config[config_key]['org_id'] = config.get("org_id", args.org_id)
|
|
1150
1152
|
self.config[config_key]['client_id'] = config.get("client_id", config.get("api_key", args.client_id))
|
|
1151
1153
|
self.config[config_key]['scopes'] = config.get("scopes", args.scopes)
|
|
1154
|
+
self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(config.get("proxy"))
|
|
1152
1155
|
config_objects.update(self.config)
|
|
1153
1156
|
config_key = dict_config[0].get("org_name")
|
|
1154
1157
|
else:
|
|
@@ -1160,6 +1163,7 @@ class MainShell(cmd.Cmd):
|
|
|
1160
1163
|
if args.org_id: self.config[config_key]['org_id'] = str(args.org_id)
|
|
1161
1164
|
if args.scopes: self.config[config_key]['scopes'] = str(args.scopes)
|
|
1162
1165
|
if args.client_id: self.config[config_key]['client_id'] = str(args.client_id)
|
|
1166
|
+
self.config[config_key]['proxy'] = proxy
|
|
1163
1167
|
config_objects.update(self.config)
|
|
1164
1168
|
missing = [
|
|
1165
1169
|
key for key, value in {
|
|
@@ -1423,6 +1427,7 @@ def main():
|
|
|
1423
1427
|
parser.add_argument("-cf", "--config_file", help="Path to config file", default=None)
|
|
1424
1428
|
parser.add_argument("-p", "--property", help="Property Name to auto-load on startup", default=None)
|
|
1425
1429
|
parser.add_argument("-on", "--org_name", help="Organization Name to use from config file", default=None)
|
|
1430
|
+
parser.add_argument("--proxy", help="Proxy URL for both HTTP and HTTPS")
|
|
1426
1431
|
args = parser.parse_args()
|
|
1427
1432
|
shell = MainShell(**vars(args))
|
|
1428
1433
|
try:
|
|
@@ -3,6 +3,14 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
from launchpy.config import config_objects, headers
|
|
5
5
|
|
|
6
|
+
def _validate_proxy(proxy: str | None) -> str | None:
|
|
7
|
+
if proxy is None:
|
|
8
|
+
return None
|
|
9
|
+
if not isinstance(proxy, str) or not proxy.strip():
|
|
10
|
+
raise ValueError("`proxy` must be a non-empty proxy URL string.")
|
|
11
|
+
return proxy
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
def find_path(path: str) -> Optional[Path]:
|
|
7
15
|
"""Checks if the file denoted by the specified `path` exists and returns the Path object
|
|
8
16
|
for the file.
|
|
@@ -22,7 +30,12 @@ def find_path(path: str) -> Optional[Path]:
|
|
|
22
30
|
return None
|
|
23
31
|
|
|
24
32
|
|
|
25
|
-
def createConfigFile(filename:str='config_launch',
|
|
33
|
+
def createConfigFile(filename:str='config_launch',
|
|
34
|
+
auth_type: str = "oauthV2",
|
|
35
|
+
proxy:bool=False,
|
|
36
|
+
scope: str = "https://ims-na1.adobelogin.com/s/ent_reactor_admin_sdk",
|
|
37
|
+
multi_org:bool=False,
|
|
38
|
+
verbose: object = False)->None:
|
|
26
39
|
"""
|
|
27
40
|
This function will create a 'config_launch_admin.json' file where you can store your access data.
|
|
28
41
|
Arguments:
|
|
@@ -31,6 +44,9 @@ def createConfigFile(filename:str='config_launch',auth_type: str = "oauthV2", sc
|
|
|
31
44
|
or
|
|
32
45
|
scope="https://ims-na1.adobelogin.com/s/ent_reactor_sdk"
|
|
33
46
|
auth_type : OPTIONAL : The type of Oauth type you want to use for your config file. Possible value: "oauthV2"
|
|
47
|
+
proxy: OPTIONAL : Whether to use a proxy for the connection. Default is False.
|
|
48
|
+
multi_org: OPTIONAL : Whether to enable multi-organization configuration. Default is False.
|
|
49
|
+
verbose: OPTIONAL : Whether to print verbose output during the creation of the config file. Default is False.
|
|
34
50
|
"""
|
|
35
51
|
json_data = {
|
|
36
52
|
'org_id': '<orgID>',
|
|
@@ -39,7 +55,8 @@ def createConfigFile(filename:str='config_launch',auth_type: str = "oauthV2", sc
|
|
|
39
55
|
}
|
|
40
56
|
if auth_type == 'oauthV2':
|
|
41
57
|
json_data['scopes'] = "<scopes>"
|
|
42
|
-
|
|
58
|
+
if proxy:
|
|
59
|
+
json_data['proxy'] = "<proxy_url>"
|
|
43
60
|
if multi_org:
|
|
44
61
|
json_data['org_name'] = '<org_name>'
|
|
45
62
|
json_data = [json_data]
|
|
@@ -50,8 +67,11 @@ def createConfigFile(filename:str='config_launch',auth_type: str = "oauthV2", sc
|
|
|
50
67
|
if verbose:
|
|
51
68
|
print(
|
|
52
69
|
f" file created at this location : {os.getcwd()}{os.sep}{filename}")
|
|
70
|
+
if proxy and verbose:
|
|
71
|
+
print("Proxy has been enabled for the connection.")
|
|
53
72
|
|
|
54
|
-
def importConfigFile(path: str = None,auth_type:str=None
|
|
73
|
+
def importConfigFile(path: str = None,auth_type:str=None,
|
|
74
|
+
proxy: str | None = None) -> None:
|
|
55
75
|
"""Reads the file denoted by the supplied `path` and retrieves the configuration information
|
|
56
76
|
from it.
|
|
57
77
|
|
|
@@ -59,6 +79,7 @@ def importConfigFile(path: str = None,auth_type:str=None) -> None:
|
|
|
59
79
|
path: REQUIRED : path to the configuration file. Can be either a fully-qualified or relative.
|
|
60
80
|
auth_type : OPTIONAL : The type of Auth to be used by default. Detected if none is passed, OauthV2 takes precedence.
|
|
61
81
|
Possible values: "jwt" or "oauthV2"
|
|
82
|
+
proxy : OPTIONAL : Proxy URL overriding the file's proxy for all organizations.
|
|
62
83
|
Example of path value.
|
|
63
84
|
"config.json"
|
|
64
85
|
"./config.json"
|
|
@@ -89,7 +110,8 @@ def importConfigFile(path: str = None,auth_type:str=None) -> None:
|
|
|
89
110
|
"org_id" : provided_config['org_id'],
|
|
90
111
|
"secret" : provided_config['secret'],
|
|
91
112
|
"client_id" : client_id,
|
|
92
|
-
"scope" : provided_config.get('scope')
|
|
113
|
+
"scope" : provided_config.get('scope'),
|
|
114
|
+
"proxy": provided_config.get("proxy") if proxy is None else proxy
|
|
93
115
|
}
|
|
94
116
|
if auth_type == 'oauthV2':
|
|
95
117
|
args["scopes"] = provided_config["scopes"].replace(' ','')
|
|
@@ -114,7 +136,8 @@ def importConfigFile(path: str = None,auth_type:str=None) -> None:
|
|
|
114
136
|
"secret" : config['secret'],
|
|
115
137
|
"client_id" : client_id,
|
|
116
138
|
"org_name" : config['org_name'],
|
|
117
|
-
"scope" : config.get('scope')
|
|
139
|
+
"scope" : config.get('scope'),
|
|
140
|
+
"proxy": config.get("proxy") if proxy is None else proxy
|
|
118
141
|
}
|
|
119
142
|
if auth_type == 'oauthV2':
|
|
120
143
|
args["scopes"] = config["scopes"].replace(' ','')
|
|
@@ -156,6 +179,7 @@ def configure(org_id: str = None,
|
|
|
156
179
|
scopes : str= None,
|
|
157
180
|
scope: str="https://ims-na1.adobelogin.com/s/ent_reactor_admin_sdk",
|
|
158
181
|
org_name: str = "default",
|
|
182
|
+
proxy: str | None = None,
|
|
159
183
|
**kwargs
|
|
160
184
|
):
|
|
161
185
|
"""Performs programmatic configuration of the API using provided values.
|
|
@@ -168,6 +192,8 @@ def configure(org_id: str = None,
|
|
|
168
192
|
scope : OPTIONAL : Scope that is needed for JWT auth.
|
|
169
193
|
org_name : OPTIONAL : The name of the org for multi-org configuration. If not provided, the "default" org will be used.
|
|
170
194
|
Possible scope: https://www.adobe.io/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/JWT/Scopes.md
|
|
195
|
+
proxy : OPTIONAL : A single proxy URL for both HTTP and HTTPS.
|
|
196
|
+
Used for OAuth, API calls, and property exports. None preserves environment settings.
|
|
171
197
|
"""
|
|
172
198
|
if not org_id:
|
|
173
199
|
raise ValueError("`org_id` must be specified in the configuration.")
|
|
@@ -177,6 +203,7 @@ def configure(org_id: str = None,
|
|
|
177
203
|
raise ValueError("`scopes` must be specified in the configuration.")
|
|
178
204
|
if not secret:
|
|
179
205
|
raise ValueError("`secret` must be specified in the configuration.")
|
|
206
|
+
proxy = _validate_proxy(proxy)
|
|
180
207
|
header = {"Accept": "application/vnd.api+json;revision=1",
|
|
181
208
|
"Content-Type": "application/vnd.api+json",
|
|
182
209
|
"Authorization": "Bearer ",
|
|
@@ -204,6 +231,7 @@ def configure(org_id: str = None,
|
|
|
204
231
|
config_object[org_name]["secret"] = secret
|
|
205
232
|
config_object[org_name]["official_scope"] = scope
|
|
206
233
|
config_object[org_name]["scopes"] = scopes
|
|
234
|
+
config_object[org_name]["proxy"] = proxy
|
|
207
235
|
# ensure the reset of the state by overwriting possible values from previous import.
|
|
208
236
|
config_object[org_name]["date_limit"] = 0
|
|
209
237
|
config_object[org_name]["token"] = ""
|
|
@@ -46,6 +46,7 @@ class AdobeRequest:
|
|
|
46
46
|
raise Exception(
|
|
47
47
|
'You have to upload the configuration file with importConfigFile method.')
|
|
48
48
|
self.config = {self.config_key: deepcopy(config_object)}
|
|
49
|
+
self.proxy = configs._validate_proxy(config_object.get("proxy"))
|
|
49
50
|
header = headers[self.config_key]
|
|
50
51
|
self.header = deepcopy(header)
|
|
51
52
|
self.retry = retry
|
|
@@ -79,8 +80,10 @@ class AdobeRequest:
|
|
|
79
80
|
"client_secret": config["secret"],
|
|
80
81
|
"scope": config["scopes"]
|
|
81
82
|
}
|
|
83
|
+
proxy = configs._validate_proxy(config.get("proxy"))
|
|
84
|
+
proxy_kwargs = {"proxies": {"http": proxy, "https": proxy}} if proxy is not None else {}
|
|
82
85
|
response = requests.post(
|
|
83
|
-
config["oauthTokenEndpointV2"], data=oauth_payload)
|
|
86
|
+
config["oauthTokenEndpointV2"], data=oauth_payload, **proxy_kwargs)
|
|
84
87
|
json_response = response.json()
|
|
85
88
|
if 'access_token' in json_response.keys():
|
|
86
89
|
token = json_response['access_token']
|
|
@@ -130,6 +133,8 @@ class AdobeRequest:
|
|
|
130
133
|
if isinstance(retries, bool) or not isinstance(retries, int) or retries < 0:
|
|
131
134
|
raise ValueError("retry must be a non-negative integer")
|
|
132
135
|
request_kwargs: dict[str, Any] = {}
|
|
136
|
+
if self.proxy is not None:
|
|
137
|
+
request_kwargs["proxies"] = {"http": self.proxy, "https": self.proxy}
|
|
133
138
|
if params is not None:
|
|
134
139
|
request_kwargs["params"] = params
|
|
135
140
|
if data is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: launchpy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Adobe Launch API wrapper
|
|
5
5
|
Author-email: Julien Piccini <piccini.julien@gmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -71,6 +71,30 @@ Most of the documentation has been imported from the datanalyst website [here](.
|
|
|
71
71
|
|
|
72
72
|
You can find release information [here](./docs/releases.md).
|
|
73
73
|
|
|
74
|
+
## Proxy support
|
|
75
|
+
|
|
76
|
+
Pass a `proxy` URL to `launchpy.configure(...)`, or add `"proxy"` to
|
|
77
|
+
your JSON configuration file. The same proxy serves both HTTP and HTTPS.
|
|
78
|
+
Proxy settings apply to OAuth authentication,
|
|
79
|
+
API requests, and asynchronous property exports, with separate settings per
|
|
80
|
+
organization.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
launchpy.configure(
|
|
84
|
+
org_id="your-org@AdobeOrg",
|
|
85
|
+
client_id="your-client-id",
|
|
86
|
+
secret="your-secret",
|
|
87
|
+
scopes="your-scopes",
|
|
88
|
+
proxy="http://proxy.example.com:8080",
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The CLI accepts `--proxy URL` at startup and in the interactive `config`
|
|
93
|
+
command. An explicit CLI proxy overrides the file setting.
|
|
94
|
+
Without an explicit proxy, existing environment-based proxy behavior is unchanged.
|
|
95
|
+
See the [Python and JSON examples](./docs/getstarted.md#proxy-configuration)
|
|
96
|
+
and [CLI examples](./docs/cli.md#proxy-configuration).
|
|
97
|
+
|
|
74
98
|
## HTTP retries
|
|
75
99
|
|
|
76
100
|
The `AdobeRequest` connector always retries HTTP 429 responses for GET, POST,
|
|
@@ -17,6 +17,7 @@ class ConnectorRetryTests(unittest.TestCase):
|
|
|
17
17
|
self.connector = AdobeRequest.__new__(AdobeRequest)
|
|
18
18
|
self.connector.header = {"Authorization": "test"}
|
|
19
19
|
self.connector.retry = 2
|
|
20
|
+
self.connector.proxy = None
|
|
20
21
|
self.connector._checkingDate = Mock()
|
|
21
22
|
sleep_patch = patch("launchpy.connector.time.sleep")
|
|
22
23
|
self.sleep = sleep_patch.start()
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from threading import Thread
|
|
8
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
9
|
+
|
|
10
|
+
import launchpy
|
|
11
|
+
from launchpy.cli.__main__ import MainShell, main
|
|
12
|
+
from launchpy.config import config_objects, headers
|
|
13
|
+
from launchpy.connector import AdobeRequest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProxyTestCase(unittest.TestCase):
|
|
17
|
+
credentials = {
|
|
18
|
+
"org_id": "test@AdobeOrg",
|
|
19
|
+
"client_id": "client",
|
|
20
|
+
"secret": "secret",
|
|
21
|
+
"scopes": "scope",
|
|
22
|
+
}
|
|
23
|
+
proxy = "http://proxy.example:8080"
|
|
24
|
+
|
|
25
|
+
def setUp(self):
|
|
26
|
+
for target in (config_objects, headers):
|
|
27
|
+
patcher = patch.dict(target, {}, clear=True)
|
|
28
|
+
patcher.start()
|
|
29
|
+
self.addCleanup(patcher.stop)
|
|
30
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
31
|
+
self.addCleanup(self.directory.cleanup)
|
|
32
|
+
|
|
33
|
+
def config_file(self, data):
|
|
34
|
+
path = Path(self.directory.name) / "config.json"
|
|
35
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
36
|
+
return str(path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProxyConfigTests(ProxyTestCase):
|
|
40
|
+
def test_configure_stores_proxy_and_resets_it_on_reconfiguration(self):
|
|
41
|
+
launchpy.configure(**self.credentials, proxy=self.proxy)
|
|
42
|
+
self.assertEqual(config_objects["default"]["proxy"], self.proxy)
|
|
43
|
+
launchpy.configure(**self.credentials)
|
|
44
|
+
self.assertIsNone(config_objects["default"]["proxy"])
|
|
45
|
+
|
|
46
|
+
def test_invalid_proxy_configurations_are_rejected(self):
|
|
47
|
+
for proxy in ([], {}, {"https": self.proxy}, 1, True, "", " "):
|
|
48
|
+
with self.subTest(proxy=proxy):
|
|
49
|
+
with self.assertRaises(ValueError):
|
|
50
|
+
launchpy.configure(**self.credentials, proxy=proxy)
|
|
51
|
+
self.assertEqual(config_objects, {})
|
|
52
|
+
|
|
53
|
+
def test_import_single_config_with_legacy_api_key(self):
|
|
54
|
+
config = dict(self.credentials, proxy=self.proxy)
|
|
55
|
+
config["api_key"] = config.pop("client_id")
|
|
56
|
+
launchpy.importConfigFile(self.config_file(config))
|
|
57
|
+
self.assertEqual(config_objects["default"]["proxy"], self.proxy)
|
|
58
|
+
self.assertEqual(config_objects["default"]["client_id"], "client")
|
|
59
|
+
|
|
60
|
+
def test_import_multi_org_keeps_proxy_independent(self):
|
|
61
|
+
configs = [
|
|
62
|
+
dict(self.credentials, org_name="first", proxy=self.proxy),
|
|
63
|
+
dict(self.credentials, org_name="second", proxy="http://other:8080"),
|
|
64
|
+
dict(self.credentials, org_name="third"),
|
|
65
|
+
]
|
|
66
|
+
launchpy.importConfigFile(self.config_file(configs))
|
|
67
|
+
self.assertEqual(config_objects["first"]["proxy"], self.proxy)
|
|
68
|
+
self.assertEqual(config_objects["second"]["proxy"], configs[1]["proxy"])
|
|
69
|
+
self.assertIsNone(config_objects["third"]["proxy"])
|
|
70
|
+
|
|
71
|
+
def test_import_proxy_override_applies_to_single_and_multi_org(self):
|
|
72
|
+
override = "http://override:8080"
|
|
73
|
+
for multi in (False, True):
|
|
74
|
+
with self.subTest(multi=multi):
|
|
75
|
+
config = dict(self.credentials, org_name="first", proxy=self.proxy)
|
|
76
|
+
data = [config] if multi else config
|
|
77
|
+
launchpy.importConfigFile(self.config_file(data), proxy=override)
|
|
78
|
+
self.assertEqual(config_objects["first" if multi else "default"]["proxy"],
|
|
79
|
+
override)
|
|
80
|
+
|
|
81
|
+
def test_import_without_proxy_preserves_old_configuration_format(self):
|
|
82
|
+
launchpy.importConfigFile(self.config_file(self.credentials))
|
|
83
|
+
self.assertIsNone(config_objects["default"]["proxy"])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ConnectorProxyTests(ProxyTestCase):
|
|
87
|
+
def make_connector(self, proxy=None):
|
|
88
|
+
launchpy.configure(**self.credentials, proxy=proxy)
|
|
89
|
+
return AdobeRequest()
|
|
90
|
+
|
|
91
|
+
@patch("launchpy.connector.requests.post")
|
|
92
|
+
def test_oauth_initial_request_and_refresh_use_proxy(self, post):
|
|
93
|
+
post.return_value.json.return_value = {"access_token": "token", "expires_in": 3600}
|
|
94
|
+
connector = self.make_connector(self.proxy)
|
|
95
|
+
connector.config["default"]["date_limit"] = 0
|
|
96
|
+
connector._checkingDate()
|
|
97
|
+
self.assertEqual(post.call_count, 2)
|
|
98
|
+
for call in post.call_args_list:
|
|
99
|
+
self.assertEqual(call.kwargs["proxies"], {"http": self.proxy, "https": self.proxy})
|
|
100
|
+
config_objects["default"]["proxy"] = "http://changed:8080"
|
|
101
|
+
self.assertEqual(connector.proxy, self.proxy)
|
|
102
|
+
|
|
103
|
+
@patch("launchpy.connector.requests.post")
|
|
104
|
+
def test_legacy_oauth_configuration_does_not_add_proxy_kwargs(self, post):
|
|
105
|
+
post.return_value.json.return_value = {"access_token": "token", "expires_in": 3600}
|
|
106
|
+
connector = self.make_connector()
|
|
107
|
+
connector.config["default"]["date_limit"] = 0
|
|
108
|
+
connector._checkingDate()
|
|
109
|
+
for call in post.call_args_list:
|
|
110
|
+
self.assertNotIn("proxies", call.kwargs)
|
|
111
|
+
|
|
112
|
+
def test_all_api_methods_preserve_proxy_when_retrying(self):
|
|
113
|
+
with patch("launchpy.connector.requests.post") as post:
|
|
114
|
+
post.return_value.json.return_value = {"access_token": "token", "expires_in": 3600}
|
|
115
|
+
connector = self.make_connector(self.proxy)
|
|
116
|
+
for method in ("get", "post", "put", "patch", "delete"):
|
|
117
|
+
with self.subTest(method=method):
|
|
118
|
+
limited = Mock(status_code=429, headers={"Retry-After": "0"})
|
|
119
|
+
success = Mock(status_code=200)
|
|
120
|
+
success.json.return_value = {"data": []}
|
|
121
|
+
with patch(f"launchpy.connector.requests.{method}",
|
|
122
|
+
side_effect=[limited, success]) as request:
|
|
123
|
+
with patch("launchpy.connector.time.sleep"):
|
|
124
|
+
getattr(connector, method + "Data")("https://example.invalid")
|
|
125
|
+
self.assertEqual(request.call_count, 2)
|
|
126
|
+
for call in request.call_args_list:
|
|
127
|
+
self.assertEqual(call.kwargs["proxies"],
|
|
128
|
+
{"http": self.proxy, "https": self.proxy})
|
|
129
|
+
|
|
130
|
+
def test_organizations_use_their_own_proxy_for_authentication(self):
|
|
131
|
+
launchpy.configure(**self.credentials, org_name="first", proxy=self.proxy)
|
|
132
|
+
launchpy.configure(**self.credentials, org_name="second",
|
|
133
|
+
proxy="http://other:8080")
|
|
134
|
+
with patch("launchpy.connector.requests.post") as post:
|
|
135
|
+
post.return_value.json.return_value = {"access_token": "token", "expires_in": 3600}
|
|
136
|
+
first = AdobeRequest(org_name="first")
|
|
137
|
+
second = AdobeRequest(org_name="second")
|
|
138
|
+
self.assertEqual(post.call_args_list[0].kwargs["proxies"],
|
|
139
|
+
{"http": first.proxy, "https": first.proxy})
|
|
140
|
+
self.assertEqual(post.call_args_list[1].kwargs["proxies"],
|
|
141
|
+
{"http": second.proxy, "https": second.proxy})
|
|
142
|
+
self.assertNotEqual(first.proxy, second.proxy)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class CLIProxyTests(ProxyTestCase):
|
|
146
|
+
def setUp(self):
|
|
147
|
+
super().setUp()
|
|
148
|
+
for target in ("launchpy.cli.__main__.console", "launchpy.Admin"):
|
|
149
|
+
patcher = patch(target)
|
|
150
|
+
patcher.start()
|
|
151
|
+
self.addCleanup(patcher.stop)
|
|
152
|
+
|
|
153
|
+
def test_startup_direct_credentials_and_proxy(self):
|
|
154
|
+
shell = MainShell(**self.credentials, proxy=self.proxy)
|
|
155
|
+
self.assertEqual(config_objects["default"]["proxy"], self.proxy)
|
|
156
|
+
self.assertEqual(shell.org_name, "default")
|
|
157
|
+
|
|
158
|
+
def test_interactive_direct_credentials_and_proxy(self):
|
|
159
|
+
shell = MainShell()
|
|
160
|
+
shell.do_config("--client_id client --secret secret --org_id org --scopes scope "
|
|
161
|
+
f"--proxy {self.proxy}")
|
|
162
|
+
self.assertEqual(config_objects["default"]["proxy"], self.proxy)
|
|
163
|
+
self.assertIsNotNone(shell.cid)
|
|
164
|
+
|
|
165
|
+
def test_single_file_proxy_and_cli_override_at_startup_and_interactively(self):
|
|
166
|
+
path = self.config_file(dict(self.credentials, proxy=self.proxy))
|
|
167
|
+
for interactive in (False, True):
|
|
168
|
+
for override in (False, True):
|
|
169
|
+
with self.subTest(interactive=interactive, override=override):
|
|
170
|
+
expected = "http://override:8080" if override else self.proxy
|
|
171
|
+
if interactive:
|
|
172
|
+
shell = MainShell()
|
|
173
|
+
extra = " --proxy http://override:8080" if override else ""
|
|
174
|
+
shell.do_config(f'--config_file "{path}"{extra}')
|
|
175
|
+
else:
|
|
176
|
+
options = {"proxy": "http://override:8080"} if override else {}
|
|
177
|
+
shell = MainShell(config_file=path, **options)
|
|
178
|
+
self.assertEqual(config_objects["default"]["proxy"], expected)
|
|
179
|
+
self.assertIsNotNone(shell.cid)
|
|
180
|
+
|
|
181
|
+
def test_multi_org_proxy_survives_switching_and_reconfiguration(self):
|
|
182
|
+
configs = [
|
|
183
|
+
dict(self.credentials, org_name="first", proxy=self.proxy),
|
|
184
|
+
dict(self.credentials, org_name="second", proxy="http://other:8080"),
|
|
185
|
+
]
|
|
186
|
+
path = self.config_file(configs)
|
|
187
|
+
for interactive in (False, True):
|
|
188
|
+
with self.subTest(interactive=interactive):
|
|
189
|
+
shell = MainShell() if interactive else MainShell(config_file=path)
|
|
190
|
+
if interactive:
|
|
191
|
+
shell.do_config(f'--config_file "{path}"')
|
|
192
|
+
for org in ("second", "first", "second"):
|
|
193
|
+
shell.do_change_org(org)
|
|
194
|
+
expected = configs[0 if org == "first" else 1]["proxy"]
|
|
195
|
+
self.assertEqual(config_objects[org]["proxy"], expected)
|
|
196
|
+
self.assertEqual(shell.org_name, org)
|
|
197
|
+
shell.do_config("--client_id client --secret secret --org_id org "
|
|
198
|
+
"--scopes scope --proxy http://new:8080")
|
|
199
|
+
self.assertEqual(config_objects["default"]["proxy"], "http://new:8080")
|
|
200
|
+
|
|
201
|
+
def test_startup_parser_and_default_org_with_proxy(self):
|
|
202
|
+
with patch("sys.argv", ["launchpy", "--client_id", "client", "--secret", "secret",
|
|
203
|
+
"--org_id", "org", "--scopes", "scope",
|
|
204
|
+
"--proxy", "http://proxy:8080"]):
|
|
205
|
+
with patch.object(MainShell, "cmdloop"):
|
|
206
|
+
main()
|
|
207
|
+
self.assertEqual(config_objects["default"]["proxy"], "http://proxy:8080")
|
|
208
|
+
|
|
209
|
+
def test_multi_org_overrides_apply_to_every_org_at_startup_and_interactively(self):
|
|
210
|
+
configs = [
|
|
211
|
+
dict(self.credentials, org_name="first", proxy=self.proxy),
|
|
212
|
+
dict(self.credentials, org_name="second"),
|
|
213
|
+
]
|
|
214
|
+
path = self.config_file(configs)
|
|
215
|
+
for interactive in (False, True):
|
|
216
|
+
with self.subTest(interactive=interactive):
|
|
217
|
+
if interactive:
|
|
218
|
+
shell = MainShell()
|
|
219
|
+
shell.do_config(f'--config_file "{path}" --org_name second '
|
|
220
|
+
'--proxy http://common:8080')
|
|
221
|
+
else:
|
|
222
|
+
shell = MainShell(config_file=path, org_name="second",
|
|
223
|
+
proxy="http://common:8080")
|
|
224
|
+
for org in ("first", "second"):
|
|
225
|
+
self.assertEqual(config_objects[org]["proxy"], "http://common:8080")
|
|
226
|
+
self.assertEqual(shell.org_name, "second")
|
|
227
|
+
|
|
228
|
+
def test_invalid_file_proxy_is_reported_before_connecting(self):
|
|
229
|
+
path = self.config_file(dict(self.credentials, proxy={"https": self.proxy}))
|
|
230
|
+
shell = MainShell()
|
|
231
|
+
with patch("launchpy.Admin") as admin:
|
|
232
|
+
with patch("launchpy.cli.__main__.console") as console:
|
|
233
|
+
shell.do_config(f'--config_file "{path}"')
|
|
234
|
+
admin.assert_not_called()
|
|
235
|
+
self.assertIn("`proxy` must be", console.print.call_args.args[0])
|
|
236
|
+
|
|
237
|
+
def test_empty_proxy_url_is_reported_before_connecting(self):
|
|
238
|
+
shell = MainShell()
|
|
239
|
+
with patch("launchpy.Admin") as admin:
|
|
240
|
+
with patch("launchpy.cli.__main__.console") as console:
|
|
241
|
+
shell.do_config('--client_id client --secret secret --org_id org '
|
|
242
|
+
'--scopes scope --proxy ""')
|
|
243
|
+
admin.assert_not_called()
|
|
244
|
+
self.assertIn("`proxy` must be", console.print.call_args.args[0])
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class LocalProxyTests(ProxyTestCase):
|
|
248
|
+
def test_requests_and_httpx_route_through_configured_proxy(self):
|
|
249
|
+
received = []
|
|
250
|
+
|
|
251
|
+
class ProxyHandler(BaseHTTPRequestHandler):
|
|
252
|
+
def respond(self):
|
|
253
|
+
received.append((self.command, self.path))
|
|
254
|
+
self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
|
255
|
+
payload = (
|
|
256
|
+
{"access_token": "test-token", "expires_in": 3600}
|
|
257
|
+
if self.path.endswith("/token") else {"data": []}
|
|
258
|
+
)
|
|
259
|
+
body = json.dumps(payload).encode()
|
|
260
|
+
self.send_response(200)
|
|
261
|
+
self.send_header("Content-Type", "application/json")
|
|
262
|
+
self.send_header("Content-Length", str(len(body)))
|
|
263
|
+
self.end_headers()
|
|
264
|
+
self.wfile.write(body)
|
|
265
|
+
|
|
266
|
+
do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = respond
|
|
267
|
+
|
|
268
|
+
def log_message(self, format, *args):
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
server = ThreadingHTTPServer(("127.0.0.1", 0), ProxyHandler)
|
|
272
|
+
thread = Thread(target=server.serve_forever, daemon=True)
|
|
273
|
+
thread.start()
|
|
274
|
+
self.addCleanup(server.server_close)
|
|
275
|
+
self.addCleanup(thread.join, 5)
|
|
276
|
+
self.addCleanup(server.shutdown)
|
|
277
|
+
proxy = f"http://127.0.0.1:{server.server_port}"
|
|
278
|
+
launchpy.configure(**self.credentials, proxy=proxy)
|
|
279
|
+
config_objects["default"]["oauthTokenEndpointV2"] = "http://example.invalid/token"
|
|
280
|
+
connector = AdobeRequest()
|
|
281
|
+
for method in ("get", "post", "put", "patch", "delete"):
|
|
282
|
+
result = getattr(connector, method + "Data")("http://example.invalid/api")
|
|
283
|
+
self.assertEqual(result, 200 if method == "delete" else {"data": []})
|
|
284
|
+
connector.config["default"]["date_limit"] = 0
|
|
285
|
+
connector._checkingDate()
|
|
286
|
+
rule = {
|
|
287
|
+
"attributes": {"name": "test-rule"},
|
|
288
|
+
"relationships": {
|
|
289
|
+
"rule_components": {"links": {"related": "http://example.invalid/components"}},
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
asyncio.run(launchpy.process_all_rules(
|
|
293
|
+
[rule], self.directory.name, {}, connector.proxy))
|
|
294
|
+
self.assertEqual(received, [
|
|
295
|
+
("POST", "http://example.invalid/token"),
|
|
296
|
+
("GET", "http://example.invalid/api"),
|
|
297
|
+
("POST", "http://example.invalid/api"),
|
|
298
|
+
("PUT", "http://example.invalid/api"),
|
|
299
|
+
("PATCH", "http://example.invalid/api"),
|
|
300
|
+
("DELETE", "http://example.invalid/api"),
|
|
301
|
+
("POST", "http://example.invalid/token"),
|
|
302
|
+
("GET", "http://example.invalid/components"),
|
|
303
|
+
])
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class AsyncProxyTests(unittest.TestCase):
|
|
307
|
+
def test_async_rule_downloads_use_single_proxy(self):
|
|
308
|
+
proxy = "http://proxy.example:8080"
|
|
309
|
+
with patch("launchpy.httpx.AsyncClient") as client_class:
|
|
310
|
+
with patch("launchpy.__extractRuleComponents__", new_callable=AsyncMock) as extract:
|
|
311
|
+
rules = [{"id": "rule1"}, {"id": "rule2"}]
|
|
312
|
+
asyncio.run(launchpy.process_all_rules(rules, "folder", {}, proxy))
|
|
313
|
+
self.assertEqual(client_class.call_args.kwargs["proxy"], proxy)
|
|
314
|
+
self.assertNotIn("mounts", client_class.call_args.kwargs)
|
|
315
|
+
self.assertEqual(extract.await_count, 2)
|
|
316
|
+
|
|
317
|
+
def test_no_proxy_keeps_default_async_client_settings(self):
|
|
318
|
+
with patch("launchpy.httpx.AsyncClient") as client_class:
|
|
319
|
+
asyncio.run(launchpy.process_all_rules([], "folder", {}))
|
|
320
|
+
self.assertNotIn("proxy", client_class.call_args.kwargs)
|
|
321
|
+
|
|
322
|
+
def test_property_export_passes_connector_proxy(self):
|
|
323
|
+
property = Mock(spec=launchpy.Property)
|
|
324
|
+
property.name = "test"
|
|
325
|
+
property.header = {}
|
|
326
|
+
property.connector = Mock(proxy="http://proxy.example:8080")
|
|
327
|
+
property.getRules.return_value = []
|
|
328
|
+
property.getDataElements.return_value = []
|
|
329
|
+
property.getExtensions.return_value = []
|
|
330
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
331
|
+
with patch("launchpy.process_all_rules", new_callable=AsyncMock) as process:
|
|
332
|
+
launchpy.extractProperty(property, folder=folder)
|
|
333
|
+
process.assert_awaited_once_with([], folder, {}, property.connector.proxy)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
if __name__ == "__main__":
|
|
337
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.2"
|
|
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
|