launchpy 0.4.7.post1__tar.gz → 0.4.8__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 (34) hide show
  1. {launchpy-0.4.7.post1/launchpy.egg-info → launchpy-0.4.8}/PKG-INFO +9 -9
  2. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/cli.md +24 -3
  3. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/releases.md +9 -1
  4. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/synchronizer.md +36 -10
  5. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/__init__.py +0 -1
  6. launchpy-0.4.8/launchpy/__version__.py +1 -0
  7. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/cli/__main__.py +74 -21
  8. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/config.py +0 -2
  9. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/configs.py +7 -37
  10. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/connector.py +0 -2
  11. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/library.py +24 -7
  12. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/property.py +10 -0
  13. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/synchronizer.py +192 -34
  14. {launchpy-0.4.7.post1 → launchpy-0.4.8/launchpy.egg-info}/PKG-INFO +9 -9
  15. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy.egg-info/requires.txt +1 -2
  16. {launchpy-0.4.7.post1 → launchpy-0.4.8}/pyproject.toml +9 -9
  17. {launchpy-0.4.7.post1 → launchpy-0.4.8}/requirements.txt +3 -5
  18. launchpy-0.4.7.post1/launchpy/__version__.py +0 -1
  19. {launchpy-0.4.7.post1 → launchpy-0.4.8}/LICENSE +0 -0
  20. {launchpy-0.4.7.post1 → launchpy-0.4.8}/README.md +0 -0
  21. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/admin.md +0 -0
  22. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/getstarted.md +0 -0
  23. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/library.md +0 -0
  24. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/main.md +0 -0
  25. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/property.md +0 -0
  26. {launchpy-0.4.7.post1 → launchpy-0.4.8}/docs/translator.md +0 -0
  27. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/admin.py +0 -0
  28. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/cli/__init__.py +0 -0
  29. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy/launchpy.py +0 -0
  30. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy.egg-info/SOURCES.txt +0 -0
  31. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy.egg-info/dependency_links.txt +0 -0
  32. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy.egg-info/entry_points.txt +0 -0
  33. {launchpy-0.4.7.post1 → launchpy-0.4.8}/launchpy.egg-info/top_level.txt +0 -0
  34. {launchpy-0.4.7.post1 → launchpy-0.4.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchpy
3
- Version: 0.4.7.post1
3
+ Version: 0.4.8
4
4
  Summary: Adobe Launch API wrapper
5
5
  Author-email: Julien Piccini <piccini.julien@gmail.com>
6
6
  License: Apache License 2.0
@@ -8,26 +8,26 @@ Project-URL: homepage, https://github.com/pitchmuc/launchpy
8
8
  Project-URL: changelog, https://github.com/pitchmuc/launchpy/blob/master/docs/releases.md
9
9
  Keywords: adobe,analytics,API,python
10
10
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python
14
14
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
- Classifier: Programming Language :: Python :: 3.6
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
19
20
  Classifier: Development Status :: 4 - Beta
20
- Requires-Python: >=3.7
21
+ Requires-Python: >=3.10
21
22
  Description-Content-Type: text/markdown
22
23
  License-File: LICENSE
23
24
  Requires-Dist: pandas
24
25
  Requires-Dist: requests
25
- Requires-Dist: PyJWT
26
- Requires-Dist: PyJWT[crypto]
27
26
  Requires-Dist: pathlib2
28
27
  Requires-Dist: pathlib
29
28
  Requires-Dist: aepp
30
29
  Requires-Dist: rich
30
+ Requires-Dist: deprecated
31
31
  Dynamic: license-file
32
32
 
33
33
  # Launchpy
@@ -43,6 +43,7 @@ This document provides an overview of the command line interface (CLI) for `laun
43
43
  - [get\_base\_libraries](#get_base_libraries)
44
44
  - [get\_base\_library](#get_base_library)
45
45
  - [sync\_from\_library](#sync_from_library)
46
+ - [Behaviors for sync\_from\_library](#behaviors-for-sync_from_library)
46
47
  - [create\_libraries](#create_libraries)
47
48
 
48
49
 
@@ -479,8 +480,23 @@ synchronizer:Base_Property_Name> get_base_library -n "Library Name"
479
480
  #### sync_from_library
480
481
  This command allows you to synchronize a library from the base property to the target properties.\
481
482
  **IMPORTANT**:By default it will use the published version of the elements that are part of the library.
482
- `-n`, `--name`: The name of the library to sync from. This is a required parameter.
483
- `-id`, `--id`: The ID of the library to sync from (overrides name if both provided). This is an optional parameter.
483
+ * `-n`, `--name`: Name of the library to sync from. This is a required parameter.
484
+ * `-id`, `--id`: ID of the library to sync from (overrides name if both provided). This is an optional parameter.
485
+ * `-f`, `--force`: Boolean. Create the component if it does not exist. Default True. Possible values: True, False
486
+ * `-ll`, `--library_linked`: Boolean. Whether to use library linked components or not. If set to True, the sync will be done using the library linked components. If set to False, the sync will be done using the component IDs in the library. Default True. Possible values: True, False
487
+ * `-p`, `--published`: Boolean. Sync the latest published version of the component instead of the current version. Default False. Possible values: True, False
488
+ * `-dr`, `--dry_run`: Boolean. If set to True, will only check the sync status of the components without actually syncing them. Default False. Possible values: True, False
489
+
490
+ ```bash
491
+ synchronizer:Base_Property_Name> sync_from_library -id "LibraryId" -ll False -p True -dr True
492
+ ```
493
+
494
+
495
+ ### Behaviors for sync_from_library
496
+ By default, the method will sync the components present in the library as they are in the library.
497
+ If the `published` is set to True, it will try to sync the version that has been published in the base property to the targets.\
498
+ If the `library_linked` is set to `False` and the `published` is set to `False` (default), it will try to sync the latest version of the components present in the base property library to the targets.\
499
+
484
500
 
485
501
  ```bash
486
502
  synchronizer:Base_Property_Name> sync_from_library -n "Library Name"
@@ -490,9 +506,14 @@ synchronizer:Base_Property_Name> sync_from_library -n "Library Name"
490
506
  This command allows you to create a library in the destination properties with the elements you have sync.\
491
507
  Arguments:
492
508
  `name`: Name for the new library to be created in the destination properties. This is a required parameter.
493
- `-env`, `--environment`: Boolean. Try to find an empty environment to build the library. Default False. Possible values: `True`, `False`
509
+ `-env`, `--environment`: String. If set to `True`, it will try to find a free environment and assign it to the library. If none is available, it will not assign any environment to the library. If the name of the environment is provided, it will try to assign it to that library. If the environment is already used in a Library, it will remove that association and assign it to the new library. Default False. Possible values: `True`, `False` or a `string` which is the (partial) name of the environment you want to assign to the library.
494
510
 
495
511
  ```bash
496
512
  synchronizer:Base_Property_Name> create_library "Library Name" -env True
497
513
  ```
514
+
515
+ ```bash
516
+ synchronizer:Base_Property_Name> create_library "Library Name" -env "Environment Name
517
+
518
+ ```
498
519
 
@@ -3,8 +3,16 @@
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.4.8
7
+ * Adding the `clear` method in CLI
8
+ * Adding `syncFromLibrary` in synchronizer[doc](./synchronizer.md#syncFromLibrary).
9
+ * Adding `sync_from_library` command in CLI[doc](./cli.md#sync_from_library-command).
10
+ * Cleaning the JWT components and requirements
11
+ * Adding the `definition` attribute in the Library class
12
+ * Adding capability to define a specific environment name when creating a library in synchronizer
13
+
6
14
  ## 0.4.7
7
- * modify `getRuleComponents` to `getRulesComponents` as it can return all the rules components if no rule is specified. Kept `getRuleComponents` as an alias for backward compatibility and also provide a single rule components retrieval.
15
+ * Modify `getRuleComponents` to `getRulesComponents` as it can return all the rules components if no rule is specified. Kept `getRuleComponents` as an alias for backward compatibility and also provide a single rule components retrieval.
8
16
  * Adding the `CLI` layer. [Documentation to CLI](cli.md)
9
17
 
10
18
  ## 0.4.6
@@ -6,6 +6,16 @@ You can recreate a (more advanced) version that supports additional methods or e
6
6
 
7
7
  The Synchronizer will allow you to specify a template property and target property **within the same organization** in order to sync these properties rules and data elements.
8
8
 
9
+ ## Menu
10
+
11
+ [MENUI](#menu)
12
+ [Pre Requisite](#pre-requisite)
13
+ [Instantiation](#instantiation)
14
+ [SyncComponent Method](#syncComponent-Method)
15
+ [SyncComponents](#syncComponents)
16
+ [SyncFromLibrary](#syncFromLibrary)
17
+ [createTargetsLibrary](#createTargetsLibrary)
18
+ [renameComponent](#renameComponent)
9
19
 
10
20
 
11
21
  ## PRE REQUISITE
@@ -85,7 +95,6 @@ synchronizor = lp.Synchronizer(base=base,targets=['another Property Name to sync
85
95
 
86
96
  ```
87
97
 
88
-
89
98
  ### Additional options
90
99
  There is a possibility to setup some rules so you can filter when migrating from one property to another. See [dynamic Filtering](#dynamic-component-filter)
91
100
 
@@ -122,7 +131,7 @@ The method will delete all rule components existing in the Target property for t
122
131
  It will then copy all template rule component existing in that base property to the target properties.\
123
132
  No history is saved from the existing rule component, which will make revision check very hard.
124
133
 
125
- ## SyncComponents Method
134
+ ## SyncComponents
126
135
 
127
136
  You can pass down a list of component names or componentIds.\
128
137
  It takes 3 arguments:\
@@ -131,10 +140,28 @@ Arguments:
131
140
  * componentsId : REQUIRED : The list of component ID to sync*
132
141
  * publishedVersion : OPTIONAL : if you want to take the version that has been published
133
142
 
143
+ ## syncFromLibrary
144
+
145
+ This method allows you to take a library in your base property and sync all of the components in that library to the target properties.
146
+ Arguments:
147
+ * library : REQUIRED : the name or the ID of the library to sync from.
148
+ * state : OPTIONAL : the state of the library to sync from. Default: 'published', possible states: "development", "submitted", "approved", "rejected", "published"
149
+ * force : OPTIONAL : If set to True, it will create the element sync if it does not exist in the target property. Set to `False`, it will prevent that behavior. Default: `True`.
150
+ * libraryLinked : OPTIONAL : If set to True, it will use library linked components for comparison. Default: `True`.
151
+ * publishedVersion : OPTIONAL : If set to True, it will compare the components to the published version in the target properties. Default: `False`.
152
+ * dryRun : OPTIONAL : If set to True, will only check the sync status of the components without actually syncing them. Default `False`. Possible values: `True`, `False`
153
+
154
+ **Note**: A library is not created automatically, so you will need to create a new one via the `createTargetsLibrary` method.
155
+
156
+ ### Behaviors for syncFromLibrary
157
+ By default, the method will sync the components present in the library as they are in the library.
158
+ If the `publishedVersion` is set to True, it will try to sync the version that has been published in the base property to the targets.\
159
+ If the `libraryLinked` is set to `False` and the `publishedVersion` is set to `False` (default), it will try to sync the latest version of the components present in the base property library to the targets.\
160
+
134
161
  ## createTargetsLibrary
135
162
 
136
163
  The synchronizer will automatically track the component that has been updated via the module.\
137
- When you are done synchronizing your components, you can run this method to create a library, that will contain all of your elements you synchronized.\
164
+ When you are done synchronizing your components, you can run this method to create a library, that will contain all the elements you synchronized.\
138
165
  The library will be available within all of your synch properties.
139
166
 
140
167
  You can pass a name to define the library name that is created.\
@@ -142,7 +169,10 @@ The name is then used to match if a library already exist with that name, in the
142
169
  Note that the match looked for is using regular expression (`re.search`) to find a match in the existing libraries name.\
143
170
  Arguments:
144
171
  * name : REQUIRED : Name of the library to create or to use.
145
- * assignEnv : OPTIONAL : If you want to automatically assign an environment to the library and build it. Default False. Possible values: `True`, `False`
172
+ * assignEnv : OPTIONAL : If you want to automatically assign an environment to the library and build it. Default False.
173
+ Possible values: `True`, `False` or a `string` which is the name of the environment you want to assign to the library.\
174
+ If `True`, it will try to find a free environment and assign it to the library. If none is available, it will not assign any environment to the library.\
175
+ If the name of the environment is provided, it will try to assign it to that library. If the environment is already used in a Library, it will remove that association and assign it to the new library. (A print statement will be printed to warn about that change of association)
146
176
 
147
177
  ## renameComponent
148
178
 
@@ -198,14 +228,10 @@ For Data Elements:
198
228
 
199
229
  #### Additional elements
200
230
  On top of the different properties check, additional information will be provided for the base component:
201
- * `base-publihsed`: If the current component check is published.\
231
+ * `base-published`: If the current component check is published.\
202
232
  **IMPORTANT NOTE**: When you using the `publishedVersion` parameter set to True, most of the element will be set to Published. In reverse, it is not because the component current version is not published that it has never been published. Use the `publishedVersion` to ensure this.
203
233
  * `base-enabled`: If the current component check is enabled.
204
- * `base-state`:
205
- * `Latest`: If the component retrieved is the latest latest version
206
- * `Unknown`: If the component retrieved cannot be evaluated to know if it is the latest.
207
- * `Draft` : If the component retrieved is not the latest and it has not yet been published
208
- * `Edited`: If the component retrieved is not the latest and has been published before.
234
+
209
235
 
210
236
  #### Similar vs Same
211
237
  The idea of checking the similarity of 2 components is partially done as not everything can be tested during the check.\
@@ -48,7 +48,6 @@ async def process_all_rules(rules_list, folder, header):
48
48
  async with httpx.AsyncClient(timeout=timeout, limits=limits) as client:
49
49
  # Create a list of "tasks" to run
50
50
  tasks = [__extractRuleComponents__(rule, client, folder, header) for rule in rules_list]
51
-
52
51
  # 'gather' runs them all concurrently
53
52
  await asyncio.gather(*tasks)
54
53
 
@@ -0,0 +1 @@
1
+ __version__ = "0.4.8"
@@ -1,5 +1,4 @@
1
- from rich import rule
2
- import launchpy
1
+ import launchpy
3
2
  import argparse, cmd, shlex, json
4
3
  from functools import wraps
5
4
  from rich.console import Console
@@ -10,6 +9,17 @@ import pandas as pd
10
9
  from datetime import datetime, timedelta
11
10
  from typing import Any, Concatenate, ParamSpec, ParamSpecKwargs
12
11
  from collections.abc import Callable
12
+ import os
13
+
14
+ def str2bool(v):
15
+ if isinstance(v, bool):
16
+ return v
17
+ if v.lower() in ('yes', 'true', 't', 'y', '1'):
18
+ return True
19
+ elif v.lower() in ('no', 'false', 'f', 'n', '0'):
20
+ return False
21
+ else:
22
+ raise argparse.ArgumentTypeError('Boolean value expected.')
13
23
 
14
24
  P = ParamSpec("P")
15
25
 
@@ -33,6 +43,11 @@ def login_required(f:Callable[Concatenate["MainShell", P], None]) -> Callable[Co
33
43
  return f(self, *args, **kwargs)
34
44
  return wrapper
35
45
 
46
+ def clear_terminal():
47
+ """Clears the terminal screen based on the OS."""
48
+ # os.name is 'nt' for Windows, 'posix' for Mac/Linux
49
+ os.system('cls' if os.name == 'nt' else 'clear')
50
+
36
51
  console = Console()
37
52
 
38
53
  class PropertyCLI(cmd.Cmd):
@@ -48,7 +63,7 @@ class PropertyCLI(cmd.Cmd):
48
63
  def do_get_extensions(self, arg):
49
64
  """Get all extensions in the property and list them."""
50
65
  parser = argparse.ArgumentParser(prog='get_extensions', add_help=True)
51
- parser.add_argument("-s", "--save", help="Boolean. Save extensions to a CSV file. Default False. Possible values: True, False", type=bool, default=False)
66
+ parser.add_argument("-s", "--save", help="Boolean. Save extensions to a CSV file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
52
67
  try:
53
68
  args = parser.parse_args(shlex.split(arg))
54
69
  extensions = self.property.getExtensions()
@@ -82,7 +97,7 @@ class PropertyCLI(cmd.Cmd):
82
97
  """Get details for a specific extension by name."""
83
98
  parser = argparse.ArgumentParser(prog='get_extension', add_help=True)
84
99
  parser.add_argument("name", help="Name of the extension to get details for", type=str)
85
- parser.add_argument("-s", "--save", help="Boolean. Save extension details to a JSON file. Default False. Possible values: True, False", type=bool, default=False)
100
+ parser.add_argument("-s", "--save", help="Boolean. Save extension details to a JSON file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
86
101
  try:
87
102
  args = parser.parse_args(shlex.split(args))
88
103
  extensions = self.property.getExtensions()
@@ -107,7 +122,7 @@ class PropertyCLI(cmd.Cmd):
107
122
  """Get all rules for the property."""
108
123
  parser = argparse.ArgumentParser(prog='get_rules', add_help=True)
109
124
  parser.add_argument("-n", "--name", help="Filter rules by name (partial match, non-case sensitive)", type=str, default=None)
110
- parser.add_argument("-s", "--save", help="Boolean. Save rules to a CSV file. Default False. Possible values: True, False", type=bool, default=False)
125
+ parser.add_argument("-s", "--save", help="Boolean. Save rules to a CSV file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
111
126
  try:
112
127
  args = parser.parse_args(shlex.split(arg))
113
128
  rules = self.property.getRules()
@@ -143,7 +158,7 @@ class PropertyCLI(cmd.Cmd):
143
158
  parser = argparse.ArgumentParser(prog='get_rule', add_help=True)
144
159
  parser.add_argument("-n", "--name", help="Name of the rule to get details for", type=str)
145
160
  parser.add_argument("-id", "--id", help="ID of the rule to get details for (overrides name if both provided)", type=str)
146
- parser.add_argument("-s", "--save", help="Boolean. Save rule details to a JSON file. Default False. Possible values: True, False", type=bool, default=False)
161
+ parser.add_argument("-s", "--save", help="Boolean. Save rule details to a JSON file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
147
162
  try:
148
163
  args = parser.parse_args(shlex.split(arg))
149
164
  if self.rules is None:
@@ -180,7 +195,7 @@ class PropertyCLI(cmd.Cmd):
180
195
  parser = argparse.ArgumentParser(prog='get_rules_components', add_help=True)
181
196
  parser.add_argument("-rn", "--rule_name", help="(Partial) Name of the rule to get components for", type=str,default=None)
182
197
  parser.add_argument("-rid", "--rule_id", help="ID of the rule to get components for (overrides rule_name if both provided)", type=str, default=None)
183
- parser.add_argument("-s", "--save", help="Boolean. Save components to a CSV file. Default False. Possible values: True, False", type=bool, default=False)
198
+ parser.add_argument("-s", "--save", help="Boolean. Save components to a CSV file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
184
199
  try:
185
200
  args = parser.parse_args(shlex.split(arg))
186
201
  if self.rules_components is None:
@@ -244,7 +259,7 @@ class PropertyCLI(cmd.Cmd):
244
259
  parser = argparse.ArgumentParser(prog='get_rule_components', add_help=True)
245
260
  parser.add_argument("-rn", "--rule_name", help="(Partial) Name of the rule to get components for", type=str, default=None)
246
261
  parser.add_argument("-rid", "--rule_id", help="ID of the rule to get components for (overrides rule_name if both provided)", type=str, default=None)
247
- parser.add_argument("-s", "--save", help="Boolean. Save components to a JSON file. Default False. Possible values: True, False", type=bool, default=False)
262
+ parser.add_argument("-s", "--save", help="Boolean. Save components to a JSON file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
248
263
  try:
249
264
  args = parser.parse_args(shlex.split(arg))
250
265
  rule = None ## fallback
@@ -285,7 +300,7 @@ class PropertyCLI(cmd.Cmd):
285
300
  """Get all data elements, or only the ones matching a name filter, for the property. Can save them in CSV file."""
286
301
  parser = argparse.ArgumentParser(prog='get_data_elements', add_help=True)
287
302
  parser.add_argument("-n", "--name", help="Filter data elements by name (partial match, non-case sensitive)", type=str, default=None)
288
- parser.add_argument("-s", "--save", help="Boolean. Save data elements to a CSV file. Default False. Possible values: True, False", type=bool, default=False)
303
+ parser.add_argument("-s", "--save", help="Boolean. Save data elements to a CSV file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
289
304
  try:
290
305
  args = parser.parse_args(shlex.split(args))
291
306
  des = self.property.getDataElements()
@@ -320,7 +335,7 @@ class PropertyCLI(cmd.Cmd):
320
335
  """Get details for a specific data element by name."""
321
336
  parser = argparse.ArgumentParser(prog='get_data_element', add_help=True)
322
337
  parser.add_argument("name", help="Name of the data element to get details for", type=str)
323
- parser.add_argument("-s", "--save", help="Boolean. Save data element details to a JSON file. Default False. Possible values: True, False", type=bool, default=False)
338
+ parser.add_argument("-s", "--save", help="Boolean. Save data element details to a JSON file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
324
339
  try:
325
340
  args = parser.parse_args(shlex.split(args))
326
341
  if self.data_elements is None:
@@ -349,7 +364,7 @@ class PropertyCLI(cmd.Cmd):
349
364
  parser = argparse.ArgumentParser(prog='get_latest_published_version', add_help=True)
350
365
  parser.add_argument("-n", "--name", help="Name of the component to get the latest published version for", type=str, default=None)
351
366
  parser.add_argument("-t", "--type", help="Type of the component (e.g. 'rule', 'data_element', 'extension') to get the latest published version for. Default None", type=str, default=None)
352
- parser.add_argument("-s", "--save", help="Boolean. Save the latest published version details to a JSON file. Default False. Possible values: True, False", type=bool, default=False)
367
+ parser.add_argument("-s", "--save", help="Boolean. Save the latest published version details to a JSON file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
353
368
  try:
354
369
  args = parser.parse_args(shlex.split(arg))
355
370
  if args.type and args.name is None:
@@ -419,7 +434,7 @@ class PropertyCLI(cmd.Cmd):
419
434
  def do_get_libraries(self, arg: Any):
420
435
  """Get all libraries for the property."""
421
436
  parser = argparse.ArgumentParser(prog='get_libraries', add_help=True)
422
- parser.add_argument("-s", "--save", help="Boolean. Save libraries to a CSV file. Default False. Possible values: True, False", type=bool, default=False)
437
+ parser.add_argument("-s", "--save", help="Boolean. Save libraries to a CSV file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
423
438
  parser.add_argument("-st", "--state", help="Filter by library state. Possible values: 'development' (default), 'submitted', 'approved', 'rejected', 'published'", type=str, default="development")
424
439
  try:
425
440
  args = parser.parse_args(shlex.split(arg))
@@ -507,6 +522,10 @@ class PropertyCLI(cmd.Cmd):
507
522
  except SystemExit:
508
523
  return
509
524
 
525
+ def do_clear(self, arg) -> None:
526
+ """Clear the terminal screen."""
527
+ clear_terminal()
528
+
510
529
  def do_exit(self, arg):
511
530
  """Return to the main menu."""
512
531
  console.print(Panel("Returning to main menu..."),style="blue")
@@ -529,7 +548,7 @@ class SynchronizerCLI(cmd.Cmd):
529
548
  parser = argparse.ArgumentParser(prog='check_component', add_help=True)
530
549
  parser.add_argument("-n", "--name", help="Name of the component to check", type=str)
531
550
  parser.add_argument("-id", "--id", help="ID of the component to check (overrides name if both provided)", type=str)
532
- parser.add_argument("-p", "--published", help="Boolean. Check the latest published version of the component instead of the current version. Default False. Possible values: True, False", type=bool, default=False)
551
+ parser.add_argument("-p", "--published", help="Boolean. Check the latest published version of the component instead of the current version. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
533
552
  try:
534
553
  args = parser.parse_args(shlex.split(ars))
535
554
  if args.id is not None:
@@ -548,8 +567,8 @@ class SynchronizerCLI(cmd.Cmd):
548
567
  parser = argparse.ArgumentParser(prog='sync', add_help=True)
549
568
  parser.add_argument("-n", "--name", help="Name of the component to sync", type=str)
550
569
  parser.add_argument("-id", "--id", help="ID of the component to sync (overrides name if both provided)", type=str)
551
- parser.add_argument("-p", "--published", help="Boolean. Sync the latest published version of the component instead of the current version. Default False. Possible values: True, False", type=bool, default=False)
552
- parser.add_argument("-f", "--force", help="Boolean. Create the component if it does not exist. Default True. Possible values: True, False", type=bool, default=True)
570
+ parser.add_argument("-p", "--published", help="Boolean. Sync the latest published version of the component instead of the current version. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
571
+ parser.add_argument("-f", "--force", help="Boolean. Create the component if it does not exist. Default True. Possible values: True, False", type=str2bool, default=True)
553
572
  try:
554
573
  args = parser.parse_args(shlex.split(args))
555
574
  if args.id is not None:
@@ -741,12 +760,22 @@ class SynchronizerCLI(cmd.Cmd):
741
760
  return
742
761
 
743
762
  def do_sync_from_library(self,args:Any):
744
- """Sync all components from a base library in the source property to the destination properties. Rules and data elements will be the latest published one"""
763
+ """Sync all components from a base library in the source property to the destination properties.
764
+ Rules and data elements will be the library version by default. Can be changed with --ll parameter."""
745
765
  parser = argparse.ArgumentParser(prog='sync_from_library', add_help=True)
746
766
  parser.add_argument("-n", "--name", help="Name of the library to sync from", type=str)
747
767
  parser.add_argument("-id", "--id", help="ID of the library to sync from (overrides name if both provided)", type=str)
768
+ parser.add_argument("-f", "--force", help="Boolean. Create the component if it does not exist. Default True. Possible values: True, False", type=str2bool, default=True)
769
+ parser.add_argument("-ll", "--library_linked", help="Boolean. Whether to use library linked components or not. If set to True, the sync will be done using the library linked components. If set to False, the sync will be done using the component IDs in the library. Default True. Possible values: True, False", type=str, default=True)
770
+ parser.add_argument("-p", "--published", help="Boolean. Sync the latest published version of the component instead of the current version. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False,)
771
+ parser.add_argument("-dr", "--dry_run", help="Boolean. If set to True, will only check the sync status of the components without actually syncing them. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False,)
748
772
  try:
749
773
  args = parser.parse_args(shlex.split(args))
774
+ publishedVersion = args.published # default False
775
+ libraryLinked = args.library_linked # default True
776
+ if args.published:
777
+ publishedVersion = True
778
+ libraryLinked = False
750
779
  if args.id is not None:
751
780
  lib = self.synchronizer.base["api"].getLibrary(args.id, return_class=True)
752
781
  else:
@@ -756,6 +785,10 @@ class SynchronizerCLI(cmd.Cmd):
756
785
  console.print(f"Library '{args.name}' not found in this property.", style="red")
757
786
  return
758
787
  lib = self.synchronizer.base["api"].getLibrary(matching_libs[0]['id'], return_class=True)
788
+ if args.dry_run:
789
+ results = self.synchronizer.__checkLibrarySync__(library=lib.id, state=lib.state, excludeSimilar=False,libraryLinked=libraryLinked,publishedVersion=publishedVersion)
790
+ console.print_json(json.dumps(results, indent=4))
791
+ return
759
792
  rules = lib.getRules()
760
793
  dataelements = lib.getDataElements()
761
794
  extensions = lib.getExtensions()
@@ -771,7 +804,11 @@ class SynchronizerCLI(cmd.Cmd):
771
804
  console.print(f"Syncing {len(rules)} rule{'s' if len(rules) > 1 else ''} from library '{lib.name}'...", style="blue")
772
805
  for rule in rules:
773
806
  try:
774
- self.synchronizer.syncComponent(componentId=rule['id'], publishedVersion=True, forceCreation=True)
807
+ if args.library_linked:
808
+ ruleId = rule['links']['self'].split('/').pop()
809
+ else:
810
+ ruleId = rule['id']
811
+ self.synchronizer.syncComponent(componentId=ruleId, publishedVersion=publishedVersion, forceCreation=args.force,libraryLinked=libraryLinked)
775
812
  console.print(f"Rule '{rule['attributes']['name']}' synced successfully.", style="green")
776
813
  except Exception as e:
777
814
  console.print(f"(!) Error syncing rule '{rule['attributes']['name']}': {str(e)}", style="red")
@@ -779,7 +816,11 @@ class SynchronizerCLI(cmd.Cmd):
779
816
  console.print(f"Syncing {len(dataelements)} data element{'s' if len(dataelements) > 1 else ''} from library '{lib.name}'...", style="blue")
780
817
  for de in dataelements:
781
818
  try:
782
- self.synchronizer.syncComponent(componentId=de['id'], publishedVersion=True, forceCreation=True)
819
+ if args.library_linked:
820
+ deId = de['links']['self'].split('/').pop()
821
+ else:
822
+ deId = de['id']
823
+ self.synchronizer.syncComponent(componentId=deId, publishedVersion=publishedVersion, forceCreation=args.force,libraryLinked=libraryLinked)
783
824
  console.print(f"Data element '{de['attributes']['name']}' synced successfully.", style="green")
784
825
  except Exception as e:
785
826
  console.print(f"(!) Error syncing data element '{de['attributes']['name']}': {str(e)}", style="red")
@@ -793,9 +834,13 @@ class SynchronizerCLI(cmd.Cmd):
793
834
  """Create libraries in the destination property based on the libraries in the source property that are selected for synchronization."""
794
835
  parser = argparse.ArgumentParser(prog='create_libraries', add_help=True)
795
836
  parser.add_argument("name",help="Name for the new library to be created in the destination properties", type=str)
796
- parser.add_argument("-env",'--environment',help='Boolean. try to find an empty environment to build the library. Default False.',type=bool,default=False)
837
+ parser.add_argument("-env",'--environment',help='Set to True to try to assign an available environment. Set a string with name of an environment to set a specific environment. Default False',type=str,default=False)
797
838
  try:
798
839
  args = parser.parse_args(shlex.split(args))
840
+ if args.environment == "True":
841
+ args.environment = True
842
+ if args.environment == "False":
843
+ args.environment = False
799
844
  res = self.synchronizer.createTargetsLibrary(name=args.name,assignEnv=args.environment)
800
845
  console.print_json(json.dumps(res, indent=4))
801
846
  except Exception as e:
@@ -804,6 +849,10 @@ class SynchronizerCLI(cmd.Cmd):
804
849
  except SystemExit:
805
850
  return
806
851
 
852
+ def do_clear(self, arg) -> None:
853
+ """Clear the terminal screen."""
854
+ clear_terminal()
855
+
807
856
  def do_exit(self, arg):
808
857
  """Return to the main menu."""
809
858
  console.print(Panel("Returning to main menu...", style="blue"))
@@ -965,7 +1014,7 @@ class MainShell(cmd.Cmd):
965
1014
  """List all properties available in the connected AEP instance."""
966
1015
  parser = argparse.ArgumentParser(prog='get_properties', add_help=True)
967
1016
  parser.add_argument("-n", "--name", help="Filter properties by name (partial match, non-case sensitive)", type=str, default=None)
968
- parser.add_argument("-s", "--save", help="Boolean. Save properties to a CSV file. Default False. Possible values: True, False", type=bool, default=False)
1017
+ parser.add_argument("-s", "--save", help="Boolean. Save properties to a CSV file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
969
1018
  try:
970
1019
  args = parser.parse_args(shlex.split(arg))
971
1020
  properties = self.admin.getProperties(self.cid)
@@ -1094,7 +1143,7 @@ class MainShell(cmd.Cmd):
1094
1143
  def do_get_extensions(self, arg:Any) -> None:
1095
1144
  """List all available extensions in the connected Adobe Launch organization."""
1096
1145
  parser = argparse.ArgumentParser(prog='get_extensions', add_help=True)
1097
- parser.add_argument("-s", "--save", help="Boolean. Save extensions to a JSON file. Default False. Possible values: True, False", type=bool, default=False)
1146
+ parser.add_argument("-s", "--save", help="Boolean. Save extensions to a JSON file. Default False. Possible values: True, False", type=str2bool, nargs="?", const=True, default=False)
1098
1147
  try:
1099
1148
  args = parser.parse_args(shlex.split(arg))
1100
1149
  extensions = self.admin.getExtensionsCatalogue()
@@ -1144,6 +1193,10 @@ class MainShell(cmd.Cmd):
1144
1193
  except SystemExit:
1145
1194
  return
1146
1195
 
1196
+ def do_clear(self, arg:Any) -> None:
1197
+ """Clear the terminal screen."""
1198
+ clear_terminal()
1199
+
1147
1200
  def do_exit(self, arg:Any) -> bool:
1148
1201
  """Exit the application."""
1149
1202
  console.print(Panel("Exiting...", style="blue"))
@@ -3,9 +3,7 @@ token = ""
3
3
  config_object = {
4
4
  "org_id": "",
5
5
  "api_key": "",
6
- "pathToKey": "",
7
6
  "secret": "",
8
- "jwtTokenEndpoint" : "https://ims-na1.adobelogin.com/ims/exchange/jwt",
9
7
  "oauthTokenEndpointV2" : "https://ims-na1.adobelogin.com/ims/token/v2",
10
8
  "date_limit" : 0,
11
9
  "scope_admin" : "https://ims-na1.adobelogin.com/s/ent_reactor_admin_sdk",
@@ -1,7 +1,7 @@
1
1
  import json, os
2
2
  from pathlib import Path
3
3
  from typing import Optional
4
- from .config import config_object, header
4
+ from launchpy.config import config_object, header
5
5
 
6
6
  def find_path(path: str) -> Optional[Path]:
7
7
  """Checks if the file denoted by the specified `path` exists and returns the Path object
@@ -30,7 +30,7 @@ def createConfigFile(filename:str='config_launch',auth_type: str = "oauthV2", sc
30
30
  scope="https://ims-na1.adobelogin.com/s/ent_reactor_admin_sdk"
31
31
  or
32
32
  scope="https://ims-na1.adobelogin.com/s/ent_reactor_sdk"
33
- auth_type : OPTIONAL : The type of Oauth type you want to use for your config file. Possible value: "jwt" or "oauthV2"
33
+ auth_type : OPTIONAL : The type of Oauth type you want to use for your config file. Possible value: "oauthV2"
34
34
  """
35
35
  json_data = {
36
36
  'org_id': '<orgID>',
@@ -39,10 +39,6 @@ def createConfigFile(filename:str='config_launch',auth_type: str = "oauthV2", sc
39
39
  }
40
40
  if auth_type == 'oauthV2':
41
41
  json_data['scopes'] = "<scopes>"
42
- elif auth_type == 'jwt':
43
- json_data["tech_id"] = "<something>@techacct.adobe.com"
44
- json_data["pathToKey"] = "<path/to/your/privatekey.key>"
45
- json_data['scope'] = scope
46
42
  if '.json' not in filename:
47
43
  filename = f"{filename}.json"
48
44
  with open(filename, 'w') as cf:
@@ -84,8 +80,6 @@ def importConfigFile(path: str = None,auth_type:str=None) -> None:
84
80
  if auth_type is None:
85
81
  if 'scopes' in provided_keys:
86
82
  auth_type = 'oauthV2'
87
- elif 'tech_id' in provided_keys and "pathToKey" in provided_keys:
88
- auth_type = 'jwt'
89
83
  args = {
90
84
  "org_id" : provided_config['org_id'],
91
85
  "secret" : provided_config['secret'],
@@ -94,9 +88,6 @@ def importConfigFile(path: str = None,auth_type:str=None) -> None:
94
88
  }
95
89
  if auth_type == 'oauthV2':
96
90
  args["scopes"] = provided_config["scopes"].replace(' ','')
97
- if auth_type == 'jwt':
98
- args["tech_id"] = provided_config["tech_id"]
99
- args["path_to_key"] = provided_config["pathToKey"]
100
91
  configure(**args)
101
92
 
102
93
 
@@ -129,19 +120,17 @@ def configure(org_id: str = None,
129
120
  tech_id: str = None,
130
121
  secret: str = None,
131
122
  client_id: str = None,
132
- path_to_key: str=None,
133
- private_key: str = None,
134
123
  scopes : str= None,
135
124
  scope: str="https://ims-na1.adobelogin.com/s/ent_reactor_admin_sdk",
125
+ **kwargs
136
126
  ):
137
127
  """Performs programmatic configuration of the API using provided values.
138
128
  Arguments:
139
129
  org_id : REQUIRED : Organization ID
140
130
  tech_id : REQUIRED : Technical Account ID
141
131
  secret : REQUIRED : secret generated for your connection
142
- client_id : REQUIRED : The client_id (old api_key) provided by the JWT connection.
143
- path_to_key : REQUIRED : If you have a file containing your private key value.
144
- private_key : REQUIRED : If you do not use a file but pass a variable directly.
132
+ client_id : REQUIRED : The client_id (old api_key) provided by the Adobe Project.
133
+ scopes : REQUIRED : The scopes required for the Oauth connection
145
134
  scope : OPTIONAL : Scope that is needed for JWT auth.
146
135
  Possible scope: https://www.adobe.io/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/JWT/Scopes.md
147
136
  """
@@ -149,37 +138,18 @@ def configure(org_id: str = None,
149
138
  raise ValueError("`org_id` must be specified in the configuration.")
150
139
  if not client_id:
151
140
  raise ValueError("`client_id` must be specified in the configuration.")
152
- if not tech_id and not scopes:
153
- raise ValueError("`tech_id` must be specified in the configuration.")
141
+ if not scopes:
142
+ raise ValueError("`scopes` must be specified in the configuration.")
154
143
  if not secret:
155
144
  raise ValueError("`secret` must be specified in the configuration.")
156
- if not path_to_key and not private_key and not scopes:
157
- raise ValueError("`scopes` must be specified if Oauth setup.\n `pathToKey` or `private_key` must be specified in the configuration if JWT setup.")
158
145
  config_object["org_id"] = org_id
159
146
  header["x-gw-ims-org-id"] = org_id
160
147
  config_object["client_id"] = client_id
161
148
  header["x-api-key"] = client_id
162
149
  config_object["tech_id"] = tech_id
163
150
  config_object["secret"] = secret
164
- config_object["pathToKey"] = path_to_key
165
- config_object["private_key"] = private_key
166
151
  config_object["official_scope"] = scope
167
152
  config_object["scopes"] = scopes
168
153
  # ensure the reset of the state by overwriting possible values from previous import.
169
154
  config_object["date_limit"] = 0
170
155
  config_object["token"] = ""
171
-
172
-
173
- def get_private_key_from_config(config: dict) -> str:
174
- """
175
- Returns the private key directly or read a file to return the private key.
176
- """
177
- private_key = config.get('private_key')
178
- if private_key is not None:
179
- return private_key
180
- private_key_path = find_path(config['pathToKey'])
181
- if private_key_path is None:
182
- raise FileNotFoundError(f'Unable to find the private key under path `{config["pathToKey"]}`.')
183
- with open(Path(private_key_path), 'r') as f:
184
- private_key = f.read()
185
- return private_key
@@ -85,8 +85,6 @@ class AdobeRequest:
85
85
  if now > self.config['date_limit']:
86
86
  if self.connectionType =='oauthV2':
87
87
  token_and_expiry = self.get_oauth_token_and_expiry_for_config(config=self.config)
88
- elif self.connectionType == 'jwt':
89
- token_and_expiry = self.get_jwt_token_and_expiry_for_config(config=self.config)
90
88
  token = token_and_expiry['token']
91
89
  self.config['token'] = deepcopy(token)
92
90
  self.config['date_limit'] = deepcopy(time.time() + token_and_expiry['expiry'] - 500)
@@ -2,6 +2,8 @@ import time
2
2
  # Non standard libraries
3
3
  from launchpy import config, connector
4
4
  from typing import Union
5
+ from copy import deepcopy
6
+ import json
5
7
 
6
8
  class Library:
7
9
  """
@@ -26,6 +28,7 @@ class Library:
26
28
  self.name = data['attributes']['name']
27
29
  self.state = data['attributes']['state']
28
30
  self.build_required = data['attributes']['build_required']
31
+ self.definition = deepcopy(data)
29
32
  self._DataElements = config.endpoints['global'] + \
30
33
  '/libraries/'+data['id']+'/data_elements'
31
34
  self._Extensions = config.endpoints['global'] + \
@@ -40,6 +43,12 @@ class Library:
40
43
  self.relationships = {}
41
44
  self._environments = {}
42
45
  self._dev_env = ''
46
+
47
+ def __str__(self):
48
+ return json.dumps(self.definition, indent=4)
49
+
50
+ def __repr__(self):
51
+ return json.dumps(self.definition, indent=4)
43
52
 
44
53
  def getDataElements(self,page:int=0,pageSize:int=50,origin:bool=True)->list:
45
54
  """
@@ -355,13 +364,24 @@ class Library:
355
364
  res = self._setEnvironment(obj,verbose=verbose)
356
365
  return res
357
366
 
358
- def _removeEnvironment(self)->None:
367
+ def removeEnvironment(self)->None:
359
368
  """
360
369
  Remove environment
361
370
  """
362
- path = f'/libraries/{self.id}/relationships/environment'
363
- new_env = self.connector.getData(self.endpoint+path)
364
- return new_env
371
+ path = f'/libraries/{self.id}'
372
+ data = {
373
+ "data": {
374
+ "id": self.id,
375
+ "type": "libraries",
376
+ "relationships": {
377
+ "environment": {
378
+ "data": None
379
+ }
380
+ }
381
+ }
382
+ }
383
+ res = self.connector.patchData(self.endpoint+path, data=data)
384
+ return res
365
385
 
366
386
  def updateLibrary(self,empty:bool=False)->dict:
367
387
  """
@@ -418,7 +438,6 @@ class Library:
418
438
  "type": "environments"
419
439
  }
420
440
  }
421
- self._removeEnvironment()
422
441
  status = self._setEnvironment(obj,verbose=verbose)
423
442
  elif self.state == 'submitted':
424
443
  env = 'staging'
@@ -428,7 +447,6 @@ class Library:
428
447
  "type": "environments"
429
448
  }
430
449
  }
431
- self._removeEnvironment()
432
450
  status = self._setEnvironment(obj,verbose=verbose)
433
451
  elif self.state == 'approved':
434
452
  env = 'production'
@@ -438,7 +456,6 @@ class Library:
438
456
  "type": "environments"
439
457
  }
440
458
  }
441
- self._removeEnvironment()
442
459
  status = self._setEnvironment(obj,verbose=verbose)
443
460
  if 'error' in status.keys():
444
461
  raise SystemExit('Issue setting environment')
@@ -1334,6 +1334,16 @@ class Property:
1334
1334
  envs = self.getEnvironments()
1335
1335
  staging = [env for env in envs if env['attributes']['stage'] == 'staging'][0]
1336
1336
  return staging['meta']['script_sources'][0]["license_path"]
1337
+
1338
+ def getEnvironmentLibrary(self,env_id:str)->dict:
1339
+ """
1340
+ Returns the library URL to use on the website for a specific environment.
1341
+ Arguments:
1342
+ env_id : REQUIRED : Environment ID
1343
+ """
1344
+ path = f"/environments/{env_id}/library"
1345
+ res = self.connector.getData('https://reactor.adobe.io'+path)
1346
+ return res.get('data',{})
1337
1347
 
1338
1348
  def extensionsInfo(data: list)->dict:
1339
1349
  """
@@ -1,10 +1,9 @@
1
1
  import re,json
2
2
  # Non standard libraries
3
- from .admin import Admin
4
- from .property import Property
5
- from .library import Library
6
- from .launchpy import Translator, copySettings
7
- from collections import defaultdict
3
+ from launchpy.admin import Admin
4
+ from launchpy.property import Property
5
+ from launchpy.library import Library
6
+ from launchpy.launchpy import Translator, copySettings
8
7
  from copy import deepcopy
9
8
  from aepp import som
10
9
 
@@ -126,31 +125,52 @@ class Synchronizer:
126
125
  'inclComponents': deepcopy(self.dict_config[rule]['inclComponents'])
127
126
  }
128
127
 
129
- def __prepareBaseComponent__(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False)->dict:
128
+ def __prepareBaseComponent__(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,**kwargs)->dict:
130
129
  """
131
130
  Prepare the base component to be used in the syncComponent method.
132
131
  Arguments:
133
132
  componentName : REQUIRED : the name of the component to sync
134
133
  componentID : REQUIRED : the id of the component to sync
135
134
  publishedVersion : OPTIONAL : if you want to take the version that has been published
135
+ kwargs:
136
+ libraryLinked : OPTIONAL : If you want to pass ID that are associated to a specific library. Default: False
136
137
  """
137
138
  cmp_base=None
139
+ componentBase = None
138
140
  if componentId is not None:
139
141
  if componentId.startswith('DE'): ## if data element
142
+ if kwargs.get('libraryLinked',False):
143
+ lib_cmp_base = self.base['api'].getDataElement(componentId)
144
+ if lib_cmp_base is None:
145
+ raise KeyError("Component ID cannot be found")
146
+ componentId = lib_cmp_base['links']['origin'].split('/').pop()
140
147
  componentBase = [de for de in self.base['dataElements'] if de['id'] == componentId]
141
148
  if len(componentBase)==0:
142
149
  raise KeyError("Component ID cannot be found")
143
- cmp_base = componentBase[0]
150
+ if type(componentBase) == list and len(componentBase)>0:
151
+ cmp_base = componentBase[0]
144
152
  elif componentId.startswith('RL'): ## if a rule
153
+ if kwargs.get('libraryLinked',False):
154
+ lib_cmp_base = self.base['api'].getRule(componentId)
155
+ if lib_cmp_base is None:
156
+ raise KeyError("Component ID cannot be found")
157
+ componentId = lib_cmp_base['links']['origin'].split('/').pop()
145
158
  componentBase = [de for de in self.base['rules'] if de['id'] == componentId]
146
159
  if len(componentBase)==0:
147
160
  raise KeyError("Component ID cannot be found")
148
- cmp_base = componentBase[0]
161
+ if type(componentBase) == list and len(componentBase)>0:
162
+ cmp_base = componentBase[0]
149
163
  elif componentId.startswith('EX'): ## if an extension
164
+ if kwargs.get('libraryLinked',False):
165
+ lib_cmp_base = self.base['api'].getExtension(componentId)
166
+ if lib_cmp_base is None:
167
+ raise KeyError("Component ID cannot be found")
168
+ componentId = lib_cmp_base['links']['origin'].split('/').pop()
150
169
  componentBase = [ext for ext in self.base['extensions'] if ext['id'] == componentId]
151
170
  if len(componentBase)==0:
152
- raise KeyError("Component ID cannot be found")
153
- cmp_base = componentBase[0]
171
+ raise KeyError("Component ID cannot be found")
172
+ if type(componentBase) == list and len(componentBase)>0:
173
+ cmp_base = componentBase[0]
154
174
  if componentId is None and componentName is not None: ## If only componentName
155
175
  for rule in self.base['rules']:
156
176
  if componentName == rule['attributes']['name']:
@@ -174,7 +194,7 @@ class Synchronizer:
174
194
  data = self.base['api'].getRevisions(cmp_baseDict['component'])
175
195
  publishedVersion = self.base['api'].getLatestPublishedVersion(data)
176
196
  if publishedVersion['attributes']['name'] != cmp_baseDict['name']:
177
- ## Updating mapping table with old name when published version name diff than last versio name.
197
+ ## Updating mapping table with old name when published version name diff than last version name.
178
198
  if cmp_baseDict['component']['type'] == 'rules':
179
199
  self.translator.extendBaseRules(
180
200
  ruleName=publishedVersion['attributes']['name'],
@@ -183,10 +203,18 @@ class Synchronizer:
183
203
  self.base['rules'].append(publishedVersion)
184
204
  if cmp_baseDict['component']['type'] == 'data_elements':
185
205
  self.base['dataElements'].append(publishedVersion)
186
- cmp_baseDict['id'] = publishedVersion['id']
187
- cmp_baseDict['name'] = publishedVersion['attributes']['name']
188
- cmp_baseDict['component'] = publishedVersion
189
- cmp_baseDict['copy'] = copySettings(publishedVersion)
206
+ cmp_baseDict = {'id':publishedVersion['id'],'name':publishedVersion['attributes']['name'],'component':publishedVersion,'copy':copySettings(publishedVersion)}
207
+ if kwargs.get('libraryLinked',False) and cmp_baseDict['component']['type'] in ['rules','data_elements']:
208
+ if lib_cmp_base['attributes']['name'] != cmp_baseDict['name']:
209
+ if cmp_baseDict['component']['type'] == 'rules':
210
+ self.translator.extendBaseRules(
211
+ ruleName=lib_cmp_base['attributes']['name'],
212
+ ruleId=lib_cmp_base['id'],
213
+ property_name=self.base["name"])
214
+ self.base['rules'].append(lib_cmp_base)
215
+ if cmp_baseDict['component']['type'] == 'data_elements':
216
+ self.base['dataElements'].append(lib_cmp_base)
217
+ cmp_baseDict = {'id':lib_cmp_base['id'],'name':lib_cmp_base['attributes']['name'],'component':lib_cmp_base,'copy':copySettings(lib_cmp_base)}
190
218
  return cmp_baseDict
191
219
 
192
220
  def syncComponent(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,forceCreation:bool=True,**kwargs)->None:
@@ -202,11 +230,13 @@ class Synchronizer:
202
230
  forceCreation : OPTIONAL : If the component does not exist in the target property, create it. Default: True
203
231
  possible kwargs:
204
232
  timeout : OPTIONAL : The timeout to be used for the rule component. If not provided, the existing timeout will be used.
233
+ libraryLinked : OPTIONAL : If you want to take a component ID that are associated to a specific library. Default: False. If set to True, the method will look for the component in the library and take the version linked to the library instead of the latest version in the property. Do not work for Extensions.
205
234
  """
206
235
  timeout = kwargs.get('timeout',None)
236
+ libraryLinked = kwargs.get('libraryLinked',False)
207
237
  if componentName is None and componentId is None:
208
238
  raise ValueError('Require a component Name of a component ID')
209
- cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion)
239
+ cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion,libraryLinked=libraryLinked)
210
240
  ## handling the data element
211
241
  if cmp_baseDict['component']['type'] == 'data_elements':
212
242
  for target in list(self.targets.keys()):
@@ -369,14 +399,15 @@ class Synchronizer:
369
399
  for component in componentsId:
370
400
  self.syncComponent(componentId=component,publishedVersion=publishedVersion)
371
401
 
372
- def createTargetsLibrary(self,name:str="syncComponents",assignEnv:bool=False)->None:
402
+ def createTargetsLibrary(self,name:str="syncComponents",assignEnv:bool|str=False)->None:
373
403
  """
374
404
  This method will create or update a Library in all of the target properties to gather all elements changed.
375
405
  If a library exists and **contains** the same name, it will be used.
376
406
  Argument:
377
407
  name : REQUIRED : The name of the library to create. Default : "syncComponents"
378
- assignEnv : REQUIRED : If you want to assign a library to an environment. By default, the library is not assigned to any environment.
408
+ assignEnv : REQUIRED : If you want to assign a library to an environment.
379
409
  If set to True, the library will be assigned to an available environment in the Target Property if available and build it.
410
+ If set to a string (regex supported), the library will be assigned to the environment with the name provided if it exists, and build it. If that environment is used in a Library, it will remove that environment from the library and assign it to the new one. Default: False
380
411
  """
381
412
  response = {target:{'libraryName':name} for target in self.targets.keys()}
382
413
  for target in list(self.targets.keys()):
@@ -410,8 +441,14 @@ class Synchronizer:
410
441
  self.targets[target]['library'].updateExtensions(existingExtensions)
411
442
  if len(newExtensions)>0:
412
443
  self.targets[target]['library'].addExtensions(newExtensions)
413
- if assignEnv:
444
+ if type(assignEnv) == bool and assignEnv == True:
414
445
  envs = self.targets[target]['api'].getEnvironments()
446
+ prod = [env for env in envs if env['attributes']['stage'].lower() == 'production']
447
+ if len(prod) == 0:
448
+ prod_script = "https://assets.adobedtm.com/" ## default script source when no production environment exist, with a random id to avoid any conflict with an existing environment script source.
449
+ else:
450
+ prod = prod[0]
451
+ prod_script = prod['meta'].get('script_sources',[{}])[0].get('minified')
415
452
  found_free_env = False
416
453
  if len(envs)>0:
417
454
  for env in envs:
@@ -426,10 +463,48 @@ class Synchronizer:
426
463
  self.targets[target]['library'].setEnvironment(envId)
427
464
  self.targets[target]['library'].build()
428
465
  response[target]['environment'] = envName
429
- response[target]['script'] = scriptSource
466
+ response[target]['script'] = {f"{target} - {self.targets[target]['library'].name}" : {
467
+ "replace": prod_script,
468
+ "with": scriptSource
469
+ }}
470
+ response[target]['build_status'] = self.targets[target]['library'].build_status
430
471
  else:
431
472
  response[target]['environment'] = "No free environment to assign"
432
473
  response[target]['script'] = None
474
+ elif type(assignEnv) == str:
475
+ envs = self.targets[target]['api'].getEnvironments()
476
+ prod = [env for env in envs if env['attributes']['stage'].lower() == 'production']
477
+ if len(prod) == 0:
478
+ prod_script = "https://assets.adobedtm.com/" ## default script source when no production environment exist, with a random id to avoid any conflict with an existing environment script source.
479
+ else:
480
+ prod = prod[0]
481
+ prod_script = prod['meta'].get('script_sources',[{}])[0].get('minified')
482
+ env = [env for env in envs if re.search(assignEnv, env['attributes']['name'], re.IGNORECASE)]
483
+ if len(env) == 0:
484
+ response[target]['environment'] = f"No environment with the name {assignEnv} to assign"
485
+ response[target]['script'] = None
486
+ elif len(env) == 1:
487
+ env = env[0]
488
+ envId = env['id']
489
+ envName = env['attributes']['name']
490
+ scriptSource = env['meta'].get('script_sources',[{}])[0].get('minified')
491
+ libraryUsed = self.targets[target]["api"].getEnvironmentLibrary(envId)
492
+ if libraryUsed is not None:
493
+ print(f'Environment is already used by library "{libraryUsed["attributes"]["name"]}" in "{target}" property. Removing the environment from that library.')
494
+ libraryUsedId = libraryUsed['id']
495
+ tmp_lib = Library(libraryUsedId,config_object=self.targets[target]['api'].connector.config,header=self.targets[target]['api'].connector.header)
496
+ tmp_lib.removeEnvironment()
497
+ self.targets[target]['library'].setEnvironment(envId)
498
+ self.targets[target]['library'].build()
499
+ response[target]['environment'] = envName
500
+ response[target]['script'] = {f"{target} - {self.targets[target]['library'].name}" : {
501
+ "replace": prod_script,
502
+ "with": scriptSource
503
+ }}
504
+ response[target]['build_status'] = self.targets[target]['library'].build_status
505
+ else:
506
+ response[target]['environment'] = f"Multiple environment with the name {assignEnv} to assign"
507
+ response[target]['script'] = None
433
508
  return response
434
509
 
435
510
 
@@ -514,25 +589,14 @@ class Synchronizer:
514
589
  action_setting_path : [str,list] : The dot notation of the paths you want to verify for the settings object. ex: ["code","customAttributes"]. If not provided, the complete settings are compared.
515
590
  condition_setting_path : [str,list] : The dot notation of the paths you want to verify for the setting object . ex: ["id",""]. If not provided, the complete settings are compared
516
591
  event_setting_path : [str,list] : The dot notation of the paths you want to verify for the setting object . ex: ["id",""]. If not provided, the complete settings are compared
592
+ libraryLinked : bool : If you want to pass ID that are associated to a specific library. Default: False
517
593
  """
518
594
  if componentName is None and componentId is None:
519
595
  raise ValueError('Require a component Name of a component ID')
520
- cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion)
596
+ cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion,libraryLinked=kwargs.get('libraryLinked',False))
521
597
  dict_result = {tar:"" for tar in self.targets.keys()}
522
598
  dict_result['base-enabled'] = cmp_baseDict['component']['attributes'].get('enabled',False)
523
599
  dict_result['base-published'] = cmp_baseDict['component']['attributes'].get('published',False)
524
- if cmp_baseDict['component']['attributes'].get('latest',False):
525
- dict_result['base-state'] = "Latest"
526
- else:
527
- if cmp_baseDict['component']['attributes'].get('latest','unknown') == 'unknown':
528
- if dict_result['base-enabled']:
529
- dict_result['base-state'] = "Draft"
530
- else:
531
- dict_result['base-state'] = "Unknown"
532
- elif dict_result['base-published'] == False:
533
- dict_result['base-state'] = "Draft"
534
- else:
535
- dict_result['base-state'] = "Edited"
536
600
  if cmp_baseDict['component']['type'] == 'data_elements':
537
601
  for target in list(self.targets.keys()):
538
602
  ## if it does not exist
@@ -675,4 +739,98 @@ class Synchronizer:
675
739
  else:
676
740
  if not excludeSimilar:
677
741
  dict_result[target] = 'Similar'+issue_pub
678
- return dict_result
742
+ return dict_result
743
+
744
+ def __checkLibrarySync__(self,library:str=None,state='published',excludeSimilar:bool=False,libraryLinked:bool=True,publishedVersion:bool=False)->dict:
745
+ """
746
+ Check if the components in a library, from the base property, is synced to the different target properties.
747
+ By default, it will compare the version of the library to the latest version in the target properties.
748
+ It will return a dictionary with the key being the component name and the value being the result of the evaluation, such as {componentName: {'targetProperty': 'similar'}} when library have same settings.
749
+ Arguments:
750
+ library : REQUIRED : the library name or the library ID to get the components.
751
+ state : OPTIONAL : the state of the library to compare. Default: 'published', possible states: "development", "submitted", "approved", "rejected", "published"
752
+ excludeSimilar : OPTIONAL : If you do not want to see the result if the comparison provide a "similar" result. Similar means that the elements are about the same. Default: False.
753
+ libraryLinked : OPTIONAL : If set to True, it will use library linked components for comparison. Default: True.
754
+ publishedVersion : OPTIONAL : If set to True, it will compare the components to the published version in the target properties. Default: False.
755
+ """
756
+ if library is None:
757
+ raise ValueError('Require a library Name of a library ID')
758
+ libraryLink = True
759
+ base_libraries = self.base['api'].getLibraries(state=state)
760
+ if library in [lib['attributes']['name'] for lib in base_libraries]:
761
+ base_library = [lib for lib in base_libraries if lib['attributes']['name'] == library][0]
762
+ elif library in [lib['id'] for lib in base_libraries]:
763
+ base_library = [lib for lib in base_libraries if lib['id'] == library][0]
764
+ else:
765
+ raise ValueError('The library name or ID provided does not exist in the base property')
766
+ myLib = Library(base_library['id'],config_object=self.base['api'].connector.config,header=self.base['api'].connector.header)
767
+ components = myLib.getFullLibrary()
768
+ rules = components['rules']
769
+ dataElements = components['data_elements']
770
+ extensions = components['extensions']
771
+ elements = rules + dataElements + extensions
772
+ if publishedVersion == True:
773
+ element_ids = {elem['id']: elem['attributes']['name'] for elem in elements}
774
+ libraryLink = False
775
+ elif libraryLinked == True:
776
+ element_ids ={elem['links']['self'].split('/').pop():elem['attributes']['name'] for elem in elements}
777
+ else:
778
+ element_ids = {elem['id']: elem['attributes']['name'] for elem in elements}
779
+ dict_check = {}
780
+ for element_id, element_name in element_ids.items():
781
+ dict_check[element_name] = self.checkComponentSync(componentId=element_id, excludeSimilar=excludeSimilar,publishedVersion=publishedVersion,libraryLinked=libraryLink)
782
+ return dict_check
783
+
784
+ def syncFromLibrary(self,library:str=None,state='published',force:bool=True,libraryLinked:bool=True,publishedVersion:bool=False,dryRun:bool=False)->None:
785
+ """
786
+ Sync the components in a library, from the base property, to the different target properties by using the createTargetsLibrary method.
787
+ Arguments:
788
+ library : REQUIRED : the library name or the library ID to get the components.
789
+ state : OPTIONAL : the state of the library to compare. Default: 'published', possible states: "development", "submitted", "approved", "rejected", "published"
790
+ force : OPTIONAL : If set to True, it will sync the components even if they do not exist in the target properties. If set to False, it will only sync the components that already exist in the target properties. Default: True.
791
+ libraryLinked : OPTIONAL : If set to True, it will sync the components using the library version. Default: False.
792
+ publishedVersion : OPTIONAL : if you want to sync the version of the library that has been published in your base vs the published version of your target. Default: False.
793
+ dryRun : OPTIONAL : If set to True, it will not actually sync the components but will return a dictionary with the components that would be synced and the target properties they would be synced to. Default: False.
794
+ """
795
+ if library is None:
796
+ raise ValueError('Require a library Name of a library ID')
797
+ libs = self.base['api'].getLibraries(state=state)
798
+ if library in [lib['attributes']['name'] for lib in libs]:
799
+ libraryId = [lib for lib in libs if lib['attributes']['name'] == library][0]['id']
800
+ elif library in [lib['id'] for lib in libs]:
801
+ libraryId = [lib for lib in libs if lib['id'] == library][0]['id']
802
+ else:
803
+ raise ValueError('The library name or ID provided does not exist in the base property')
804
+ if dryRun:
805
+ return self.__checkLibrarySync__(library=library,state=state,excludeSimilar=False,libraryLinked=libraryLinked,publishedVersion=publishedVersion)
806
+ myLib = self.base['api'].getLibrary(libraryId,return_class=True)
807
+ rules = myLib.getRules()
808
+ dataelements = myLib.getDataElements()
809
+ extensions = myLib.getExtensions()
810
+ if len(extensions)>0:
811
+ for ext in extensions:
812
+ try:
813
+ self.upgradeTargetExtension(extensionName=ext['attributes']['name'])
814
+ except:
815
+ print(f'Extension {ext["attributes"]["name"]} could not be updated in the target properties. Please check if the extension exist and if there is an update available.')
816
+ if len(rules)>0:
817
+ for rule in rules:
818
+ try:
819
+ if libraryLinked:
820
+ ruleId = rule['links']['self'].split('/').pop()
821
+ else:
822
+ ruleId = rule['id']
823
+ self.syncComponent(componentId=ruleId,publishedVersion=publishedVersion, forceCreation=force,libraryLinked=libraryLinked)
824
+ except:
825
+ print(f'Rule {rule["attributes"]["name"]} could not be updated in the target properties. Please check if the rule exist.')
826
+ if len(dataelements)>0:
827
+ for de in dataelements:
828
+ try:
829
+ if libraryLinked:
830
+ deId = de['links']['self'].split('/').pop()
831
+ else:
832
+ deId = de['id']
833
+ self.syncComponent(componentId=deId,publishedVersion=publishedVersion,forceCreation=force,libraryLinked=libraryLinked)
834
+ except:
835
+ print(f'Data Element {de["attributes"]["name"]} could not be updated in the target properties. Please check if the data element exist.')
836
+ return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchpy
3
- Version: 0.4.7.post1
3
+ Version: 0.4.8
4
4
  Summary: Adobe Launch API wrapper
5
5
  Author-email: Julien Piccini <piccini.julien@gmail.com>
6
6
  License: Apache License 2.0
@@ -8,26 +8,26 @@ Project-URL: homepage, https://github.com/pitchmuc/launchpy
8
8
  Project-URL: changelog, https://github.com/pitchmuc/launchpy/blob/master/docs/releases.md
9
9
  Keywords: adobe,analytics,API,python
10
10
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python
14
14
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
- Classifier: Programming Language :: Python :: 3.6
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
19
20
  Classifier: Development Status :: 4 - Beta
20
- Requires-Python: >=3.7
21
+ Requires-Python: >=3.10
21
22
  Description-Content-Type: text/markdown
22
23
  License-File: LICENSE
23
24
  Requires-Dist: pandas
24
25
  Requires-Dist: requests
25
- Requires-Dist: PyJWT
26
- Requires-Dist: PyJWT[crypto]
27
26
  Requires-Dist: pathlib2
28
27
  Requires-Dist: pathlib
29
28
  Requires-Dist: aepp
30
29
  Requires-Dist: rich
30
+ Requires-Dist: deprecated
31
31
  Dynamic: license-file
32
32
 
33
33
  # Launchpy
@@ -1,8 +1,7 @@
1
1
  pandas
2
2
  requests
3
- PyJWT
4
- PyJWT[crypto]
5
3
  pathlib2
6
4
  pathlib
7
5
  aepp
8
6
  rich
7
+ deprecated
@@ -10,29 +10,29 @@ authors = [
10
10
  description = "Adobe Launch API wrapper"
11
11
  dynamic = ["version"]
12
12
  readme = "README.md"
13
- requires-python = ">=3.7"
13
+ requires-python = ">=3.10"
14
14
  keywords = ["adobe", "analytics", "API", "python"]
15
15
  license = {text = "Apache License 2.0"}
16
16
  classifiers = [
17
17
  "Intended Audience :: Developers",
18
- "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
18
+ "License :: OSI Approved :: Apache Software License",
19
19
  "Operating System :: OS Independent",
20
20
  "Programming Language :: Python",
21
21
  "Topic :: Scientific/Engineering :: Information Analysis",
22
- "Programming Language :: Python :: 3.6",
23
- "Programming Language :: Python :: 3.7",
24
- "Programming Language :: Python :: 3.8",
25
- "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
26
27
  "Development Status :: 4 - Beta"
27
28
  ]
28
29
  dependencies = ['pandas',
29
30
  "requests",
30
- "PyJWT",
31
- "PyJWT[crypto]",
32
31
  "pathlib2",
33
32
  "pathlib",
34
33
  "aepp",
35
- "rich",]
34
+ "rich",
35
+ "deprecated"]
36
36
 
37
37
  [project.urls]
38
38
  homepage = "https://github.com/pitchmuc/launchpy"
@@ -1,9 +1,7 @@
1
1
  pathlib>=1.0.0
2
2
  pathlib2>=2.3.5
3
3
  requests>=2.25.0
4
- aepp=>0.3.10
5
- pandas>=2.1.0
6
4
  aepp>=0.3.12
7
- httpx>=0.24.0
8
- asyncio>=3.0.0
9
- rich>=14.0.0
5
+ pandas>=2.1.0
6
+ rich>=14.0.0
7
+ deprecated
@@ -1 +0,0 @@
1
- __version__ = "0.4.7-1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes