launchpy 0.5.2__tar.gz → 0.5.4__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 (37) hide show
  1. {launchpy-0.5.2 → launchpy-0.5.4}/PKG-INFO +25 -1
  2. {launchpy-0.5.2 → launchpy-0.5.4}/README.md +24 -0
  3. {launchpy-0.5.2 → launchpy-0.5.4}/docs/cli.md +49 -0
  4. {launchpy-0.5.2 → launchpy-0.5.4}/docs/getstarted.md +55 -0
  5. {launchpy-0.5.2 → launchpy-0.5.4}/docs/main.md +11 -0
  6. {launchpy-0.5.2 → launchpy-0.5.4}/docs/releases.md +15 -0
  7. {launchpy-0.5.2 → launchpy-0.5.4}/docs/synchronizer.md +10 -0
  8. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/__init__.py +7 -3
  9. launchpy-0.5.4/launchpy/__version__.py +1 -0
  10. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/cli/__main__.py +25 -18
  11. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/configs.py +33 -5
  12. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/connector.py +6 -1
  13. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/synchronizer.py +38 -29
  14. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy.egg-info/PKG-INFO +25 -1
  15. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy.egg-info/SOURCES.txt +3 -1
  16. {launchpy-0.5.2 → launchpy-0.5.4}/tests/test_connector.py +1 -0
  17. launchpy-0.5.4/tests/test_proxies.py +337 -0
  18. launchpy-0.5.4/tests/test_synchronizer.py +428 -0
  19. launchpy-0.5.2/launchpy/__version__.py +0 -1
  20. {launchpy-0.5.2 → launchpy-0.5.4}/LICENSE +0 -0
  21. {launchpy-0.5.2 → launchpy-0.5.4}/docs/admin.md +0 -0
  22. {launchpy-0.5.2 → launchpy-0.5.4}/docs/library.md +0 -0
  23. {launchpy-0.5.2 → launchpy-0.5.4}/docs/property.md +0 -0
  24. {launchpy-0.5.2 → launchpy-0.5.4}/docs/translator.md +0 -0
  25. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/admin.py +0 -0
  26. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/cli/__init__.py +0 -0
  27. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/config.py +0 -0
  28. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/launchpy.py +0 -0
  29. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/library.py +0 -0
  30. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy/property.py +0 -0
  31. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy.egg-info/dependency_links.txt +0 -0
  32. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy.egg-info/entry_points.txt +0 -0
  33. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy.egg-info/requires.txt +0 -0
  34. {launchpy-0.5.2 → launchpy-0.5.4}/launchpy.egg-info/top_level.txt +0 -0
  35. {launchpy-0.5.2 → launchpy-0.5.4}/pyproject.toml +0 -0
  36. {launchpy-0.5.2 → launchpy-0.5.4}/requirements.txt +0 -0
  37. {launchpy-0.5.2 → launchpy-0.5.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchpy
3
- Version: 0.5.2
3
+ Version: 0.5.4
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:
@@ -490,9 +533,12 @@ Arguments:
490
533
  `-r`, `--regex`: Regex pattern to filter rules by name (partial match, non-case sensitive). This is an optional parameter, if not provided, all rules are synced.
491
534
  `-c`, `--create` : Flag. Create rules that do not exist in the destination property. Default False.
492
535
  `-p`, `--published` : Flag. Sync the latest published version of the rules. Default False.
536
+ `-i`, `--ignore_disabled` : Optional boolean. Skip disabled source rules. Default False; a bare flag means True. With `-p`, checks the latest published source revision.
493
537
 
494
538
  ```bash
495
539
  synchronizer:Base_Property_Name> sync_rules -r "Rule Name" -c
540
+ synchronizer:Base_Property_Name> sync_rules -i
541
+ synchronizer:Base_Property_Name> sync_rules --ignore_disabled True -p
496
542
  ```
497
543
 
498
544
  #### sync_data_elements
@@ -501,9 +547,12 @@ Arguments:
501
547
  `-r`, `--regex`: Regex pattern to filter data elements by name (partial match, non-case sensitive). This is an optional parameter, if not provided, all data elements are synced.
502
548
  `-c`, `--create` : Flag. Create data elements that do not exist in the destination property. Default False.
503
549
  `-p`, `--published` : Flag. Sync the latest published version of the data elements. Default False.
550
+ `-i`, `--ignore_disabled` : Optional boolean. Skip disabled source data elements. Default False; a bare flag means True. With `-p`, checks the latest published source revision.
504
551
 
505
552
  ```bash
506
553
  synchronizer:Base_Property_Name> sync_data_elements -r "Data Element Name" -c
554
+ synchronizer:Base_Property_Name> sync_data_elements -i
555
+ synchronizer:Base_Property_Name> sync_data_elements --ignore_disabled False
507
556
  ```
508
557
 
509
558
  #### sync_extensions
@@ -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,21 @@
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.4
7
+ * Fixing enabled state handling for rules, data elements, and extensions during synchronization. Before release, state enablement was respected only during -published synchronization.
8
+ * Ensuring that the verbose flag correctly displays detailed information for all synchronized entities, including those with enabled state changes.
9
+ * Adding the `ignore_disabled` flag to the synchronization commands to allow skipping disabled source entities.
10
+ * Adding `-i` / `--ignore_disabled` flag to the CLI synchronization commands.
11
+
12
+
13
+ ## 0.5.3
14
+ * 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.
15
+ * Applying proxy settings to OAuth token acquisition and refresh, all API requests and their retries, and asynchronous rule-component downloads during `extractProperty`.
16
+ * 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.
17
+ * 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.
18
+ * 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.
19
+ * Adding proxy configuration examples and documentation for Python, JSON, and CLI usage.
20
+
6
21
  ## 0.5.2
7
22
  * Fixing recursive GET retries and always retrying HTTP 429 responses for GET, POST, PUT, PATCH, and DELETE.
8
23
  * Respecting `Retry-After` seconds and HTTP dates, with exponential backoff when the header is absent or invalid.
@@ -150,6 +150,7 @@ synchronizor.syncComponent('my Component Name',publishedVersion=True)
150
150
  #### Data Elements
151
151
  The method will replace the existing configuration of a data element by the template configuration.\
152
152
  Completely overriding the existing element configuration in the target properties.
153
+ When a data element is created, its enabled state is updated if needed to match the selected source revision. The response from that update is retained in the target cache and library stack.
153
154
 
154
155
  #### Rules
155
156
  The method will delete all rule components existing in the Target property for that rule.\
@@ -198,6 +199,7 @@ Arguments:
198
199
  * regex : OPTIONAL : If you want to filter the data elements to sync based on a regex pattern. Default: None (sync all data elements)
199
200
  * forceCreation : OPTIONAL : If set to True, it will sync the data elements even if they do not exist in the target properties. If set to False, it will only sync the data elements that already exist in the target properties. Default: False.
200
201
  * publishedVersion : OPTIONAL : If set to True, it will sync the latest published version of the data elements. Default: False.
202
+ * ignoreDisabled : OPTIONAL : If set to True, skip data elements whose selected source revision has `enabled=False`. Default: False.
201
203
 
202
204
  ## SyncRules
203
205
  It will synchronize the rules in the base property to the target properties.
@@ -205,7 +207,14 @@ Arguments:
205
207
  * regex : OPTIONAL : If you want to filter the rules to sync based on a regex pattern. Default: None (sync all rules)
206
208
  * forceCreation : OPTIONAL : If set to True, it will sync the rules even if they do not exist in the target properties. If set to False, it will only sync the rules that already exist in the target properties. Default: False.
207
209
  * publishedVersion : OPTIONAL : If set to True, it will sync the latest published version of the rules. Default: False.
210
+ * ignoreDisabled : OPTIONAL : If set to True, skip rules whose selected source revision has `enabled=False`. Default: False.
208
211
 
212
+ For both methods, `ignoreDisabled=True` checks the source revision being synchronized, not the target's enabled state. With `publishedVersion=True`, it checks the latest published source revision rather than the current draft. Disabled components are not created or updated, even with `forceCreation=True`, and retain an empty per-target dictionary in the returned results.
213
+
214
+ ```python
215
+ synchronizor.syncRules(ignoreDisabled=True)
216
+ synchronizor.syncDataElements(ignoreDisabled=True, publishedVersion=True)
217
+ ```
209
218
 
210
219
  ## createTargetsLibrary
211
220
 
@@ -238,6 +247,7 @@ Check if the component,from the base property, is synced to the different target
238
247
  It can also check for the Extensions.\
239
248
  It will return a dictionary with the key being the target property and the value being the result of the evaluation, such as {targetProperty: 'similar'} when component have same settings or {targetPropery : '<difference>'} when component do not have same settings.\
240
249
  See below for differences.\
250
+ For rules and data elements, enabled-state differences are checked in both current and published modes. A matching configuration does not override an enabled-state difference with `"Similar"`. When a data element has multiple differences, they are returned together as a comma-separated message. Library dry runs use the same comparison logic.
241
251
  Arguments:
242
252
  * componentName : REQUIRED : the name of the component to compare
243
253
  * componentID : REQUIRED : the id of the component to compare
@@ -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
- async with httpx.AsyncClient(timeout=timeout, limits=limits) as client:
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.4"
@@ -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
@@ -594,10 +595,11 @@ class SynchronizerCLI(cmd.Cmd):
594
595
  parser.add_argument("-r", "--regex", help="Regex pattern to filter rules by name (partial match, non-case sensitive). Default None.", type=str, default=None)
595
596
  parser.add_argument("-c", "--create", help="Create rules that do not exist in the destination property. Default False.", action="store_true", default=False)
596
597
  parser.add_argument("-p", "--published", help="Sync the latest published version of the rules. Default False.", action="store_true", default=False)
598
+ parser.add_argument("-i", "--ignore_disabled", help="Skip disabled source rules. Default False. A bare flag means True.", type=str2bool, nargs="?", const=True, default=False)
597
599
  parser.add_argument("-v", "--verbose", help="Print detailed information about the sync process, including successes. Default False.", action="store_true", default=False)
598
600
  try:
599
601
  args = parser.parse_args(shlex.split(args))
600
- result = self.synchronizer.syncRules(regex=args.regex, forceCreation=args.create, publishedVersion=args.published)
602
+ result = self.synchronizer.syncRules(regex=args.regex, forceCreation=args.create, publishedVersion=args.published, ignoreDisabled=args.ignore_disabled)
601
603
  for ruleName, targetResults in result.items():
602
604
  for prop, message in (targetResults or {}).items():
603
605
  if "does not exist" in message:
@@ -616,10 +618,11 @@ class SynchronizerCLI(cmd.Cmd):
616
618
  parser.add_argument("-r", "--regex", help="Regex pattern to filter data elements by name (partial match, non-case sensitive). Default None.", type=str, default=None)
617
619
  parser.add_argument("-c", "--create", help="Create data elements that do not exist in the destination property. Default False.", action="store_true", default=False)
618
620
  parser.add_argument("-p", "--published", help="Sync the latest published version of the data elements. Default False.", action="store_true", default=False)
621
+ parser.add_argument("-i", "--ignore_disabled", help="Skip disabled source data elements. Default False. A bare flag means True.", type=str2bool, nargs="?", const=True, default=False)
619
622
  parser.add_argument("-v", "--verbose", help="Print detailed information about the sync process, including successes. Default False.", action="store_true", default=False)
620
623
  try:
621
624
  args = parser.parse_args(shlex.split(args))
622
- result = self.synchronizer.syncDataElements(regex=args.regex, forceCreation=args.create, publishedVersion=args.published)
625
+ result = self.synchronizer.syncDataElements(regex=args.regex, forceCreation=args.create, publishedVersion=args.published, ignoreDisabled=args.ignore_disabled)
623
626
  for deName, targetResults in result.items():
624
627
  for prop, message in (targetResults or {}).items():
625
628
  if "does not exist" in message:
@@ -961,6 +964,7 @@ class MainShell(cmd.Cmd):
961
964
 
962
965
  def __init__(self, **kwargs:ParamSpecKwargs) -> None:
963
966
  super().__init__()
967
+ proxy = _validate_proxy(kwargs.get("proxy"))
964
968
  self.admin = None
965
969
  self.cid = None
966
970
  self.properties = None
@@ -988,12 +992,13 @@ class MainShell(cmd.Cmd):
988
992
  with open(config_path, "rb") as f:
989
993
  dict_config = json.load(f)
990
994
  if type(dict_config) == dict:
991
- config_key = kwargs.get("org_name", "default")
995
+ config_key = kwargs.get("org_name") or "default"
992
996
  self.config = {config_key: deepcopy(self.config_template)}
993
997
  self.config[config_key]['secret'] = dict_config.get("secret", kwargs.get("secret"))
994
998
  self.config[config_key]['org_id'] = dict_config.get("org_id", kwargs.get("org_id"))
995
999
  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
1000
  self.config[config_key]['scopes'] = dict_config.get("scopes", kwargs.get("scopes"))
1001
+ self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(dict_config.get("proxy"))
997
1002
  config_objects.update(self.config)
998
1003
  elif type(dict_config) == list:
999
1004
  self.config = {}
@@ -1007,15 +1012,17 @@ class MainShell(cmd.Cmd):
1007
1012
  self.config[config_key]['org_id'] = config.get("org_id", kwargs.get("org_id"))
1008
1013
  self.config[config_key]['client_id'] = config.get("client_id", config.get("api_key", kwargs.get("client_id", kwargs.get("api_key"))))
1009
1014
  self.config[config_key]['scopes'] = config.get("scopes", kwargs.get("scopes"))
1015
+ self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(config.get("proxy"))
1010
1016
  config_objects.update(self.config)
1011
1017
  config_key = dict_config[0].get("org_name", "default")
1012
1018
  else:
1013
- config_key = kwargs.get("org_name", "default")
1019
+ config_key = kwargs.get("org_name") or "default"
1014
1020
  self.config = {config_key: deepcopy(self.config_template)}
1015
1021
  self.config[config_key]['secret'] = kwargs.get("secret")
1016
1022
  self.config[config_key]['org_id'] = kwargs.get("org_id")
1017
1023
  self.config[config_key]['client_id'] = kwargs.get("client_id",kwargs.get("api_key"))
1018
1024
  self.config[config_key]['scopes'] = kwargs.get("scopes")
1025
+ self.config[config_key]['proxy'] = proxy
1019
1026
  config_objects.update(self.config)
1020
1027
  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
1028
  myheaders = {}
@@ -1024,7 +1031,7 @@ class MainShell(cmd.Cmd):
1024
1031
  myheaders[org_name]["x-gw-ims-org-id"] = config['org_id']
1025
1032
  myheaders[org_name]["x-api-key"] = config['client_id']
1026
1033
  headers.update(myheaders)
1027
- config_key = kwargs.get("org_name", config_key)
1034
+ config_key = kwargs.get("org_name") or config_key
1028
1035
  console.print("Configuring connection...", style="blue")
1029
1036
  self._configure_connection(config_key)
1030
1037
  if kwargs.get("property") is not None:
@@ -1051,13 +1058,15 @@ class MainShell(cmd.Cmd):
1051
1058
  else:
1052
1059
  config_key = list(self.config.keys())[0]
1053
1060
  self.org_name = config_key
1054
- self.config = launchpy.configure(
1061
+ launchpy.configure(
1055
1062
  secret=config_objects[config_key]['secret'],
1056
1063
  org_id=config_objects[config_key]['org_id'],
1057
1064
  client_id=config_objects[config_key]['client_id'],
1058
1065
  scopes=config_objects[config_key]['scopes'],
1059
- org_name=config_key
1066
+ org_name=config_key,
1067
+ proxy=config_objects[config_key].get("proxy")
1060
1068
  )
1069
+ self.config = config_objects
1061
1070
  self.admin = launchpy.Admin(org_name=config_key)
1062
1071
  self.cid = self.admin.getCompanyId()
1063
1072
  if config_key == 'default' or config_key is None:
@@ -1075,16 +1084,7 @@ class MainShell(cmd.Cmd):
1075
1084
  if org_name not in config_objects.keys():
1076
1085
  console.print(f"(!) Error: Organization '{org_name}' not found in the configuration.", style="red")
1077
1086
  return
1078
- self.config = launchpy.configure(
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}> "
1087
+ self._configure_connection(org_name)
1088
1088
  console.print(f"Switched to organization '{org_name}'.", style="green")
1089
1089
  except Exception as e:
1090
1090
  console.print(f"(!) Error: {str(e)}", style="red")
@@ -1097,10 +1097,11 @@ class MainShell(cmd.Cmd):
1097
1097
  parser = argparse.ArgumentParser(prog='create_config_file', add_help=True)
1098
1098
  parser.add_argument("-fn", "--file_name", help="file name for your config file", default="launchpy_config.json",type=str)
1099
1099
  parser.add_argument("-m","--multi_org", help="Enable multi-org configuration", action="store_true", default=False)
1100
+ parser.add_argument("-p","--proxy", help="Enable proxy for the connection", action="store_true", default=False)
1100
1101
  try:
1101
1102
  args = parser.parse_args(shlex.split(arg))
1102
1103
  filename = args.file_name
1103
- launchpy.createConfigFile(filename=filename,multi_org=args.multi_org)
1104
+ launchpy.createConfigFile(filename=filename,proxy=args.proxy,multi_org=args.multi_org)
1104
1105
  filename_json = filename + ".json" if not filename.endswith(".json") else filename
1105
1106
  console.print(f"Configuration file created at {Path.cwd() / Path(filename_json)}", style="green")
1106
1107
  return
@@ -1121,8 +1122,10 @@ class MainShell(cmd.Cmd):
1121
1122
  parser.add_argument("-cf", "--config_file", help="Path to config file", default=None)
1122
1123
  parser.add_argument("-p", "--property", help="Property Name to auto-load on startup", default=None)
1123
1124
  parser.add_argument("-on", "--org_name", help="Organization Name to use from config file", default=None)
1125
+ parser.add_argument("--proxy", help="Proxy URL for both HTTP and HTTPS")
1124
1126
  try:
1125
1127
  args = parser.parse_args(shlex.split(arg))
1128
+ proxy = _validate_proxy(args.proxy)
1126
1129
  if args.config_file is not None:
1127
1130
  config_path = Path(args.config_file)
1128
1131
  if not config_path.is_absolute():
@@ -1136,6 +1139,7 @@ class MainShell(cmd.Cmd):
1136
1139
  self.config[config_key]['org_id'] = dict_config.get("org_id", args.org_id)
1137
1140
  self.config[config_key]['client_id'] = dict_config.get("client_id", dict_config.get("api_key", args.client_id))
1138
1141
  self.config[config_key]['scopes'] = dict_config.get("scopes", args.scopes)
1142
+ self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(dict_config.get("proxy"))
1139
1143
  config_objects.update(self.config)
1140
1144
  elif type(dict_config) == list:
1141
1145
  self.config = {}
@@ -1149,6 +1153,7 @@ class MainShell(cmd.Cmd):
1149
1153
  self.config[config_key]['org_id'] = config.get("org_id", args.org_id)
1150
1154
  self.config[config_key]['client_id'] = config.get("client_id", config.get("api_key", args.client_id))
1151
1155
  self.config[config_key]['scopes'] = config.get("scopes", args.scopes)
1156
+ self.config[config_key]['proxy'] = proxy if proxy is not None else _validate_proxy(config.get("proxy"))
1152
1157
  config_objects.update(self.config)
1153
1158
  config_key = dict_config[0].get("org_name")
1154
1159
  else:
@@ -1160,6 +1165,7 @@ class MainShell(cmd.Cmd):
1160
1165
  if args.org_id: self.config[config_key]['org_id'] = str(args.org_id)
1161
1166
  if args.scopes: self.config[config_key]['scopes'] = str(args.scopes)
1162
1167
  if args.client_id: self.config[config_key]['client_id'] = str(args.client_id)
1168
+ self.config[config_key]['proxy'] = proxy
1163
1169
  config_objects.update(self.config)
1164
1170
  missing = [
1165
1171
  key for key, value in {
@@ -1423,6 +1429,7 @@ def main():
1423
1429
  parser.add_argument("-cf", "--config_file", help="Path to config file", default=None)
1424
1430
  parser.add_argument("-p", "--property", help="Property Name to auto-load on startup", default=None)
1425
1431
  parser.add_argument("-on", "--org_name", help="Organization Name to use from config file", default=None)
1432
+ parser.add_argument("--proxy", help="Proxy URL for both HTTP and HTTPS")
1426
1433
  args = parser.parse_args()
1427
1434
  shell = MainShell(**vars(args))
1428
1435
  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',auth_type: str = "oauthV2", scope: str = "https://ims-na1.adobelogin.com/s/ent_reactor_admin_sdk", multi_org:bool=False, verbose: object = False)->None:
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) -> 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"] = ""