launchpy 0.5.0__tar.gz → 0.5.1__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.5.0/launchpy.egg-info → launchpy-0.5.1}/PKG-INFO +2 -4
  2. {launchpy-0.5.0 → launchpy-0.5.1}/docs/cli.md +28 -3
  3. {launchpy-0.5.0 → launchpy-0.5.1}/docs/releases.md +3 -2
  4. {launchpy-0.5.0 → launchpy-0.5.1}/docs/synchronizer.md +28 -3
  5. launchpy-0.5.1/launchpy/__version__.py +1 -0
  6. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/cli/__main__.py +55 -16
  7. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/property.py +3 -0
  8. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/synchronizer.py +79 -41
  9. {launchpy-0.5.0 → launchpy-0.5.1/launchpy.egg-info}/PKG-INFO +2 -4
  10. {launchpy-0.5.0 → launchpy-0.5.1}/pyproject.toml +1 -3
  11. launchpy-0.5.0/launchpy/__version__.py +0 -1
  12. {launchpy-0.5.0 → launchpy-0.5.1}/LICENSE +0 -0
  13. {launchpy-0.5.0 → launchpy-0.5.1}/README.md +0 -0
  14. {launchpy-0.5.0 → launchpy-0.5.1}/docs/admin.md +0 -0
  15. {launchpy-0.5.0 → launchpy-0.5.1}/docs/getstarted.md +0 -0
  16. {launchpy-0.5.0 → launchpy-0.5.1}/docs/library.md +0 -0
  17. {launchpy-0.5.0 → launchpy-0.5.1}/docs/main.md +0 -0
  18. {launchpy-0.5.0 → launchpy-0.5.1}/docs/property.md +0 -0
  19. {launchpy-0.5.0 → launchpy-0.5.1}/docs/translator.md +0 -0
  20. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/__init__.py +0 -0
  21. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/admin.py +0 -0
  22. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/cli/__init__.py +0 -0
  23. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/config.py +0 -0
  24. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/configs.py +0 -0
  25. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/connector.py +0 -0
  26. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/launchpy.py +0 -0
  27. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy/library.py +0 -0
  28. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy.egg-info/SOURCES.txt +0 -0
  29. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy.egg-info/dependency_links.txt +0 -0
  30. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy.egg-info/entry_points.txt +0 -0
  31. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy.egg-info/requires.txt +0 -0
  32. {launchpy-0.5.0 → launchpy-0.5.1}/launchpy.egg-info/top_level.txt +0 -0
  33. {launchpy-0.5.0 → launchpy-0.5.1}/requirements.txt +0 -0
  34. {launchpy-0.5.0 → launchpy-0.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchpy
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Adobe Launch API wrapper
5
5
  Author-email: Julien Piccini <piccini.julien@gmail.com>
6
6
  License: Apache License 2.0
@@ -12,13 +12,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.10
16
- Classifier: Programming Language :: Python :: 3.11
17
15
  Classifier: Programming Language :: Python :: 3.12
18
16
  Classifier: Programming Language :: Python :: 3.13
19
17
  Classifier: Programming Language :: Python :: 3.14
20
18
  Classifier: Development Status :: 4 - Beta
21
- Requires-Python: >=3.10
19
+ Requires-Python: >=3.12
22
20
  Description-Content-Type: text/markdown
23
21
  License-File: LICENSE
24
22
  Requires-Dist: pandas
@@ -17,7 +17,7 @@ This document provides an overview of the command line interface (CLI) for `laun
17
17
  - [extract\_property](#extract_property)
18
18
  - [load\_property](#load_property)
19
19
  - [get\_extensions](#get_extensions)
20
- - [load\_extension](#load_extension)
20
+ - [load\_synchronizer](#load_synchronizer)
21
21
  - [Property Layer Commands](#property-layer-commands)
22
22
  - [get\_extensions](#get_extensions-1)
23
23
  - [get\_extension](#get_extension)
@@ -286,8 +286,8 @@ python -m launchpy.cli -cf <path_to_your_config_file>
286
286
  launchpy> get_extensions -s True
287
287
  ```
288
288
 
289
- #### load_extension
290
- Load a the synchronizer layer, allowing you to manage and synchronize properties with rules and data elements.\
289
+ #### load_synchronizer
290
+ Load the synchronizer layer, allowing you to manage and synchronize properties with extensions, rules and data elements.\
291
291
  Arguments:
292
292
  `base_name`: Name of the Launch Property to use as base.
293
293
  `-t`, `--targets`: list of target property names for synchronization. This is a required parameter.
@@ -299,6 +299,31 @@ launchpy> load_synchronizer "Base Property Name" -t "Target Property 1" "Target
299
299
  synchronizer:Base_Property_Name>
300
300
  ```
301
301
 
302
+ **Synchronizing across organizations**\
303
+ The synchronizer also supports a base property and/or target properties that live in a *different* Adobe organization than the one currently active in the CLI. This is useful when you manage several IMS orgs and want to push the same rules, data elements and extensions from a single base property to properties in other orgs.
304
+
305
+ To reference a property in another organization, append `@<org_name>` to its name, where `<org_name>` matches an `org_name` entry from a multi-org config file (see [create_config_file](#create_config_file)). Any property name without `@<org_name>` is resolved against the organization that is currently active in the session (the one last set via [config](#config) or [change_org](#change_org)).
306
+
307
+ First, load a multi-org config file so every organization you want to reference is available in the session:
308
+ ```bash
309
+ python -m launchpy.cli -cf <path_to_your_multi_org_config_file> --org_name client_a
310
+ launchpy:client_a>
311
+ ```
312
+
313
+ Then load the synchronizer using `@org_name` on the base and/or the targets:
314
+ ```bash
315
+ launchpy:client_a> load_synchronizer "Base Property Name@client_a" -t "Target Property 1@client_b" "Target Property 2@client_c"
316
+ synchronizer:Base_Property_Name>
317
+ ```
318
+
319
+ Organizations can be mixed freely; targets that omit `@org_name` resolve to the currently active organization (`client_a` below):
320
+ ```bash
321
+ launchpy:client_a> load_synchronizer "Base Property Name" -t "Target Property 1" "Target Property 2@client_b"
322
+ synchronizer:Base_Property_Name>
323
+ ```
324
+
325
+ **Note**: if a private extension used by the base property is named differently in a target organization, the CLI cannot map the two names for you — this requires passing a `mapping_extensions` dictionary (`{"target-extension-name": "base-extension-name"}`) to `launchpy.Synchronizer` directly through the Python API, usually done via a Notebook, since that option is not exposed as a CLI flag.
326
+
302
327
  ### Property Layer Commands
303
328
 
304
329
  Once you have instantiated the property layer for a specific property, you can access various commands to manage that property. These commands allow you to perform actions such as viewing and editing rules, data elements, and other configurations related to that property.
@@ -4,7 +4,6 @@ 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
6
  ## 0.5.0
7
- * Adding option to extract only the latest published version of the artifacts when extracting a property. This is available in the CLI with the `-p` or `--published_version` flag and in the `extractProperty` function with the `publishedVersion` parameter.
8
7
  * Fixing the `create_property` method in the CLI
9
8
  * Adding support for multi-org config files.
10
9
  * Adding support for multi-org instantiation in the synchronizer module.
@@ -14,7 +13,9 @@ This has been started after the 0.3.0 release.\
14
13
  * Adding `sync_data_elements` capabilitiy in the CLI and `syncDataElements` in synchronizer module.
15
14
  * Fixing the `createTargetsLibrary` method in the synchronizer where using existing library that contained rules not touched by the synchronizer would delete them. Now it will keep them and only update the ones that are in the source property.
16
15
  * `extractProperty` can now extract the elements as they are in their published version. This is available in the CLI with the `-p` or `--published_version` flag and in the `extractProperty` function with the `publishedVersion` parameter.
17
- * `extractProperty` has a new parameter `folder` that allows you to specify the folder where the property will be extracted. If None, a folder named after the property name will be created in the current working directory.
16
+ * `extractProperty` has a new parameter `folder` that allows you to specify the folder where the property will be extracted. If None, a folder named after the property name will be created in the current working directory.\
17
+ Patch:
18
+ * Printing the outcome that elements are not created when you do not have the `forceCreation` parameter.
18
19
 
19
20
 
20
21
  ## 0.4.9
@@ -54,9 +54,34 @@ That is possible, but the requirements are still needed regarding the following
54
54
  * You have unique name for your rule and data elements
55
55
  * You have the same extension installed in both the template and target properties.
56
56
 
57
- The synchronizer will use the environment credential lastly loaded in the system.\
58
- We would recommend to load first the target organization that you want to sync to, and then the base organization.\
59
- You will be able to use the Property instance of the target organization in order to sync the element afterwards.
57
+ #### Using the `@org_name` shorthand (recommended)
58
+
59
+ The simplest way is to import a **multi-org config file** (a JSON list where each block has its own `org_name`, see [getstarted](getstarted.md)) so that every organization you want to reference is loaded in a single call, then reference a property in a specific organization by appending `@<org_name>` to its name in `base` or `targets`. Any name without `@<org_name>` is resolved against the default/last configured organization.
60
+
61
+ ```py
62
+ import launchpy as lp
63
+
64
+ lp.importConfigFile('config_multi_org.json') ## loads every org_name block in the file at once
65
+
66
+ synchronizor = lp.Synchronizer(
67
+ base='Base Property Name@client_a',
68
+ targets=['Target Property 1@client_b', 'Target Property 2@client_c']
69
+ )
70
+ ```
71
+
72
+ You can mix organizations freely; a target without `@org_name` resolves to the default organization (the first one loaded, or the one set by your last `configure()` call without an explicit `org_name`):
73
+
74
+ ```py
75
+ synchronizor = lp.Synchronizer(
76
+ base='Base Property Name', ## resolved in the default org
77
+ targets=['Target Property 1', 'Target Property 2@client_b']
78
+ )
79
+ ```
80
+
81
+ #### Using explicit Property instances (alternative)
82
+
83
+ You can also build the `Property` instances yourself and pass them directly instead of strings. This is useful for one-off scripts or when you want to select a property that `getProperties` returns without relying on name matching.\
84
+ The synchronizer will use the environment credential lastly loaded in the system, so we recommend loading the target organization first, then the base organization, so that the `Property` instances you build stay tied to the correct organization's connector.
60
85
 
61
86
  Example of setup
62
87
 
@@ -0,0 +1 @@
1
+ __version__ = "0.5.1"
@@ -578,7 +578,11 @@ class SynchronizerCLI(cmd.Cmd):
578
578
  result = self.synchronizer.syncComponent(componentId=args.id, publishedVersion=args.published, forceCreation=args.create, verbose=args.verbose)
579
579
  else:
580
580
  result = self.synchronizer.syncComponent(componentName=args.name, publishedVersion=args.published, forceCreation=args.create, verbose=args.verbose)
581
- console.print(f"Component '{args.name or args.id}' synced successfully.", style="green")
581
+ for prop, message in result.items():
582
+ if "does not exist" in message:
583
+ console.print(f"[{prop}] {message}", style="yellow")
584
+ elif args.verbose:
585
+ console.print(f"[{prop}] {message}", style="green")
582
586
  except Exception as e:
583
587
  console.print(f"(!) Error: {str(e)}", style="red")
584
588
  return
@@ -590,32 +594,44 @@ class SynchronizerCLI(cmd.Cmd):
590
594
  parser.add_argument("-r", "--regex", help="Regex pattern to filter rules by name (partial match, non-case sensitive). Default None.", type=str, default=None)
591
595
  parser.add_argument("-c", "--create", help="Create rules that do not exist in the destination property. Default False.", action="store_true", default=False)
592
596
  parser.add_argument("-p", "--published", help="Sync the latest published version of the rules. Default False.", action="store_true", default=False)
597
+ parser.add_argument("-v", "--verbose", help="Print detailed information about the sync process, including successes. Default False.", action="store_true", default=False)
593
598
  try:
594
599
  args = parser.parse_args(shlex.split(args))
595
600
  result = self.synchronizer.syncRules(regex=args.regex, forceCreation=args.create, publishedVersion=args.published)
596
- console.print(f"All rules synced successfully.", style="green")
601
+ for ruleName, targetResults in result.items():
602
+ for prop, message in (targetResults or {}).items():
603
+ if "does not exist" in message:
604
+ console.print(f"[{ruleName}][{prop}] {message}", style="yellow")
605
+ elif args.verbose:
606
+ console.print(f"[{ruleName}][{prop}] {message}", style="green")
597
607
  except Exception as e:
598
608
  console.print(f"(!) Error: {str(e)}", style="red")
599
609
  return
600
610
  except SystemExit:
601
611
  return
602
-
612
+
603
613
  def do_sync_data_elements(self,args:Any):
604
614
  """Sync all data elements between source and destination properties."""
605
615
  parser = argparse.ArgumentParser(prog='sync_data_elements', add_help=True)
606
616
  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)
607
617
  parser.add_argument("-c", "--create", help="Create data elements that do not exist in the destination property. Default False.", action="store_true", default=False)
608
618
  parser.add_argument("-p", "--published", help="Sync the latest published version of the data elements. Default False.", action="store_true", default=False)
619
+ parser.add_argument("-v", "--verbose", help="Print detailed information about the sync process, including successes. Default False.", action="store_true", default=False)
609
620
  try:
610
621
  args = parser.parse_args(shlex.split(args))
611
622
  result = self.synchronizer.syncDataElements(regex=args.regex, forceCreation=args.create, publishedVersion=args.published)
612
- console.print(f"All data elements synced successfully.", style="green")
623
+ for deName, targetResults in result.items():
624
+ for prop, message in (targetResults or {}).items():
625
+ if "does not exist" in message:
626
+ console.print(f"[{deName}][{prop}] {message}", style="yellow")
627
+ elif args.verbose:
628
+ console.print(f"[{deName}][{prop}] {message}", style="green")
613
629
  except Exception as e:
614
630
  console.print(f"(!) Error: {str(e)}", style="red")
615
631
  return
616
632
  except SystemExit:
617
633
  return
618
-
634
+
619
635
  def do_sync_extensions(self,args:Any):
620
636
  """Sync all extensions between source and destination properties."""
621
637
  parser = argparse.ArgumentParser(prog='sync_extensions', add_help=True)
@@ -625,13 +641,18 @@ class SynchronizerCLI(cmd.Cmd):
625
641
  try:
626
642
  args = parser.parse_args(shlex.split(args))
627
643
  result = self.synchronizer.syncExtensions(regex=args.regex, forceCreation=args.create, verbose=args.verbose)
628
- console.print(f"All extensions synced successfully.", style="green")
644
+ for extName, targetResults in result.items():
645
+ for prop, message in (targetResults or {}).items():
646
+ if "does not exist" in message:
647
+ console.print(f"[{extName}][{prop}] {message}", style="yellow")
648
+ elif args.verbose:
649
+ console.print(f"[{extName}][{prop}] {message}", style="green")
629
650
  except Exception as e:
630
651
  console.print(f"(!) Error: {str(e)}", style="red")
631
652
  return
632
653
  except SystemExit:
633
654
  return
634
-
655
+
635
656
  def do_rename_component(self,args:Any):
636
657
  """Rename a component in the destination property."""
637
658
  parser = argparse.ArgumentParser(prog='rename_component', add_help=True)
@@ -661,16 +682,22 @@ class SynchronizerCLI(cmd.Cmd):
661
682
  parser = argparse.ArgumentParser(prog='upgrade_extension', add_help=True)
662
683
  parser.add_argument("name", help="Name of the extension to upgrade", type=str)
663
684
  parser.add_argument("-p", "--platform", help="Platform to be used for extension upgrade. default 'web'. Possible values: web, app", type=str, default="web")
685
+ parser.add_argument("-v", "--verbose", help="Print detailed information about the upgrade process, including successes. Default False.", action="store_true", default=False)
664
686
  try:
665
687
  args = parser.parse_args(shlex.split(args))
666
- self.synchronizer.upgradeTargetExtension(extensionName=args.name, platform=args.platform)
667
- console.print(f"Extension '{args.name}' upgraded successfully.", style="green")
688
+ response = self.synchronizer.upgradeTargetExtension(extensionName=args.name, platform=args.platform, verbose=args.verbose)
689
+ for prop, message in response.items():
690
+ failed = message.startswith("Could not upgrade")
691
+ if failed:
692
+ console.print(f"[{prop}] {message}", style="red")
693
+ elif args.verbose:
694
+ console.print(f"[{prop}] {message}", style="green")
668
695
  except Exception as e:
669
696
  console.print(f"(!) Error: {str(e)}", style="red")
670
697
  return
671
698
  except SystemExit:
672
699
  return
673
-
700
+
674
701
  def do_get_base_rules(self,args:Any):
675
702
  """Get the base rules that are available for synchronization."""
676
703
  parser = argparse.ArgumentParser(prog='get_base_rules', add_help=True)
@@ -848,8 +875,12 @@ class SynchronizerCLI(cmd.Cmd):
848
875
  console.print(f"Upgrading {len(extensions)} extension{'s' if len(extensions) > 1 else ''} from library '{lib.name}'...", style="blue")
849
876
  for ext in extensions:
850
877
  try:
851
- self.synchronizer.upgradeTargetExtension(extensionName=ext['attributes']['name'], platform=ext['attributes']['platform'], publishedVersion=publishedVersion, verbose=args.verbose)
852
- console.print(f"Extension '{ext['attributes']['name']}' upgraded successfully.", style="green")
878
+ response = self.synchronizer.upgradeTargetExtension(extensionName=ext['attributes']['name'], platform=ext['attributes']['platform'], verbose=args.verbose)
879
+ for prop, message in response.items():
880
+ if message.startswith("Could not upgrade"):
881
+ console.print(f"[{prop}] {message}", style="red")
882
+ elif args.verbose:
883
+ console.print(f"[{prop}] {message}", style="green")
853
884
  except Exception as e:
854
885
  console.print(f"(!) Error upgrading extension '{ext['attributes']['name']}': {str(e)}", style="red")
855
886
  if len(rules)>0:
@@ -860,8 +891,12 @@ class SynchronizerCLI(cmd.Cmd):
860
891
  ruleId = rule['links']['self'].split('/').pop()
861
892
  else:
862
893
  ruleId = rule['id']
863
- self.synchronizer.syncComponent(componentId=ruleId, publishedVersion=publishedVersion, forceCreation=args.create,libraryLinked=libraryLinked,verbose=args.verbose)
864
- console.print(f"Rule '{rule['attributes']['name']}' synced successfully.", style="green")
894
+ response = self.synchronizer.syncComponent(componentId=ruleId, publishedVersion=publishedVersion, forceCreation=args.create,libraryLinked=libraryLinked,verbose=args.verbose)
895
+ for prop, message in response.items():
896
+ if "does not exist" in message:
897
+ console.print(f"[{prop}] {message}", style="yellow")
898
+ elif args.verbose:
899
+ console.print(f"[{prop}] {message}", style="green")
865
900
  except Exception as e:
866
901
  console.print(f"(!) Error syncing rule '{rule['attributes']['name']}': {str(e)}", style="red")
867
902
  if len(dataelements)>0:
@@ -872,8 +907,12 @@ class SynchronizerCLI(cmd.Cmd):
872
907
  deId = de['links']['self'].split('/').pop()
873
908
  else:
874
909
  deId = de['id']
875
- self.synchronizer.syncComponent(componentId=deId, publishedVersion=publishedVersion, forceCreation=args.create,libraryLinked=libraryLinked,verbose=args.verbose)
876
- console.print(f"Data element '{de['attributes']['name']}' synced successfully.", style="green")
910
+ response = self.synchronizer.syncComponent(componentId=deId, publishedVersion=publishedVersion, forceCreation=args.create,libraryLinked=libraryLinked,verbose=args.verbose)
911
+ for prop, message in response.items():
912
+ if "does not exist" in message:
913
+ console.print(f"[{prop}] {message}", style="yellow")
914
+ elif args.verbose:
915
+ console.print(f"[{prop}] {message}", style="green")
877
916
  except Exception as e:
878
917
  console.print(f"(!) Error syncing data element '{de['attributes']['name']}': {str(e)}", style="red")
879
918
  except Exception as e:
@@ -199,6 +199,9 @@ class Property:
199
199
  'update': False,
200
200
  'internal_id': ext['id']
201
201
  } for ext in extensions if ext['attributes']['name'] == name}
202
+ if len(dict_extensions) == 0:
203
+ installed_names = [ext['attributes']['name'] for ext in extensions]
204
+ raise ValueError(f"No extension named '{name}' is installed on this property. Installed extensions: {installed_names}")
202
205
  for name in dict_extensions:
203
206
  new_id = self._getExtensionPackageIdByName(name, platform=platform, verbose=verbose)
204
207
  if new_id != dict_extensions[name]['package_id']:
@@ -164,6 +164,7 @@ class Synchronizer:
164
164
  """
165
165
  cmp_base=None
166
166
  componentBase = None
167
+ lib_cmp_base = None
167
168
  if componentId is not None:
168
169
  if componentId.startswith('DE'): ## if data element
169
170
  if kwargs.get('libraryLinked',False):
@@ -232,7 +233,7 @@ class Synchronizer:
232
233
  if cmp_baseDict['component']['type'] == 'data_elements':
233
234
  self.base['dataElements'].append(publishedVersion)
234
235
  cmp_baseDict = {'id':publishedVersion['id'],'name':publishedVersion['attributes']['name'],'component':publishedVersion,'copy':copySettings(publishedVersion)}
235
- if kwargs.get('libraryLinked',False) and cmp_baseDict['component']['type'] in ['rules','data_elements']:
236
+ if lib_cmp_base is not None and kwargs.get('libraryLinked',False) and cmp_baseDict['component']['type'] in ['rules','data_elements']:
236
237
  if lib_cmp_base['attributes']['name'] != cmp_baseDict['name']:
237
238
  with self._base_lock:
238
239
  if cmp_baseDict['component']['type'] == 'rules':
@@ -246,12 +247,13 @@ class Synchronizer:
246
247
  cmp_baseDict = {'id':lib_cmp_base['id'],'name':lib_cmp_base['attributes']['name'],'component':lib_cmp_base,'copy':copySettings(lib_cmp_base)}
247
248
  return cmp_baseDict
248
249
 
249
- def syncComponent(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,forceCreation:bool=False,**kwargs)->None:
250
+ def syncComponent(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,forceCreation:bool=False,**kwargs)->dict:
250
251
  """
251
252
  Synchronize a component from the base property to the different target properties.
252
253
  It will detect the component with the same name in target properties.
253
254
  It will delete the components related objects (ruleComponents, data element code) in the target property and push the template version.
254
- If you uploaded a dynamicRuleComponent data element config during the init method, any element that are not sync because of the exclComponentList will return a dict: {componentName:False}
255
+ If you uploaded a dynamicRuleComponent data element config during the init method, any element that are not sync because of the exclComponentList will return a dict: {componentName:False}
256
+ Returns a dictionary keyed by target property name, with the status message for that target (created, updated, or not synced because the component does not exist and forceCreation is False).
255
257
  Arguments:
256
258
  componentName : REQUIRED : the name of the component to sync
257
259
  componentID : REQUIRED : the id of the component to sync
@@ -267,6 +269,7 @@ class Synchronizer:
267
269
  verbose = kwargs.get('verbose',False)
268
270
  if componentName is None and componentId is None:
269
271
  raise ValueError('Require a component Name of a component ID')
272
+ response = {}
270
273
  cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion,libraryLinked=libraryLinked)
271
274
  ## handling the data element
272
275
  if cmp_baseDict['component']['type'] == 'data_elements':
@@ -306,9 +309,11 @@ class Synchronizer:
306
309
  attr_dict=translatedComponent)
307
310
  self.targets[target]['libraryStack']['dataElements'].append(comp)
308
311
  self.targets[target]['dataElements'].append(comp)
312
+ response[target] = f'Data element "{cmp_baseDict["name"]}" created in "{target}".'
309
313
  else:
310
- if verbose:
311
- print(f'The data element "{cmp_baseDict["name"]}" does not exist in the target property "{target}". Set forceCreation to True if you want to create it.')
314
+ message = f'Data element "{cmp_baseDict["name"]}" does not exist in the target property "{target}". Not synced, set forceCreation to True if you want to create it.'
315
+ print(message)
316
+ response[target] = message
312
317
  else:
313
318
  index,old_component = [(index,de) for index,de in enumerate(self.targets[target]['dataElements']) if de.get('attributes',{}).get('name') == cmp_baseDict['name']][0]
314
319
  attributes = {
@@ -328,6 +333,7 @@ class Synchronizer:
328
333
  del self.targets[target]['dataElements'][index]
329
334
  self.targets[target]['dataElements'].append(comp)
330
335
  self.targets[target]['libraryStack']['dataElements'].append(comp)
336
+ response[target] = f'Data element "{cmp_baseDict["name"]}" updated in "{target}".'
331
337
  ## Rules part
332
338
  if cmp_baseDict['component']['type'] == 'rules':
333
339
  ## fetching all rule components associated with a rule.
@@ -385,10 +391,12 @@ class Synchronizer:
385
391
  delay_next=translatedComponent['delay_next'],
386
392
  timeout=translatedComponent['timeout'],
387
393
  )
394
+ response[target] = f'Rule "{cmp_baseDict["name"]}" created in "{target}".'
388
395
  else:
389
396
  flagSkipCreation = True
390
- if verbose:
391
- print(f'The rule "{cmp_baseDict["name"]}" does not exist in the target property "{target}". Not creating it')
397
+ message = f'The rule "{cmp_baseDict["name"]}" does not exist in the target property "{target}". Not synced, set forceCreation to True if you want to create it.'
398
+ print(message)
399
+ response[target] = message
392
400
  else: ## if a rule exist with the same name
393
401
  if verbose:
394
402
  print(f'The rule "{cmp_baseDict["name"]}" exists in the target property "{target}". Updating it')
@@ -424,6 +432,7 @@ class Synchronizer:
424
432
  delay_next=translatedComponent['delay_next'],
425
433
  timeout=translatedComponent['timeout'],
426
434
  )
435
+ response[target] = f'Rule "{cmp_baseDict["name"]}" updated in "{target}".'
427
436
  ## updating rule attribute if difference between base and target
428
437
  if not flagSkipCreation: ## if the rule was created or updated, we want to make sure that the attribute are the same as the template
429
438
  if cmp_baseDict['component']['attributes']['enabled'] != targetRule['attributes']['enabled']:
@@ -449,7 +458,7 @@ class Synchronizer:
449
458
  comp_base_settings = cmp_baseDict['copy']['settings']
450
459
  comp_base_extension_id = cmp_baseDict['copy']['extension_id']
451
460
  descriptor = cmp_baseDict['copy']['descriptor']
452
- if cmp_baseDict['name'] not in [ext['attributes']['name'] for ext in self.targets[target]['extensions']]:
461
+ if cmp_baseDict['name'] not in [ext['attributes']['name'] for ext in self.targets[target]['extensions']]: ## if extension does not exist
453
462
  if forceCreation:
454
463
  if verbose:
455
464
  print(f'The extension "{cmp_baseDict["name"]}" does not exist in the target property "{target}". Creating it')
@@ -463,9 +472,11 @@ class Synchronizer:
463
472
  self.targets[target]['libraryStack']['extensions'].append(comp)
464
473
  self.targets[target]['extensions'].append(comp)
465
474
  self.translator.extendExtensions(self.targets[target]['extensions'],target)
475
+ response[target] = f'Extension "{cmp_baseDict["name"]}" created in "{target}".'
466
476
  else:
467
- if verbose:
468
- print(f'The extension "{cmp_baseDict["name"]}" does not exist in the target property "{target}". Set forceCreation to True if you want to create it.')
477
+ message = f'The extension "{cmp_baseDict["name"]}" does not exist in the target property "{target}". Not synced, set forceCreation to True if you want to create it.'
478
+ print(message)
479
+ response[target] = message
469
480
  else: ## if extension exist, we will update it
470
481
  if verbose:
471
482
  print(f'The extension "{cmp_baseDict["name"]}" exists in the target property "{target}". Updating it')
@@ -480,27 +491,32 @@ class Synchronizer:
480
491
  self.targets[target]['extensions'].append(comp)
481
492
  self.targets[target]['libraryStack']['extensions'].append(comp)
482
493
  self.translator.extendExtensions(self.targets[target]['extensions'],target)
494
+ response[target] = f'Extension "{cmp_baseDict["name"]}" updated in "{target}".'
495
+ return response
483
496
 
484
-
485
- def syncComponents(self,componentsName:list=None,componentsId:list=None,publishedVersion:bool=False,forceCreation:bool=False)->None:
497
+ def syncComponents(self,componentsName:list=None,componentsId:list=None,publishedVersion:bool=False,forceCreation:bool=False)->dict:
486
498
  """
487
499
  Sync multiple components by looping through the list of name passed.
500
+ Returns a dictionary keyed by component name (or id), with the per-target status returned by syncComponent.
488
501
  Arguments:
489
502
  componentsName : REQUIRED : The list of component names to sync
490
503
  componentsId : REQUIRED : The list of component ID to sync
491
504
  publishedVersion : OPTIONAL : if you want to take the version that has been published
492
505
  forceCreation : 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: False.
493
506
  """
507
+ results = {}
494
508
  if componentsName is not None:
495
509
  for component in componentsName:
496
- self.syncComponent(componentName=component,publishedVersion=publishedVersion,forceCreation=forceCreation)
510
+ results[component] = self.syncComponent(componentName=component,publishedVersion=publishedVersion,forceCreation=forceCreation)
497
511
  if componentsId is not None:
498
512
  for component in componentsId:
499
- self.syncComponent(componentId=component,publishedVersion=publishedVersion,forceCreation=forceCreation)
500
-
501
- def syncRules(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False)->None:
513
+ results[component] = self.syncComponent(componentId=component,publishedVersion=publishedVersion,forceCreation=forceCreation)
514
+ return results
515
+
516
+ def syncRules(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False)->dict:
502
517
  """
503
518
  Synchronizing the rules in the base property to the target properties.
519
+ Returns a dictionary keyed by rule name, with the per-target status (created, updated, or not synced because the rule does not exist and forceCreation is False).
504
520
  Arguments:
505
521
  regex : OPTIONAL : If you want to filter the rules to sync based on a regex pattern. Default: None (sync all rules)
506
522
  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.
@@ -509,14 +525,17 @@ class Synchronizer:
509
525
  base_rule_names = [rule['attributes']['name'] for rule in self.base['rules']]
510
526
  if regex is not None:
511
527
  base_rule_names = [rule for rule in base_rule_names if re.search(regex,rule)]
528
+ results = {}
512
529
  with ThreadPoolExecutor() as executor:
513
- futures = [executor.submit(self.syncComponent, componentName=rule, forceCreation=forceCreation, publishedVersion=publishedVersion) for rule in base_rule_names]
514
- for future in futures:
515
- future.result()
530
+ future_to_name = {executor.submit(self.syncComponent, componentName=rule, forceCreation=forceCreation, publishedVersion=publishedVersion): rule for rule in base_rule_names}
531
+ for future in future_to_name:
532
+ results[future_to_name[future]] = future.result()
533
+ return results
516
534
 
517
- def syncDataElements(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False)->None:
535
+ def syncDataElements(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False)->dict:
518
536
  """
519
537
  Synchronizing the data elements in the base property to the target properties.
538
+ Returns a dictionary keyed by data element name, with the per-target status (created, updated, or not synced because the data element does not exist and forceCreation is False).
520
539
  Arguments:
521
540
  regex : OPTIONAL : If you want to filter the data elements to sync based on a regex pattern. Default: None (sync all data elements)
522
541
  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.
@@ -525,15 +544,18 @@ class Synchronizer:
525
544
  base_de_names = [de['attributes']['name'] for de in self.base['dataElements']]
526
545
  if regex is not None:
527
546
  base_de_names = [de for de in base_de_names if re.search(regex,de)]
547
+ results = {}
528
548
  with ThreadPoolExecutor() as executor:
529
- futures = [executor.submit(self.syncComponent, componentName=de, forceCreation=forceCreation, publishedVersion=publishedVersion) for de in base_de_names]
530
- for future in futures:
531
- future.result()
532
-
533
- def syncExtensions(self,regex:str=None,forceCreation:bool=False, **kwargs)->None:
549
+ future_to_name = {executor.submit(self.syncComponent, componentName=de, forceCreation=forceCreation, publishedVersion=publishedVersion): de for de in base_de_names}
550
+ for future in future_to_name:
551
+ results[future_to_name[future]] = future.result()
552
+ return results
553
+
554
+ def syncExtensions(self,regex:str=None,forceCreation:bool=False, **kwargs)->dict:
534
555
  """
535
556
  Take extensions from the Base and try to install them on the target properties.
536
557
  It will check if the extension is already installed in the target property, if not it will install it.
558
+ Returns a dictionary keyed by extension name, with the per-target status (created, updated, or not synced because the extension does not exist and forceCreation is False).
537
559
  Arguments:
538
560
  regex : OPTIONAL : If you want to filter the extensions name to sync based on a regex pattern. Default: None (sync all extensions)
539
561
  forceCreation : OPTIONAL : If set to True, will force the creation of the extension even if it already exists. Default: False
@@ -542,8 +564,10 @@ class Synchronizer:
542
564
  base_extension_names = [ext['attributes']['name'] for ext in base_extensions]
543
565
  if regex is not None:
544
566
  base_extension_names = [ext for ext in base_extension_names if re.search(regex,ext)]
567
+ results = {}
545
568
  for ext_name in base_extension_names:
546
- self.syncComponent(componentName=ext_name,forceCreation=forceCreation,**kwargs)
569
+ results[ext_name] = self.syncComponent(componentName=ext_name,forceCreation=forceCreation,**kwargs)
570
+ return results
547
571
 
548
572
 
549
573
  def createTargetsLibrary(self,name:str="syncComponents",assignEnv:bool|str=False,**kwargs)->None:
@@ -672,9 +696,10 @@ class Synchronizer:
672
696
  """
673
697
  if extensionName is None:
674
698
  raise ValueError("Require an extension name")
699
+ response = {}
675
700
  for prop, target in self.targets.items():
676
701
  try:
677
- extensionUpdate = target['api'].checkExtensionUpdate(extensionName)
702
+ extensionUpdate = target['api'].checkExtensionUpdate(extensionName,platform=platform)
678
703
  for extName, extUpdateDict in extensionUpdate.items():
679
704
  if extUpdateDict["update"]:
680
705
  res = target['api'].upgradeExtension(extUpdateDict['internal_id'],extUpdateDict["package_id"])
@@ -683,13 +708,18 @@ class Synchronizer:
683
708
  index = [index for index, ext in enumerate(target['extensions']) if ext['attributes']['name'] == extName][0]
684
709
  del target['extensions'][index]
685
710
  target['extensions'].append(res)
711
+ response[prop] = f"Extension '{extName}' upgraded successfully."
686
712
  if verbose:
687
713
  print(f"Extension '{extName}' upgraded successfully in '{prop}' property.")
688
714
  else:
715
+ response[prop] = f"Extension '{extName}' is already up to date."
689
716
  if verbose:
690
717
  print(f"Extension '{extName}' is already up to date in '{prop}' property.")
691
- except:
692
- raise ValueError(f"Could not find an extension name: {extensionName}")
718
+ except Exception as e:
719
+ response[prop] = f"Could not upgrade extension '{extensionName}': {e}"
720
+ if verbose:
721
+ print(f"Could not upgrade extension '{extensionName}' in '{prop}' property: {e}")
722
+ return response
693
723
 
694
724
 
695
725
  def renameComponent(self,old_name:str=None,new_name:str=None)->None:
@@ -939,9 +969,10 @@ class Synchronizer:
939
969
  dict_check[element_name] = self.checkComponentSync(componentId=element_id, excludeSimilar=excludeSimilar,publishedVersion=publishedVersion,libraryLinked=libraryLink)
940
970
  return dict_check
941
971
 
942
- def syncFromLibrary(self,library:str=None,state='published',forceCreation:bool=False,libraryLinked:bool=True,publishedVersion:bool=False,dryRun:bool=False,**kwargs)->None:
972
+ def syncFromLibrary(self,library:str=None,state='published',forceCreation:bool=False,libraryLinked:bool=True,publishedVersion:bool=False,dryRun:bool=False,**kwargs)->dict:
943
973
  """
944
974
  Sync the components in a library, from the base property, to the different target properties by using the createTargetsLibrary method.
975
+ Returns a dictionary keyed by component name, with the per-target status (created, updated, or not synced because the component does not exist and forceCreation is False), or the error message if the sync failed.
945
976
  Arguments:
946
977
  library : REQUIRED : the library name or the library ID to get the components.
947
978
  state : OPTIONAL : the state of the library to compare. Default: 'published', possible states: "development", "submitted", "approved", "rejected", "published"
@@ -949,7 +980,7 @@ class Synchronizer:
949
980
  libraryLinked : OPTIONAL : If set to True, it will sync the components using the library version. Default: False.
950
981
  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.
951
982
  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.
952
- Possible kwargs:
983
+ Possible kwargs:
953
984
  verbose : bool : If set to True, it will print the name of the components and the property that are being synced and the target property. Default: False.
954
985
  """
955
986
  verbose = kwargs.get('verbose', False)
@@ -968,12 +999,15 @@ class Synchronizer:
968
999
  rules = myLib.getRules()
969
1000
  dataelements = myLib.getDataElements()
970
1001
  extensions = myLib.getExtensions()
1002
+ results = {}
971
1003
  if len(extensions)>0:
972
1004
  for ext in extensions:
973
1005
  try:
974
- self.upgradeTargetExtension(extensionName=ext['attributes']['name'],platform=ext['attributes']['platform'],verbose=verbose)
975
- except:
976
- 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.')
1006
+ results[ext['attributes']['name']] = self.upgradeTargetExtension(extensionName=ext['attributes']['name'],platform=ext['attributes']['platform'],verbose=verbose)
1007
+ except Exception as e:
1008
+ message = f'Extension {ext["attributes"]["name"]} could not be updated in the target properties: {e}'
1009
+ print(message)
1010
+ results[ext['attributes']['name']] = message
977
1011
  if len(rules)>0:
978
1012
  for rule in rules:
979
1013
  try:
@@ -981,9 +1015,11 @@ class Synchronizer:
981
1015
  ruleId = rule['links']['self'].split('/').pop()
982
1016
  else:
983
1017
  ruleId = rule['id']
984
- self.syncComponent(componentId=ruleId,publishedVersion=publishedVersion, forceCreation=forceCreation,libraryLinked=libraryLinked,verbose=verbose)
985
- except:
986
- print(f'Rule {rule["attributes"]["name"]} could not be updated in the target properties. Please check if the rule exist.')
1018
+ results[rule['attributes']['name']] = self.syncComponent(componentId=ruleId,publishedVersion=publishedVersion, forceCreation=forceCreation,libraryLinked=libraryLinked,verbose=verbose)
1019
+ except Exception as e:
1020
+ message = f'Rule {rule["attributes"]["name"]} could not be updated in the target properties: {e}'
1021
+ print(message)
1022
+ results[rule['attributes']['name']] = message
987
1023
  if len(dataelements)>0:
988
1024
  for de in dataelements:
989
1025
  try:
@@ -991,7 +1027,9 @@ class Synchronizer:
991
1027
  deId = de['links']['self'].split('/').pop()
992
1028
  else:
993
1029
  deId = de['id']
994
- self.syncComponent(componentId=deId,publishedVersion=publishedVersion,forceCreation=forceCreation,libraryLinked=libraryLinked,verbose=verbose)
995
- except:
996
- print(f'Data Element {de["attributes"]["name"]} could not be updated in the target properties. Please check if the data element exist.')
997
- return
1030
+ results[de['attributes']['name']] = self.syncComponent(componentId=deId,publishedVersion=publishedVersion,forceCreation=forceCreation,libraryLinked=libraryLinked,verbose=verbose)
1031
+ except Exception as e:
1032
+ message = f'Data Element {de["attributes"]["name"]} could not be updated in the target properties: {e}'
1033
+ print(message)
1034
+ results[de['attributes']['name']] = message
1035
+ return results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchpy
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Adobe Launch API wrapper
5
5
  Author-email: Julien Piccini <piccini.julien@gmail.com>
6
6
  License: Apache License 2.0
@@ -12,13 +12,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.10
16
- Classifier: Programming Language :: Python :: 3.11
17
15
  Classifier: Programming Language :: Python :: 3.12
18
16
  Classifier: Programming Language :: Python :: 3.13
19
17
  Classifier: Programming Language :: Python :: 3.14
20
18
  Classifier: Development Status :: 4 - Beta
21
- Requires-Python: >=3.10
19
+ Requires-Python: >=3.12
22
20
  Description-Content-Type: text/markdown
23
21
  License-File: LICENSE
24
22
  Requires-Dist: pandas
@@ -10,7 +10,7 @@ authors = [
10
10
  description = "Adobe Launch API wrapper"
11
11
  dynamic = ["version"]
12
12
  readme = "README.md"
13
- requires-python = ">=3.10"
13
+ requires-python = ">=3.12"
14
14
  keywords = ["adobe", "analytics", "API", "python"]
15
15
  license = {text = "Apache License 2.0"}
16
16
  classifiers = [
@@ -19,8 +19,6 @@ classifiers = [
19
19
  "Operating System :: OS Independent",
20
20
  "Programming Language :: Python",
21
21
  "Topic :: Scientific/Engineering :: Information Analysis",
22
- "Programming Language :: Python :: 3.10",
23
- "Programming Language :: Python :: 3.11",
24
22
  "Programming Language :: Python :: 3.12",
25
23
  "Programming Language :: Python :: 3.13",
26
24
  "Programming Language :: Python :: 3.14",
@@ -1 +0,0 @@
1
- __version__ = "0.5.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes