launchpy 0.4.4.post3__tar.gz → 0.4.5.post1__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 (31) hide show
  1. {launchpy-0.4.4.post3/launchpy.egg-info → launchpy-0.4.5.post1}/PKG-INFO +2 -2
  2. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/releases.md +11 -0
  3. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/synchronizer.md +53 -21
  4. launchpy-0.4.5.post1/launchpy/__version__.py +1 -0
  5. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/property.py +98 -28
  6. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/synchronizer.py +187 -113
  7. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1/launchpy.egg-info}/PKG-INFO +2 -2
  8. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/requirements.txt +3 -1
  9. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/setup.py +2 -2
  10. launchpy-0.4.4.post3/launchpy/__version__.py +0 -1
  11. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/LICENSE +0 -0
  12. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/README.md +0 -0
  13. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/admin.md +0 -0
  14. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/getstarted.md +0 -0
  15. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/library.md +0 -0
  16. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/main.md +0 -0
  17. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/property.md +0 -0
  18. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/docs/translator.md +0 -0
  19. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/__init__.py +0 -0
  20. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/admin.py +0 -0
  21. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/config.py +0 -0
  22. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/configs.py +0 -0
  23. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/connector.py +0 -0
  24. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/launchpy.py +0 -0
  25. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy/library.py +0 -0
  26. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy.egg-info/SOURCES.txt +0 -0
  27. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy.egg-info/dependency_links.txt +0 -0
  28. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy.egg-info/requires.txt +0 -0
  29. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/launchpy.egg-info/top_level.txt +0 -0
  30. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/pyproject.toml +0 -0
  31. {launchpy-0.4.4.post3 → launchpy-0.4.5.post1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchpy
3
- Version: 0.4.4.post3
3
+ Version: 0.4.5.post1
4
4
  Summary: Adobe Launch API wrapper
5
5
  Home-page: https://github.com/pitchmuc/launchpy
6
6
  Author: Julien Piccini
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.7
19
19
  Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Development Status :: 4 - Beta
22
- Requires-Python: >=3.7
22
+ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pandas
@@ -3,6 +3,17 @@
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.5
7
+ * adding `getExtension` in `Property` class
8
+ * improve `upgradeExtension` in `Property` class
9
+ * improving `checkComponentSync` in `Synchronizer` class
10
+ * improving `getLatestPublishedVersion` with error message defining which property is failing\
11
+ Patch:
12
+ * Better support of `publishedVersion` in checkComponentSync
13
+ * Adding more check in the `checkComponentSync` report
14
+ * Documentation improved in synchronizer
15
+ * Support of extension added in `createTargetsLibrary`
16
+
6
17
  ## 0.4.4
7
18
  * adding a `getBuilds` method on the `Library` class instance
8
19
  * extend support for pagination on `getEnvironments`
@@ -104,36 +104,68 @@ Arguments:
104
104
 
105
105
  ## checkComponentSync
106
106
 
107
- This method will allow you to check if a component is the same in the target properties, when compare to the base.
108
- It works on Rules, Data Elements, Extensions, with their name or ID.
109
- It is returning a dictionary with the name of the target properties as keys and `True` when the component is the same, `False` when the component is **not** the same.
107
+ Check if the component,from the base property, is synced to the different target properties.
108
+ It can also check for the Extensions.\
109
+ It will return a dictionary with the key being the target property and the value being the result of the evaluation, such as {targetProperty: 'similar'} when component have same settings or {targetPropery : '<difference>'} when component do not have same settings.\
110
+ See below for differences.\
110
111
  Arguments:
111
- * componentName : REQUIRED : The name of the component to compare (can be replaced by componentId)
112
- * componentId : OPTIONAL : The ID of the component to compare
113
- * publishedVersion : OPTIONAL : if you want to compare to the version that has been published in your base to your target latest version.
112
+ * componentName : REQUIRED : the name of the component to compare
113
+ * componentID : REQUIRED : the id of the component to compare
114
+ * publishedVersion : OPTIONAL : if you want to compare to the version that has been published in your base vs the published version of your target.
115
+ possible kwargs:
116
+ * 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.
117
+ * 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
118
+ * 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
114
119
 
115
120
  ### Check realised for component
116
121
 
117
122
  The script will provide these evaluations:
118
123
  For Extensions:
119
- * It will check if the same version of the extension is used (return `False` in case different versions)
120
- * It will check if the same settings has been applied (return `False` else `True`)
124
+ * It will check if the extension is also present in that property. (returns the following sentence `f'Extension "{extension_name}" is not present'` if not the case )
125
+ * It will check if the same version of the extension is used (return the following sentence `f'Extension version is different: {version_base} vs {version_target}` in case different versions)
126
+ * It will check if the same settings has been applied (return `'Extension settings are different'` if not the same)
121
127
 
122
128
  For Rules:
123
- * It will check if the rules contain the same number of ruleComponents (return `False` in case different numbers)
124
- * It will check each ruleComponent if it can be found (based on Name)
125
- * If the ruleComponent name cannot be found in the target property : return `False`
126
- * If the ruleComponent name is found and the settings are different : return `False`
127
- * If all ruleComponent names have been found and their settings are identical :
128
- * If ruleComponent is an event :
129
- * If the rule_order attribute is the same return `True` else return `False`
130
- * If the ruleComponent is an action :
131
- * If the timeout attribute is the same return `True` else return `False`
132
- * neither an action or event ruleComponent : return `True` as settings are identical.
129
+ * It will check if the rule exist in the target property (if not, returning `"Rule does not exist in Target"`)
130
+ * It will check if the rules contain the same number of ruleComponents (return `"The rule does not have the same number of components"` in case different numbers)
131
+ * It will check if the enabled state is the same than your base (if not, returning `"Rule enabled status is different"`)
132
+ * It will check if the published state is the same than your base (if not, returning `"Rule published status is different"`)
133
+ * It will check each ruleComponent if it can be found (based on name) and generate a list of differences based on each component check:
134
+ * If the ruleComponent name cannot be found in the target property : return `f'component {rule-component-name} does not exist in Target'`
135
+ * If the ruleComponent is an event it will check if the order of the event is different. If different, it will return: `rule_order is different`
136
+ * If the ruleComponent is an condition or an action it will check if the tiemout is different. If different, it will return: `f"condition|action {rule-component-name} timeout is different"`
137
+ * If the ruleComponent cannot be found in the target rule, it will add the following elements: `'component {rule-component-name} does not exist in Target'`
138
+ * If the ruleComponent name is found and the settings are different : return `f'<componentType> {rule-component-name} has different settings'`.\
139
+ ComponentType can be: `event, condition, action`\
140
+ The settings can be check globally or you can specify a specific part of the setting to be checked (see `possible kwargs`)
141
+ * If the ruleComponent are all similar, then the result will be `Similar`
142
+
133
143
 
134
144
  For Data Elements:
135
- * It will check if the Data Element name can be found : return `False` if not found
136
- * If component Name is found and settings are the same : return `True`, else return `False`
145
+ * It will check if the Data Element name can be found : return `f'Data Element "{data-element-name}" does not exist in Target'` if not found
146
+ * If Data Element (based on name) is found and settings are the same : return `"Similar"`, else return `"Data Element settings are different"`
147
+
148
+ #### Additional elements
149
+ On top of the different properties check, additional information will be provided for the base component:
150
+ * `base-publihsed`: If the current component check is published.\
151
+ **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.
152
+ * `base-enabled`: If the current component check is enabled.
153
+ * `base-state`:
154
+ * `Latest`: If the component retrieved is the latest latest version
155
+ * `Unknown`: If the component retrieved cannot be evaluated to know if it is the latest.
156
+ * `Draft` : If the component retrieved is not the latest and it has not yet been published
157
+ * `Edited`: If the component retrieved is not the latest and has been published before.
158
+
159
+ #### Similar vs Same
160
+ The idea of checking the similarity of 2 components is partially done as not everything can be tested during the check.\
161
+ For that reason, the wording `Similar` is used and not `Same` as they are not technically the same. (different ids, different things.)
162
+
163
+ #### publishedVersion for checking
164
+ In the case that you want to compare the published versions, 2 similar problems could occure:
165
+ - **The base component is not published**. The result of that would be the **failing of the comparison** (an exception will be thrown) because you need to have your base clean to start a comparison with a published version.
166
+ - **One or all the Targets properties do not have the component published**. In that case, I tried to handle that nicely and then try to default to the latest version for comparison. In the result, the following parenthesis would be added `(draft)`
167
+
168
+ Therefore, the `(draft)` is always referring to the target property.
137
169
 
138
170
  ## Dynamic Component filter
139
171
 
@@ -172,7 +204,7 @@ An example on how the data element looks like:
172
204
 
173
205
  #### Data Elements
174
206
 
175
- You can pass a Data Element name during the instanciation of the `Synchronizer`. This data element needs to resides in the base property and it will contain a valid JSON that will describe the different rules that will apply during the synchronization.\
207
+ You can pass a Data Element name during the instanciation of the `Synchronizer`. This data element needs to resides in the base property and it will contain a **valid** JSON that will describe the different rules that will apply during the synchronization.\
176
208
  The format of the JSON representation for the data element is [explained above](#dynamic-component-filter)\
177
209
  The data element name needs to be passed in the `dynamicRuleComponent` parameter.
178
210
 
@@ -0,0 +1 @@
1
+ __version__ = "0.4.5-1"
@@ -63,7 +63,7 @@ class Property:
63
63
  return str(json.dumps(self.definition, indent=4))
64
64
 
65
65
 
66
- def _getExtensionPackage(self, ext_name: str, platform: str = "web", verbose: bool = False)->dict:
66
+ def _getExtensionPackageIdByName(self, ext_name: str, platform: str = "web", verbose: bool = False)->dict:
67
67
  """
68
68
  Retrieve extension id of the catalog from an extension name.
69
69
  It will be used later on to check for available updates.
@@ -82,6 +82,23 @@ class Property:
82
82
  print('extension name : ' + str(data['attributes']['name']))
83
83
  print('extension id : ' + str(data['id']))
84
84
  return extension_id
85
+
86
+ def getExtensionPackage(self, extension_id: str = None, verbose: bool = False)->dict:
87
+ """
88
+ Retrieve the extension package information based on the extension id.
89
+ Arguments:
90
+ extension_id : REQUIRED : ID of the extension package to retrieve
91
+ verbose : OPTIONAL : if set to True, will print the extension name and id.
92
+ """
93
+ if extension_id is None:
94
+ raise ValueError("You must provide an extension id")
95
+ path = f'/extension_packages/{extension_id}'
96
+ res_ext = self.connector.getData(self.endpoint+path)
97
+ data = res_ext['data']
98
+ if verbose:
99
+ print('extension name : ' + str(data['attributes']['name']))
100
+ print('extension id : ' + str(data['id']))
101
+ return data
85
102
 
86
103
  def getRessource(self,res_url: str = None, params: dict = None):
87
104
  """
@@ -145,6 +162,16 @@ class Property:
145
162
  except:
146
163
  data = extensions
147
164
  return data
165
+
166
+ def getExtension(self,extensionId:str=None)-> object:
167
+ """
168
+ Retrieve the definiton of the extension based on its ID.
169
+ Arguments:
170
+ extensionId : REQUIRED : ID of the extension to retrieve
171
+ """
172
+ path = f"/extensions/{extensionId}"
173
+ extensions = self.connector.getData(self.endpoint+path)
174
+ return extensions['data']
148
175
 
149
176
  def checkExtensionUpdate(self, name:str=None, platform: str = "web", verbose: bool = False):
150
177
  """
@@ -174,7 +201,7 @@ class Property:
174
201
  'internal_id': ext['id']
175
202
  } for ext in extensions if ext['attributes']['name'] == name}
176
203
  for name in dict_extensions:
177
- new_id = self._getExtensionPackage(name, platform=platform, verbose=verbose)
204
+ new_id = self._getExtensionPackageIdByName(name, platform=platform, verbose=verbose)
178
205
  if new_id != dict_extensions[name]['package_id']:
179
206
  dict_extensions[name]['package_id'] = new_id
180
207
  dict_extensions[name]['update'] = True
@@ -188,28 +215,63 @@ class Property:
188
215
  extension_id : REQUIRED : Your internal ID for this extension in your property (EX....)
189
216
  package_id : REQUIRED : new extension id for the extension (EP...)
190
217
  """
191
- data = {'data': {
192
- 'id': extension_id,
193
- "type": "extensions",
194
- "relationships": {"extension_package": {
195
- "links": {
196
- "related": "https://reactor.adobe.io/extensions/"+extension_id+"/extension_package"
197
- },
198
- "data": {
199
- "id": package_id,
200
- "type": "extension_packages"
218
+ if extension_id is None:
219
+ raise ValueError("You must provide an extension id")
220
+ if package_id is None:
221
+ raise ValueError("You must provide a package id")
222
+ myExtension = self.getExtension(extension_id)
223
+ latest_package_id = myExtension.get('meta').get('upgrade_extension_package_id',None)
224
+ current_package_id = myExtension['relationships']['extension_package']['data']['id']
225
+ if latest_package_id is None:
226
+ settings = myExtension.get('attributes',{}).get('settings','{}')
227
+ attributes = {'delegate_descriptor_id': f'{myExtension.get("attributes",{}).get("name")}::extensionConfiguration::config','settings':settings}
228
+ data = {'data': {
229
+ 'id': extension_id,
230
+ "type": "extensions",
231
+ "relationships": {"extension_package": {
232
+ "links": {
233
+ "related": "https://reactor.adobe.io/extensions/"+extension_id+"/extension_package"
234
+ },
235
+ "data": {
236
+ "id": package_id,
237
+ "type": "extension_packages"
238
+ }
239
+ }
240
+ },
241
+ 'meta': {'upgrade_extension_package_id': package_id}
242
+ }
201
243
  }
202
- }
203
- },
204
- 'meta': {
205
- 'upgrade_extension_package_id': package_id
206
- }
207
- }
208
- }
244
+ else:
245
+ if latest_package_id != current_package_id: ##using a different package that the one provided
246
+ settings = myExtension.get('attributes',{}).get('settings','{}')
247
+ attributes = {'delegate_descriptor_id': f'{myExtension.get("attributes",{}).get("name")}::extensionConfiguration::config','settings':settings}
248
+ data = {'data': {
249
+ "attributes": attributes,
250
+ 'id': extension_id,
251
+ "type": "extensions",
252
+ "relationships": {"extension_package": {
253
+ "links": {
254
+ "related": "https://reactor.adobe.io/extensions/"+extension_id+"/extension_package"
255
+ },
256
+ "data": {
257
+ "id": latest_package_id,
258
+ "type": "extension_packages"
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ else:
265
+ raise ValueError("Extension already up to date")
266
+
209
267
  path = f"/extensions/{extension_id}"
210
268
  res = self.connector.patchData(
211
269
  self.endpoint+path, data=data)
212
- return res
270
+ try:
271
+ data = res['data'] # skip meta for now
272
+ except:
273
+ data = res
274
+ return data
213
275
 
214
276
  def getProfile(self)->dict:
215
277
  """
@@ -926,20 +988,27 @@ class Property:
926
988
  lastPage=True
927
989
  return data
928
990
 
929
- def getLatestPublishedVersion(self,revisions:list=None)->dict:
991
+ def getLatestPublishedVersion(self,revisions:Union[list,dict]=None)->dict:
930
992
  """
931
993
  Find the latest published version of a component based on the list of revisions retrieved via getRevisions methods.
932
994
  Arguments:
933
- revisions : REQUIRED : list of revisions
995
+ revisions : REQUIRED : list of revisions or dictionary of the component.
934
996
  """
935
997
  if revisions is None:
936
- raise ValueError('Require a list of revisions')
998
+ raise ValueError('Require a list of revisions at least')
999
+ if type(revisions) == dict:
1000
+ revisions = self.getRevisions(element=revisions)
937
1001
  publishedIndexVersions:dict = {index:rev['attributes']['revision_number'] for index, rev in enumerate(revisions) if rev['attributes']['published'] == True}
1002
+ maxVersion = max(e['attributes']['revision_number'] for e in revisions)
938
1003
  if len(publishedIndexVersions) == 0:
939
- raise IndexError("You want to retrieve a published version of the component.\nBut no published version can be found. Please check if your component has been published")
1004
+ raise IndexError(f"You want to retrieve a published version of the component.\nBut no published version can be found in {self.name}. Please check if your component has been published")
940
1005
  maxRevisionIndex = [index for index,value in publishedIndexVersions.items() if value == max(list(publishedIndexVersions.values()))][0]
941
- return revisions[maxRevisionIndex]
942
-
1006
+ publishedVersion = revisions[maxRevisionIndex]
1007
+ if maxVersion == publishedVersion['attributes']['revision_number']:
1008
+ publishedVersion['attributes']['latest'] = True
1009
+ else:
1010
+ publishedVersion['attributes']['latest'] = False
1011
+ return publishedVersion
943
1012
 
944
1013
  def reviseDataElement(self, dataElement_id: str)->dict:
945
1014
  """
@@ -1127,6 +1196,8 @@ class Property:
1127
1196
  argument:
1128
1197
  extension_id : REQUIRED : the extension id
1129
1198
  attr_dict : REQUIRED : dictionary that will be passed to Launch for update
1199
+ possible kwargs:
1200
+ upgradeLatest : OPTIONAL : Boolean if you want to update the relationships of the extension to the new version
1130
1201
  """
1131
1202
  obj = {
1132
1203
  "data": {
@@ -1135,8 +1206,7 @@ class Property:
1135
1206
  "type": "extensions"
1136
1207
  }
1137
1208
  }
1138
- extensions = self.connector.patchData(
1139
- config.endpoints['global']+'/extensions/'+extension_id, data=obj)
1209
+ extensions = self.connector.patchData(self.endpoint+'/extensions/'+extension_id, data=obj)
1140
1210
  try:
1141
1211
  data = extensions['data']
1142
1212
  except:
@@ -6,6 +6,7 @@ from .library import Library
6
6
  from .launchpy import Translator, copySettings
7
7
  from collections import defaultdict
8
8
  from copy import deepcopy
9
+ from aepp import som
9
10
 
10
11
  class Synchronizer:
11
12
  """
@@ -54,7 +55,7 @@ class Synchronizer:
54
55
  self.targets[target]['rules'] = self.targets[target]['api'].getRules()
55
56
  self.targets[target]['extensions'] = self.targets[target]['api'].getExtensions()
56
57
  self.targets[target]['dataElements'] = self.targets[target]['api'].getDataElements()
57
- self.targets[target]['libraryStack'] = {'dataElements':[],'rules':[]}
58
+ self.targets[target]['libraryStack'] = {'dataElements':[],'rules':[],"extensions":[]}
58
59
  self.translator.extendExtensions(self.targets[target]['extensions'],self.targets[target]['name'])
59
60
  if len(self.targets[target]['rules']) > 0:
60
61
  self.translator.extendRules(self.targets[target]['rules'],self.targets[target]['name'])
@@ -114,23 +115,14 @@ class Synchronizer:
114
115
  'inclComponents': deepcopy(self.dict_config[rule]['inclComponents'])
115
116
  }
116
117
 
117
-
118
- def syncComponent(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,**kwargs)->None:
118
+ def __prepareBaseComponent__(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False)->dict:
119
119
  """
120
- Synchronize a component from the base property to the different target properties.
121
- It will detect the component with the same name in target properties.
122
- It will delete the components related objects (ruleComponents, data element code) in the target property and push the template version.
123
- 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}
120
+ Prepare the base component to be used in the syncComponent method.
124
121
  Arguments:
125
122
  componentName : REQUIRED : the name of the component to sync
126
123
  componentID : REQUIRED : the id of the component to sync
127
124
  publishedVersion : OPTIONAL : if you want to take the version that has been published
128
- possible kwargs:
129
- timeout : OPTIONAL : The timeout to be used for the rule component. If not provided, the existing timeout will be used.
130
125
  """
131
- timeout = kwargs.get('timeout',None)
132
- if componentName is None and componentId is None:
133
- raise ValueError('Require a component Name of a component ID')
134
126
  cmp_base=None
135
127
  if componentId is not None:
136
128
  if componentId.startswith('DE'): ## if data element
@@ -143,6 +135,11 @@ class Synchronizer:
143
135
  if len(componentBase)==0:
144
136
  raise KeyError("Component ID cannot be found")
145
137
  cmp_base = componentBase[0]
138
+ elif componentId.startswith('EX'): ## if an extension
139
+ componentBase = [ext for ext in self.base['extensions'] if ext['id'] == componentId]
140
+ if len(componentBase)==0:
141
+ raise KeyError("Component ID cannot be found")
142
+ cmp_base = componentBase[0]
146
143
  if componentId is None and componentName is not None: ## If only componentName
147
144
  for rule in self.base['rules']:
148
145
  if componentName == rule['attributes']['name']:
@@ -151,12 +148,18 @@ class Synchronizer:
151
148
  for de in self.base['dataElements']:
152
149
  if componentName == de['attributes']['name']:
153
150
  cmp_base = de
151
+ if cmp_base is None:
152
+ for ext in self.base['extensions']:
153
+ if componentName == ext['attributes']['name']:
154
+ cmp_base = ext
154
155
  ## In case we do not find any match
155
156
  if cmp_base is None:
156
157
  raise KeyError("The component ID or component Name cannot be matched in your template property")
157
158
  ## Here creating a dictionary that provide all information related to your component
158
159
  cmp_baseDict = {'id':cmp_base['id'],'name':cmp_base['attributes']['name'],'component':cmp_base,'copy':copySettings(cmp_base)}
159
- if publishedVersion:
160
+ if cmp_baseDict['component']['type'] == 'extensions':
161
+ cmp_baseDict['version'] = cmp_base['attributes'].get('version','1.0.0')
162
+ if publishedVersion and cmp_baseDict['component']['type'] not in ['extensions']:
160
163
  data = self.base['api'].getRevisions(cmp_baseDict['component'])
161
164
  publishedVersion = self.base['api'].getLatestPublishedVersion(data)
162
165
  if publishedVersion['attributes']['name'] != cmp_baseDict['name']:
@@ -173,6 +176,25 @@ class Synchronizer:
173
176
  cmp_baseDict['name'] = publishedVersion['attributes']['name']
174
177
  cmp_baseDict['component'] = publishedVersion
175
178
  cmp_baseDict['copy'] = copySettings(publishedVersion)
179
+ return cmp_baseDict
180
+
181
+ def syncComponent(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,**kwargs)->None:
182
+ """
183
+ Synchronize a component from the base property to the different target properties.
184
+ It will detect the component with the same name in target properties.
185
+ It will delete the components related objects (ruleComponents, data element code) in the target property and push the template version.
186
+ 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}
187
+ Arguments:
188
+ componentName : REQUIRED : the name of the component to sync
189
+ componentID : REQUIRED : the id of the component to sync
190
+ publishedVersion : OPTIONAL : if you want to take the version that has been published
191
+ possible kwargs:
192
+ timeout : OPTIONAL : The timeout to be used for the rule component. If not provided, the existing timeout will be used.
193
+ """
194
+ timeout = kwargs.get('timeout',None)
195
+ if componentName is None and componentId is None:
196
+ raise ValueError('Require a component Name of a component ID')
197
+ cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion)
176
198
  ## handling the data element
177
199
  if cmp_baseDict['component']['type'] == 'data_elements':
178
200
  for target in list(self.targets.keys()):
@@ -206,7 +228,7 @@ class Synchronizer:
206
228
  self.targets[target]['libraryStack']['dataElements'].append(comp)
207
229
  self.targets[target]['dataElements'].append(comp)
208
230
  else:
209
- index,old_component = [(index,de) for index,de in enumerate(self.targets[target]['dataElements']) if de['attributes']['name'] == cmp_baseDict['name']][0]
231
+ index,old_component = [(index,de) for index,de in enumerate(self.targets[target]['dataElements']) if de.get('attributes',{}).get('name') == cmp_baseDict['name']][0]
210
232
  attributes = {
211
233
  "name" : translatedComponent['name'],
212
234
  "enabled" : translatedComponent["enabled"],
@@ -359,6 +381,13 @@ class Synchronizer:
359
381
  self.targets[target]['library'].updateDataElements(existingDataElements)
360
382
  if len(newDataElements)>0:
361
383
  self.targets[target]['library'].addDataElements(newDataElements)
384
+ ## taking care of the extensions
385
+ existingExtensions = [ext['id'] for ext in self.targets[target]['library'].relationships['extensions'] if ext['id'] in [e['id'] for e in self.targets[target]['libraryStack']['extensions']]]
386
+ newExtensions = [ext['id'] for ext in self.targets[target]['libraryStack']['extensions'] if ext['id'] not in existingExtensions]
387
+ if len(existingExtensions) > 0:
388
+ self.targets[target]['library'].updateExtensions(existingExtensions)
389
+ if len(newExtensions)>0:
390
+ self.targets[target]['library'].addExtensions(newExtensions)
362
391
 
363
392
  def upgradeTargetExtension(self,extensionName:str=None,platform:str="web")->dict:
364
393
  """
@@ -370,14 +399,20 @@ class Synchronizer:
370
399
  """
371
400
  if extensionName is None:
372
401
  raise ValueError("Require an extension name")
373
- for prop, target in self.targets:
402
+ for prop, target in self.targets.items():
374
403
  try:
375
404
  extensionUpdate = target['api'].checkExtensionUpdate(extensionName)
405
+ for extName, extUpdateDict in extensionUpdate.items():
406
+ if extUpdateDict["update"]:
407
+ res = target['api'].upgradeExtension(extUpdateDict['internal_id'],extUpdateDict["package_id"])
408
+ target['libraryStack']['extensions'].append(res)
409
+ if len([index for index, ext in enumerate(target['extensions']) if ext['attributes']['name'] == extName])>0:
410
+ index = [index for index, ext in enumerate(target['extensions']) if ext['attributes']['name'] == extName][0]
411
+ del target['extensions'][index]
412
+ target['extensions'].append(res)
413
+
376
414
  except:
377
415
  raise ValueError(f"Could not find an extension name: {extensionName}")
378
-
379
-
380
-
381
416
 
382
417
 
383
418
  def renameComponent(self,old_name:str=None,new_name:str=None)->None:
@@ -419,83 +454,62 @@ class Synchronizer:
419
454
  self.targets[target]['libraryStack']['rules'].append(comp)
420
455
 
421
456
 
422
- def checkComponentSync(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,**kwargs)->bool:
457
+ def checkComponentSync(self,componentName:str=None,componentId:str=None,publishedVersion:bool=False,excludeSimilar:bool=False,**kwargs)->bool:
423
458
  """
424
459
  Check if the component,from the base property, is synced to the different target properties.
425
460
  It can also check for the Extensions.
426
- It will returned {targetProperty: True} when component have same settings or {targetPropery : False} when component do not have same settings.
461
+ It will return a dictionary with the key being the target property and the value being the result of the evaluation, such as {targetProperty: 'similar'} when component have same settings or {targetPropery : False} when component do not have same settings.
427
462
  Arguments:
428
463
  componentName : REQUIRED : the name of the component to compare
429
464
  componentID : REQUIRED : the id of the component to compare
430
- publishedVersion : OPTIONAL : if you want to compare to the version that has been published in your base
465
+ publishedVersion : OPTIONAL : if you want to compare to the version that has been published in your base vs the published version of your target.
466
+ 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.
467
+ possible kwargs:
468
+ 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.
469
+ 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
470
+ 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
431
471
  """
432
472
  if componentName is None and componentId is None:
433
473
  raise ValueError('Require a component Name of a component ID')
434
- cmp_base=None
435
- if componentId is not None:
436
- if componentId.startswith('DE'): ## if data element
437
- componentBase = [de for de in self.base['dataElements'] if de['id'] == componentId]
438
- if len(componentBase)==0:
439
- raise KeyError("Component ID cannot be found")
440
- cmp_base = componentBase[0]
441
- elif componentId.startswith('RL'): ## if a rule
442
- componentBase = [de for de in self.base['rules'] if de['id'] == componentId]
443
- if len(componentBase)==0:
444
- raise KeyError("Component ID cannot be found")
445
- cmp_base = componentBase[0]
446
- elif componentId.startswith('EX'): ## if an extension
447
- componentBase = [ext for ext in self.base['extensions'] if ext['id'] == componentId]
448
- if len(componentBase)==0:
449
- raise KeyError("Component ID cannot be found")
450
- cmp_base = componentBase[0]
451
- if componentId is None and componentName is not None: ## If only componentName
452
- for rule in self.base['rules']:
453
- if componentName == rule['attributes']['name']:
454
- cmp_base = rule
455
- if cmp_base is None: ## data elements
456
- for de in self.base['dataElements']:
457
- if componentName == de['attributes']['name']:
458
- cmp_base = de
459
- if cmp_base is None: ## extensions
460
- for ext in self.base['extensions']:
461
- if componentName == ext['attributes']['name']:
462
- cmp_base = ext
463
- ## In case we do not find any match
464
- if cmp_base is None:
465
- raise KeyError("The component ID or component Name cannot be matched in your template property")
466
- ## Here creating a dictionary that provide all information related to your component
467
- cmp_baseDict = {'id':cmp_base['id'],'name':cmp_base['attributes']['name'],'component':cmp_base,'copy':copySettings(cmp_base)}
468
- if publishedVersion:
469
- data = self.base['api'].getRevisions(cmp_baseDict['component'])
470
- publishedVersion = self.base['api'].getLatestPublishedVersion(data)
471
- if publishedVersion['attributes']['name'] != cmp_baseDict['name']:
472
- ## Updating mapping table with old name when published version name diff than last version name.
473
- if cmp_baseDict['component']['type'] == 'rules':
474
- self.translator.extendBaseRules(
475
- ruleName=publishedVersion['attributes']['name'],
476
- ruleId=publishedVersion['id'],
477
- property_name=self.base["name"])
478
- self.base['rules'].append(publishedVersion)
479
- if cmp_baseDict['component']['type'] == 'data_elements':
480
- self.base['dataElements'].append(publishedVersion)
481
- if cmp_baseDict['component']['type'] == 'extensions':
482
- self.base['extensions'].append(publishedVersion)
483
- cmp_baseDict['id'] = publishedVersion['id']
484
- cmp_baseDict['name'] = publishedVersion['attributes']['name']
485
- cmp_baseDict['component'] = publishedVersion
486
- cmp_baseDict['copy'] = copySettings(publishedVersion)
487
- dict_result = {tar:False for tar in self.targets.keys()}
474
+ cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion)
475
+ dict_result = {tar:"" for tar in self.targets.keys()}
476
+ dict_result['base-enabled'] = cmp_baseDict['component']['attributes'].get('enabled',False)
477
+ dict_result['base-published'] = cmp_baseDict['component']['attributes'].get('published',False)
478
+ if cmp_baseDict['component']['attributes'].get('latest',False):
479
+ dict_result['base-state'] = "Latest"
480
+ else:
481
+ if cmp_baseDict['component']['attributes'].get('latest','unknown') == 'unknown':
482
+ if dict_result['base-enabled']:
483
+ dict_result['base-state'] = "Draft"
484
+ else:
485
+ dict_result['base-state'] = "Unknown"
486
+ elif dict_result['base-published'] == False:
487
+ dict_result['base-state'] = "Draft"
488
+ else:
489
+ dict_result['base-state'] = "Edited"
488
490
  if cmp_baseDict['component']['type'] == 'data_elements':
489
491
  for target in list(self.targets.keys()):
490
492
  ## if it does not exist
491
- if cmp_baseDict['name'] not in [de.get('attributes',{}).get('name') for de in self.targets[target]['dataElements']]:
492
- dict_result[target] = False
493
+ if cmp_baseDict['name'] not in [de.get('attributes',{}).get('name') for de in self.targets[target]['dataElements']]:
494
+ dict_result[target] = f'Data Element "{cmp_baseDict["name"]}" does not exist in Target'
495
+ else:
496
+ index,target_de = [(index,de) for index,de in enumerate(self.targets[target]['dataElements']) if de['attributes']['name'] == cmp_baseDict['name']][0]
497
+ issue_pub = ""
498
+ if publishedVersion:
499
+ try:
500
+ revisions_dataElement = self.targets[target]["api"].getRevisions(target_de)
501
+ target_de = self.targets[target]["api"].getLatestPublishedVersion(revisions_dataElement)
502
+ except:
503
+ issue_pub = " (draft)"
504
+ if target_de['attributes']['enabled'] != cmp_baseDict['component']['attributes']['enabled']:
505
+ dict_result[target] = "Data Element enabled status is different"+issue_pub
506
+ if target_de['attributes']['published'] != cmp_baseDict['component']['attributes']['published']:
507
+ dict_result[target] = "Data Element published status is different"+issue_pub
508
+ if target_de['attributes']['settings'] == cmp_baseDict['component']['attributes']['settings']:
509
+ if not excludeSimilar:
510
+ dict_result[target] = "Similar"+issue_pub
493
511
  else:
494
- index,old_component = [(index,de) for index,de in enumerate(self.targets[target]['dataElements']) if de['attributes']['name'] == cmp_baseDict['name']][0]
495
- if old_component['attributes']['settings'] == cmp_baseDict['component']['attributes']['settings']:
496
- dict_result[target] = True
497
- else:
498
- dict_result[target] = False
512
+ dict_result[target] = "Data Element settings are different"+issue_pub
499
513
  if cmp_baseDict['component']['type'] == 'rules':
500
514
  rcsLink = cmp_baseDict['component'].get('relationships',{}).get('rule_components',{}).get('links',{}).get('related')
501
515
  resResource = self.base['api'].getRessource(rcsLink)
@@ -506,53 +520,113 @@ class Synchronizer:
506
520
  for target in list(self.targets.keys()):
507
521
  ## if rule does not exist
508
522
  if cmp_baseDict['name'] not in [rule['attributes']['name'] for rule in self.targets[target]['rules']]:
509
- dict_result[target] = False
523
+ dict_result[target] = "Rule does not exist in Target"
510
524
  else:
511
525
  index, targetRule = [(index,rule) for index, rule in enumerate(self.targets[target]['rules']) if rule['attributes']['name'] == cmp_baseDict['name']][0]
512
- targetRuleId = targetRule['id']
526
+ issue_pub = ""
527
+ componentsDifferences = [] ## list of differences for rule components for rule to check difference
528
+ if publishedVersion:
529
+ try:
530
+ revisions_targetRule = self.targets[target]["api"].getRevisions(targetRule)
531
+ targetRule = self.targets[target]["api"].getLatestPublishedVersion(revisions_targetRule)
532
+ except:
533
+ issue_pub = " (not published)"
534
+ if targetRule['attributes']['enabled'] != cmp_baseDict['component']['attributes']['enabled']:
535
+ componentsDifferences.append("Rule enabled status is different")
536
+ if targetRule['attributes']['published'] != cmp_baseDict['component']['attributes']['published']:
537
+ componentsDifferences.append("Rule published status is different")
513
538
  rcsLinkTarget = targetRule.get('relationships',{}).get('rule_components',{}).get('links',{}).get('related')
514
539
  resResource = self.targets[target]['api'].getRessource(rcsLinkTarget)
515
- old_components:list = resResource['data']
540
+ target_rule_components:list = resResource['data']
516
541
  #if not same amunt of rule component
517
- if len(template_ruleComponents) != len(old_components):
518
- dict_result[target] = False
519
- dict_result[target] = False ## bydefault, there is a difference
520
- componentsDifferences = [] ## array of compontent for rule to check difference
542
+ if len(template_ruleComponents) != len(target_rule_components):
543
+ dict_result[target] = "The rule does not have the same number of components"
521
544
  for base_comp in template_ruleComponents:
522
545
  checkExist = False
523
- for old_comp in old_components:
524
- if old_comp['attributes']['name'] == base_comp['attributes']['name']:
546
+ for comp in target_rule_components:
547
+ if comp['attributes']['name'] == base_comp['attributes']['name']:
525
548
  checkExist = True
526
- if old_comp['attributes']['settings'] == base_comp['attributes']['settings']:
527
- if '::events::' in base_comp['attributes']['delegate_descriptor_id']:
528
- if old_comp['attributes']['rule_order'] == base_comp['attributes']['rule_order']:
529
- componentsDifferences.append(True)
530
- else:
531
- componentsDifferences.append(False)
532
- elif '::actions::' in base_comp['attributes']['delegate_descriptor_id']:
533
- if old_comp['attributes']['timeout'] == base_comp['attributes']['timeout']:
534
- componentsDifferences.append(True)
535
- else:
536
- componentsDifferences.append(False)
537
- else: ## no action no event but same settings
538
- componentsDifferences.append(True)
539
- else: ## settings not the same
540
- componentsDifferences.append(False)
549
+ if '::events::' in base_comp['attributes']['delegate_descriptor_id']:
550
+ if comp['attributes']['rule_order'] != base_comp['attributes']['rule_order']:
551
+ componentsDifferences.append("rule_order is different")
552
+ if kwargs.get('event_setting_path',None) is not None:
553
+ list_event_path = kwargs.get('event_setting_path',None)
554
+ if type(list_event_path) == str:
555
+ list_event_path = list(list_event_path)
556
+ mySettings_base = som.Som(json.loads(base_comp['attributes']['settings']))
557
+ mySettings_target = som.Som(json.loads(comp['attributes']['settings']))
558
+ for path in list_event_path:
559
+ tmp_base = mySettings_base.get(path,'')
560
+ tmp_target = mySettings_target.get(path,'')
561
+ if tmp_base != tmp_target:
562
+ componentsDifferences.append(f'event "{comp['attributes']['name']}" has different settings')
563
+ else:
564
+ if base_comp['attributes']['settings'] != comp['attributes']['settings']:
565
+ componentsDifferences.append(f'event "{comp['attributes']['name']}" has different settings')
566
+ elif '::conditions::' in base_comp['attributes']['delegate_descriptor_id']:
567
+ if comp['attributes']['timeout'] != base_comp['attributes']['timeout']:
568
+ componentsDifferences.append(f'condition "{comp['attributes']['name']}" timeout is different')
569
+ if kwargs.get('condition_setting_path',None) is not None:
570
+ list_event_path = kwargs.get('condition_setting_path',None)
571
+ if type(list_event_path) == str:
572
+ list_event_path = list(list_event_path)
573
+ mySettings_base = som.Som(json.loads(base_comp['attributes']['settings']))
574
+ mySettings_target = som.Som(json.loads(comp['attributes']['settings']))
575
+ for path in list_event_path:
576
+ tmp_base = mySettings_base.get(path,'')
577
+ tmp_target = mySettings_target.get(path,'')
578
+ if tmp_base != tmp_target:
579
+ componentsDifferences.append(f'condition "{comp['attributes']['name']}" has different settings')
580
+ else:
581
+ if base_comp['attributes']['settings'] != comp['attributes']['settings']:
582
+ componentsDifferences.append(f'condition "{comp['attributes']['name']}" has different settings')
583
+ elif '::actions::' in base_comp['attributes']['delegate_descriptor_id']:
584
+ if comp['attributes']['timeout'] != base_comp['attributes']['timeout']:
585
+ componentsDifferences.append(f'action "{comp['attributes']['name']}" timeout is different')
586
+ if kwargs.get('action_setting_path',None) is not None:
587
+ list_actions_path = kwargs.get('action_setting_path',None)
588
+ if type(list_actions_path) == str:
589
+ list_actions_path = list(list_actions_path)
590
+ mySettings_base = som.Som(json.loads(base_comp['attributes']['settings']))
591
+ mySettings_target = som.Som(json.loads(comp['attributes']['settings']))
592
+ for path in list_actions_path:
593
+ tmp_base = mySettings_base.get(path,'')
594
+ tmp_target = mySettings_target.get(path,'')
595
+ if tmp_base != tmp_target:
596
+ componentsDifferences.append(f'action "{comp['attributes']['name']}" has different settings')
597
+ else:
598
+ if base_comp['attributes']['settings'] != comp['attributes']['settings']:
599
+ componentsDifferences.append(f'action "{comp['attributes']['name']}" has different settings')
541
600
  if checkExist == False: ## does not exist
542
- componentsDifferences.append(False)
543
- if all(componentsDifferences):
544
- dict_result[target] = True
601
+ componentsDifferences.append(f'component "{base_comp['attributes']['name']}" does not exist in Target')
602
+ if len(componentsDifferences)>0:
603
+ dict_result[target] = ','.join(componentsDifferences) + issue_pub
604
+ else:
605
+ if not excludeSimilar:
606
+ dict_result[target] = 'Similar' + issue_pub
545
607
  if cmp_baseDict['component']['type'] == 'extensions':
546
608
  for target in list(self.targets.keys()):
547
609
  if cmp_baseDict['name'] not in [ext['attributes']['name'] for ext in self.targets[target]['extensions']]:
548
- dict_result[target] = False
610
+ dict_result[target] = f'Extension "{cmp_baseDict['name']}" is not present'
549
611
  else:
550
612
  index, extensionTarget = [(index,ext) for index, ext in enumerate(self.targets[target]['extensions']) if ext['attributes']['name'] == cmp_baseDict['name']][0]
613
+ issue_pub = ""
614
+ if publishedVersion:
615
+ try:
616
+ revisions_targetExt = self.targets[target]["api"].getRevisions(extensionTarget)
617
+ extensionTarget = self.targets[target]["api"].getLatestPublishedVersion(revisions_targetExt)
618
+ except:
619
+ issue_pub = " (not published)"
620
+ if extensionTarget['attributes']['enabled'] != cmp_baseDict['component']['attributes']['enabled']:
621
+ dict_result[target] = f'Extension enabled status is different: {cmp_baseDict["component"]["attributes"]["enabled"]} vs {extensionTarget["attributes"]["enabled"]}'+issue_pub
622
+ if extensionTarget['attributes']['published'] != cmp_baseDict['component']['attributes']['published']:
623
+ dict_result[target] = f'Extension published status is different: {cmp_baseDict["component"]["attributes"]["published"]} vs {extensionTarget["attributes"]["published"]}'+issue_pub
551
624
  if extensionTarget['attributes']['version'] != cmp_baseDict['component']['attributes']['version']:
552
- dict_result[target] = False
625
+ dict_result[target] = f'Extension version is different: {cmp_baseDict['component']['attributes']['version']} vs {extensionTarget['attributes']['version']}'+issue_pub
553
626
  else:
554
627
  if extensionTarget['attributes']['settings'] != cmp_baseDict['component']['attributes']['settings']:
555
- dict_result[target] = False
628
+ dict_result[target] = 'Extension settings are different'+issue_pub
556
629
  else:
557
- dict_result[target] = True
630
+ if not excludeSimilar:
631
+ dict_result[target] = 'Similar'+issue_pub
558
632
  return dict_result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: launchpy
3
- Version: 0.4.4.post3
3
+ Version: 0.4.5.post1
4
4
  Summary: Adobe Launch API wrapper
5
5
  Home-page: https://github.com/pitchmuc/launchpy
6
6
  Author: Julien Piccini
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.7
19
19
  Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Development Status :: 4 - Beta
22
- Requires-Python: >=3.7
22
+ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pandas
@@ -2,4 +2,6 @@ pathlib>=1.0.0
2
2
  PyJWT[crypto]>=1.7.1
3
3
  PyJWT>=1.7.1
4
4
  pathlib2>=2.3.5
5
- requests>=2.25.0
5
+ requests>=2.25.0
6
+ aepp=>0.3.10
7
+ pandas>=2.1.0
@@ -42,7 +42,7 @@ setup(
42
42
  "Topic :: Software Development :: Libraries",
43
43
  "Development Status :: 4 - Beta"
44
44
  ],
45
- python_requires='>=3.7',
45
+ python_requires='>=3.10',
46
46
  install_requires=["requests",
47
- "PyJWT", "pathlib2", "pathlib", "PyJWT[crypto]"],
47
+ "PyJWT", "pathlib2", "pathlib", "PyJWT[crypto]","aepp","pandas"],
48
48
  )
@@ -1 +0,0 @@
1
- __version__ = "0.4.4-3"
File without changes
File without changes
File without changes