launchpy 0.5.3__tar.gz → 0.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {launchpy-0.5.3 → launchpy-0.5.4}/PKG-INFO +1 -1
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/cli.md +6 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/releases.md +7 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/synchronizer.md +10 -0
- launchpy-0.5.4/launchpy/__version__.py +1 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/cli/__main__.py +4 -2
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/synchronizer.py +38 -29
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy.egg-info/PKG-INFO +1 -1
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy.egg-info/SOURCES.txt +2 -1
- launchpy-0.5.4/tests/test_synchronizer.py +428 -0
- launchpy-0.5.3/launchpy/__version__.py +0 -1
- {launchpy-0.5.3 → launchpy-0.5.4}/LICENSE +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/README.md +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/admin.md +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/getstarted.md +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/library.md +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/main.md +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/property.md +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/docs/translator.md +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/__init__.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/admin.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/cli/__init__.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/config.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/configs.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/connector.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/launchpy.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/library.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy/property.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy.egg-info/dependency_links.txt +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy.egg-info/entry_points.txt +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy.egg-info/requires.txt +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/launchpy.egg-info/top_level.txt +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/pyproject.toml +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/requirements.txt +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/setup.cfg +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/tests/test_connector.py +0 -0
- {launchpy-0.5.3 → launchpy-0.5.4}/tests/test_proxies.py +0 -0
|
@@ -533,9 +533,12 @@ Arguments:
|
|
|
533
533
|
`-r`, `--regex`: Regex pattern to filter rules by name (partial match, non-case sensitive). This is an optional parameter, if not provided, all rules are synced.
|
|
534
534
|
`-c`, `--create` : Flag. Create rules that do not exist in the destination property. Default False.
|
|
535
535
|
`-p`, `--published` : Flag. Sync the latest published version of the rules. Default False.
|
|
536
|
+
`-i`, `--ignore_disabled` : Optional boolean. Skip disabled source rules. Default False; a bare flag means True. With `-p`, checks the latest published source revision.
|
|
536
537
|
|
|
537
538
|
```bash
|
|
538
539
|
synchronizer:Base_Property_Name> sync_rules -r "Rule Name" -c
|
|
540
|
+
synchronizer:Base_Property_Name> sync_rules -i
|
|
541
|
+
synchronizer:Base_Property_Name> sync_rules --ignore_disabled True -p
|
|
539
542
|
```
|
|
540
543
|
|
|
541
544
|
#### sync_data_elements
|
|
@@ -544,9 +547,12 @@ Arguments:
|
|
|
544
547
|
`-r`, `--regex`: Regex pattern to filter data elements by name (partial match, non-case sensitive). This is an optional parameter, if not provided, all data elements are synced.
|
|
545
548
|
`-c`, `--create` : Flag. Create data elements that do not exist in the destination property. Default False.
|
|
546
549
|
`-p`, `--published` : Flag. Sync the latest published version of the data elements. Default False.
|
|
550
|
+
`-i`, `--ignore_disabled` : Optional boolean. Skip disabled source data elements. Default False; a bare flag means True. With `-p`, checks the latest published source revision.
|
|
547
551
|
|
|
548
552
|
```bash
|
|
549
553
|
synchronizer:Base_Property_Name> sync_data_elements -r "Data Element Name" -c
|
|
554
|
+
synchronizer:Base_Property_Name> sync_data_elements -i
|
|
555
|
+
synchronizer:Base_Property_Name> sync_data_elements --ignore_disabled False
|
|
550
556
|
```
|
|
551
557
|
|
|
552
558
|
#### sync_extensions
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
This page gathered the changes made between version of the launchpy module.\
|
|
4
4
|
This has been started after the 0.3.0 release.\
|
|
5
5
|
|
|
6
|
+
## 0.5.4
|
|
7
|
+
* Fixing enabled state handling for rules, data elements, and extensions during synchronization. Before release, state enablement was respected only during -published synchronization.
|
|
8
|
+
* Ensuring that the verbose flag correctly displays detailed information for all synchronized entities, including those with enabled state changes.
|
|
9
|
+
* Adding the `ignore_disabled` flag to the synchronization commands to allow skipping disabled source entities.
|
|
10
|
+
* Adding `-i` / `--ignore_disabled` flag to the CLI synchronization commands.
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
## 0.5.3
|
|
7
14
|
* Adding optional `proxy` support to `configure`, `importConfigFile`, `createConfigFile` and JSON configuration files. A single proxy URL is used for both HTTP and HTTPS, with separate settings per organization.
|
|
8
15
|
* Applying proxy settings to OAuth token acquisition and refresh, all API requests and their retries, and asynchronous rule-component downloads during `extractProperty`.
|
|
@@ -150,6 +150,7 @@ synchronizor.syncComponent('my Component Name',publishedVersion=True)
|
|
|
150
150
|
#### Data Elements
|
|
151
151
|
The method will replace the existing configuration of a data element by the template configuration.\
|
|
152
152
|
Completely overriding the existing element configuration in the target properties.
|
|
153
|
+
When a data element is created, its enabled state is updated if needed to match the selected source revision. The response from that update is retained in the target cache and library stack.
|
|
153
154
|
|
|
154
155
|
#### Rules
|
|
155
156
|
The method will delete all rule components existing in the Target property for that rule.\
|
|
@@ -198,6 +199,7 @@ Arguments:
|
|
|
198
199
|
* regex : OPTIONAL : If you want to filter the data elements to sync based on a regex pattern. Default: None (sync all data elements)
|
|
199
200
|
* forceCreation : OPTIONAL : If set to True, it will sync the data elements even if they do not exist in the target properties. If set to False, it will only sync the data elements that already exist in the target properties. Default: False.
|
|
200
201
|
* publishedVersion : OPTIONAL : If set to True, it will sync the latest published version of the data elements. Default: False.
|
|
202
|
+
* ignoreDisabled : OPTIONAL : If set to True, skip data elements whose selected source revision has `enabled=False`. Default: False.
|
|
201
203
|
|
|
202
204
|
## SyncRules
|
|
203
205
|
It will synchronize the rules in the base property to the target properties.
|
|
@@ -205,7 +207,14 @@ Arguments:
|
|
|
205
207
|
* regex : OPTIONAL : If you want to filter the rules to sync based on a regex pattern. Default: None (sync all rules)
|
|
206
208
|
* forceCreation : OPTIONAL : If set to True, it will sync the rules even if they do not exist in the target properties. If set to False, it will only sync the rules that already exist in the target properties. Default: False.
|
|
207
209
|
* publishedVersion : OPTIONAL : If set to True, it will sync the latest published version of the rules. Default: False.
|
|
210
|
+
* ignoreDisabled : OPTIONAL : If set to True, skip rules whose selected source revision has `enabled=False`. Default: False.
|
|
208
211
|
|
|
212
|
+
For both methods, `ignoreDisabled=True` checks the source revision being synchronized, not the target's enabled state. With `publishedVersion=True`, it checks the latest published source revision rather than the current draft. Disabled components are not created or updated, even with `forceCreation=True`, and retain an empty per-target dictionary in the returned results.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
synchronizor.syncRules(ignoreDisabled=True)
|
|
216
|
+
synchronizor.syncDataElements(ignoreDisabled=True, publishedVersion=True)
|
|
217
|
+
```
|
|
209
218
|
|
|
210
219
|
## createTargetsLibrary
|
|
211
220
|
|
|
@@ -238,6 +247,7 @@ Check if the component,from the base property, is synced to the different target
|
|
|
238
247
|
It can also check for the Extensions.\
|
|
239
248
|
It will return a dictionary with the key being the target property and the value being the result of the evaluation, such as {targetProperty: 'similar'} when component have same settings or {targetPropery : '<difference>'} when component do not have same settings.\
|
|
240
249
|
See below for differences.\
|
|
250
|
+
For rules and data elements, enabled-state differences are checked in both current and published modes. A matching configuration does not override an enabled-state difference with `"Similar"`. When a data element has multiple differences, they are returned together as a comma-separated message. Library dry runs use the same comparison logic.
|
|
241
251
|
Arguments:
|
|
242
252
|
* componentName : REQUIRED : the name of the component to compare
|
|
243
253
|
* componentID : REQUIRED : the id of the component to compare
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.4"
|
|
@@ -595,10 +595,11 @@ class SynchronizerCLI(cmd.Cmd):
|
|
|
595
595
|
parser.add_argument("-r", "--regex", help="Regex pattern to filter rules by name (partial match, non-case sensitive). Default None.", type=str, default=None)
|
|
596
596
|
parser.add_argument("-c", "--create", help="Create rules that do not exist in the destination property. Default False.", action="store_true", default=False)
|
|
597
597
|
parser.add_argument("-p", "--published", help="Sync the latest published version of the rules. Default False.", action="store_true", default=False)
|
|
598
|
+
parser.add_argument("-i", "--ignore_disabled", help="Skip disabled source rules. Default False. A bare flag means True.", type=str2bool, nargs="?", const=True, default=False)
|
|
598
599
|
parser.add_argument("-v", "--verbose", help="Print detailed information about the sync process, including successes. Default False.", action="store_true", default=False)
|
|
599
600
|
try:
|
|
600
601
|
args = parser.parse_args(shlex.split(args))
|
|
601
|
-
result = self.synchronizer.syncRules(regex=args.regex, forceCreation=args.create, publishedVersion=args.published)
|
|
602
|
+
result = self.synchronizer.syncRules(regex=args.regex, forceCreation=args.create, publishedVersion=args.published, ignoreDisabled=args.ignore_disabled)
|
|
602
603
|
for ruleName, targetResults in result.items():
|
|
603
604
|
for prop, message in (targetResults or {}).items():
|
|
604
605
|
if "does not exist" in message:
|
|
@@ -617,10 +618,11 @@ class SynchronizerCLI(cmd.Cmd):
|
|
|
617
618
|
parser.add_argument("-r", "--regex", help="Regex pattern to filter data elements by name (partial match, non-case sensitive). Default None.", type=str, default=None)
|
|
618
619
|
parser.add_argument("-c", "--create", help="Create data elements that do not exist in the destination property. Default False.", action="store_true", default=False)
|
|
619
620
|
parser.add_argument("-p", "--published", help="Sync the latest published version of the data elements. Default False.", action="store_true", default=False)
|
|
621
|
+
parser.add_argument("-i", "--ignore_disabled", help="Skip disabled source data elements. Default False. A bare flag means True.", type=str2bool, nargs="?", const=True, default=False)
|
|
620
622
|
parser.add_argument("-v", "--verbose", help="Print detailed information about the sync process, including successes. Default False.", action="store_true", default=False)
|
|
621
623
|
try:
|
|
622
624
|
args = parser.parse_args(shlex.split(args))
|
|
623
|
-
result = self.synchronizer.syncDataElements(regex=args.regex, forceCreation=args.create, publishedVersion=args.published)
|
|
625
|
+
result = self.synchronizer.syncDataElements(regex=args.regex, forceCreation=args.create, publishedVersion=args.published, ignoreDisabled=args.ignore_disabled)
|
|
624
626
|
for deName, targetResults in result.items():
|
|
625
627
|
for prop, message in (targetResults or {}).items():
|
|
626
628
|
if "does not exist" in message:
|
|
@@ -263,6 +263,7 @@ class Synchronizer:
|
|
|
263
263
|
timeout : OPTIONAL : The timeout to be used for the rule component. If not provided, the existing timeout will be used.
|
|
264
264
|
libraryLinked : OPTIONAL : If you want to take a component ID that are associated to a specific library. Default: False. If set to True, the method will look for the component in the library and take the version linked to the library instead of the latest version in the property. Do not work for Extensions.
|
|
265
265
|
verbose : OPTIONAL : If set to True, it will print the synchronization status of each property. Default: False.
|
|
266
|
+
ignoreDisabled : OPTIONAL : Skip disabled source rules and data elements, returning an empty dictionary. Uses the selected revision. Default: False.
|
|
266
267
|
"""
|
|
267
268
|
timeout = kwargs.get('timeout',None)
|
|
268
269
|
libraryLinked = kwargs.get('libraryLinked',False)
|
|
@@ -271,6 +272,10 @@ class Synchronizer:
|
|
|
271
272
|
raise ValueError('Require a component Name of a component ID')
|
|
272
273
|
response = {}
|
|
273
274
|
cmp_baseDict = self.__prepareBaseComponent__(componentName=componentName,componentId=componentId,publishedVersion=publishedVersion,libraryLinked=libraryLinked)
|
|
275
|
+
if (kwargs.get('ignoreDisabled',False)
|
|
276
|
+
and cmp_baseDict['component']['type'] in ('rules','data_elements')
|
|
277
|
+
and not cmp_baseDict['component']['attributes']['enabled']):
|
|
278
|
+
return response
|
|
274
279
|
## handling the data element
|
|
275
280
|
if cmp_baseDict['component']['type'] == 'data_elements':
|
|
276
281
|
for target in list(self.targets.keys()):
|
|
@@ -288,6 +293,16 @@ class Synchronizer:
|
|
|
288
293
|
if len(self.target_configs.get(target,{}).get('inclComponents',[]))==0 or flagAllowList:
|
|
289
294
|
with self._target_locks[target]:
|
|
290
295
|
translatedComponent = self.translator.translate(target,data_element=cmp_baseDict['copy'])
|
|
296
|
+
attributes = {
|
|
297
|
+
"name" : translatedComponent['name'],
|
|
298
|
+
"enabled" : translatedComponent["enabled"],
|
|
299
|
+
"delegate_descriptor_id" : translatedComponent["descriptor"],
|
|
300
|
+
"storage_duration" : translatedComponent["storage_duration"],
|
|
301
|
+
"force_lower_case" : translatedComponent["force_lower_case"],
|
|
302
|
+
"clean_text" : translatedComponent["clean_text"],
|
|
303
|
+
"settings" : translatedComponent["settings"],
|
|
304
|
+
"default_value": translatedComponent["default_value"]
|
|
305
|
+
}
|
|
291
306
|
## if it does not exist
|
|
292
307
|
if cmp_baseDict['name'] not in [de.get('attributes',{}).get('name') for de in self.targets[target]['dataElements']]:
|
|
293
308
|
if forceCreation:
|
|
@@ -304,9 +319,9 @@ class Synchronizer:
|
|
|
304
319
|
default_value= translatedComponent["default_value"]
|
|
305
320
|
)
|
|
306
321
|
if cmp_baseDict['component']['attributes']['enabled'] != comp['attributes']['enabled']:
|
|
307
|
-
|
|
322
|
+
comp = self.targets[target]['api'].updateDataElement(
|
|
308
323
|
dataElement_id=comp['id'],
|
|
309
|
-
attr_dict=
|
|
324
|
+
attr_dict=attributes)
|
|
310
325
|
self.targets[target]['libraryStack']['dataElements'].append(comp)
|
|
311
326
|
self.targets[target]['dataElements'].append(comp)
|
|
312
327
|
response[target] = f'Data element "{cmp_baseDict["name"]}" created in "{target}".'
|
|
@@ -316,16 +331,6 @@ class Synchronizer:
|
|
|
316
331
|
response[target] = message
|
|
317
332
|
else:
|
|
318
333
|
index,old_component = [(index,de) for index,de in enumerate(self.targets[target]['dataElements']) if de.get('attributes',{}).get('name') == cmp_baseDict['name']][0]
|
|
319
|
-
attributes = {
|
|
320
|
-
"name" : translatedComponent['name'],
|
|
321
|
-
"enabled" : translatedComponent["enabled"],
|
|
322
|
-
"delegate_descriptor_id" : translatedComponent["descriptor"],
|
|
323
|
-
"storage_duration" : translatedComponent["storage_duration"],
|
|
324
|
-
"force_lower_case" : translatedComponent["force_lower_case"],
|
|
325
|
-
"clean_text" : translatedComponent["clean_text"],
|
|
326
|
-
"settings" : translatedComponent["settings"],
|
|
327
|
-
"default_value": translatedComponent["default_value"]
|
|
328
|
-
}
|
|
329
334
|
comp = self.targets[target]['api'].updateDataElement(
|
|
330
335
|
dataElement_id=old_component['id'],
|
|
331
336
|
attr_dict=attributes,
|
|
@@ -513,7 +518,7 @@ class Synchronizer:
|
|
|
513
518
|
results[component] = self.syncComponent(componentId=component,publishedVersion=publishedVersion,forceCreation=forceCreation)
|
|
514
519
|
return results
|
|
515
520
|
|
|
516
|
-
def syncRules(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False)->dict:
|
|
521
|
+
def syncRules(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False,ignoreDisabled:bool=False)->dict:
|
|
517
522
|
"""
|
|
518
523
|
Synchronizing the rules in the base property to the target properties.
|
|
519
524
|
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).
|
|
@@ -521,18 +526,19 @@ class Synchronizer:
|
|
|
521
526
|
regex : OPTIONAL : If you want to filter the rules to sync based on a regex pattern. Default: None (sync all rules)
|
|
522
527
|
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.
|
|
523
528
|
publishedVersion : OPTIONAL : If set to True, it will sync the latest published version of the rules. Default: False.
|
|
529
|
+
ignoreDisabled : OPTIONAL : Skip rules whose selected source revision is disabled. Skipped rules have an empty per-target result. Default: False.
|
|
524
530
|
"""
|
|
525
531
|
base_rule_names = [rule['attributes']['name'] for rule in self.base['rules']]
|
|
526
532
|
if regex is not None:
|
|
527
533
|
base_rule_names = [rule for rule in base_rule_names if re.search(regex,rule)]
|
|
528
534
|
results = {}
|
|
529
535
|
with ThreadPoolExecutor() as executor:
|
|
530
|
-
future_to_name = {executor.submit(self.syncComponent, componentName=rule, forceCreation=forceCreation, publishedVersion=publishedVersion): rule for rule in base_rule_names}
|
|
536
|
+
future_to_name = {executor.submit(self.syncComponent, componentName=rule, forceCreation=forceCreation, publishedVersion=publishedVersion, ignoreDisabled=ignoreDisabled): rule for rule in base_rule_names}
|
|
531
537
|
for future in future_to_name:
|
|
532
538
|
results[future_to_name[future]] = future.result()
|
|
533
539
|
return results
|
|
534
540
|
|
|
535
|
-
def syncDataElements(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False)->dict:
|
|
541
|
+
def syncDataElements(self,regex:str=None,forceCreation:bool=False,publishedVersion:bool=False,ignoreDisabled:bool=False)->dict:
|
|
536
542
|
"""
|
|
537
543
|
Synchronizing the data elements in the base property to the target properties.
|
|
538
544
|
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).
|
|
@@ -540,13 +546,14 @@ class Synchronizer:
|
|
|
540
546
|
regex : OPTIONAL : If you want to filter the data elements to sync based on a regex pattern. Default: None (sync all data elements)
|
|
541
547
|
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.
|
|
542
548
|
publishedVersion : OPTIONAL : If set to True, it will sync the latest published version of the data elements. Default: False.
|
|
549
|
+
ignoreDisabled : OPTIONAL : Skip data elements whose selected source revision is disabled. Skipped data elements have an empty per-target result. Default: False.
|
|
543
550
|
"""
|
|
544
551
|
base_de_names = [de['attributes']['name'] for de in self.base['dataElements']]
|
|
545
552
|
if regex is not None:
|
|
546
553
|
base_de_names = [de for de in base_de_names if re.search(regex,de)]
|
|
547
554
|
results = {}
|
|
548
555
|
with ThreadPoolExecutor() as executor:
|
|
549
|
-
future_to_name = {executor.submit(self.syncComponent, componentName=de, forceCreation=forceCreation, publishedVersion=publishedVersion): de for de in base_de_names}
|
|
556
|
+
future_to_name = {executor.submit(self.syncComponent, componentName=de, forceCreation=forceCreation, publishedVersion=publishedVersion, ignoreDisabled=ignoreDisabled): de for de in base_de_names}
|
|
550
557
|
for future in future_to_name:
|
|
551
558
|
results[future_to_name[future]] = future.result()
|
|
552
559
|
return results
|
|
@@ -793,21 +800,23 @@ class Synchronizer:
|
|
|
793
800
|
else:
|
|
794
801
|
index,target_de = [(index,de) for index,de in enumerate(self.targets[target]['dataElements']) if de['attributes']['name'] == cmp_baseDict['name']][0]
|
|
795
802
|
issue_pub = ""
|
|
803
|
+
differences: list[str] = []
|
|
796
804
|
if publishedVersion:
|
|
797
805
|
try:
|
|
798
806
|
revisions_dataElement = self.targets[target]["api"].getRevisions(target_de)
|
|
799
807
|
target_de = self.targets[target]["api"].getLatestPublishedVersion(revisions_dataElement)
|
|
800
808
|
except:
|
|
801
809
|
issue_pub = " (draft)"
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if target_de['attributes']['settings']
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
810
|
+
if target_de['attributes']['enabled'] != cmp_baseDict['component']['attributes']['enabled']:
|
|
811
|
+
differences.append("Data Element enabled status is different")
|
|
812
|
+
if publishedVersion and target_de['attributes']['published'] != cmp_baseDict['component']['attributes']['published']:
|
|
813
|
+
differences.append("Data Element published status is different")
|
|
814
|
+
if target_de['attributes']['settings'] != cmp_baseDict['component']['attributes']['settings']:
|
|
815
|
+
differences.append("Data Element settings are different")
|
|
816
|
+
if differences:
|
|
817
|
+
dict_result[target] = ','.join(differences) + issue_pub
|
|
818
|
+
elif not excludeSimilar:
|
|
819
|
+
dict_result[target] = "Similar"+issue_pub
|
|
811
820
|
if cmp_baseDict['component']['type'] == 'rules':
|
|
812
821
|
rcsLink = cmp_baseDict['component'].get('relationships',{}).get('rule_components',{}).get('links',{}).get('related')
|
|
813
822
|
resResource = self.base['api'].getRessource(rcsLink)
|
|
@@ -829,10 +838,10 @@ class Synchronizer:
|
|
|
829
838
|
targetRule = self.targets[target]["api"].getLatestPublishedVersion(revisions_targetRule)
|
|
830
839
|
except:
|
|
831
840
|
issue_pub = " (not published)"
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
841
|
+
if targetRule['attributes']['enabled'] != cmp_baseDict['component']['attributes']['enabled']:
|
|
842
|
+
componentsDifferences.append("Rule enabled status is different")
|
|
843
|
+
if publishedVersion and targetRule['attributes']['published'] != cmp_baseDict['component']['attributes']['published']:
|
|
844
|
+
componentsDifferences.append("Rule published status is different")
|
|
836
845
|
rcsLinkTarget = targetRule.get('relationships',{}).get('rule_components',{}).get('links',{}).get('related')
|
|
837
846
|
resResource = self.targets[target]['api'].getRessource(rcsLinkTarget)
|
|
838
847
|
target_rule_components:list = resResource['data']
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
import threading
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
from launchpy.cli.__main__ import SynchronizerCLI
|
|
7
|
+
from launchpy.launchpy import Translator
|
|
8
|
+
from launchpy.property import Property
|
|
9
|
+
from launchpy.synchronizer import Synchronizer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def component(kind: str, identifier: str, enabled: bool) -> dict:
|
|
13
|
+
data = {
|
|
14
|
+
"id": identifier,
|
|
15
|
+
"type": kind,
|
|
16
|
+
"attributes": {
|
|
17
|
+
"name": "Sample component",
|
|
18
|
+
"enabled": enabled,
|
|
19
|
+
"published": False,
|
|
20
|
+
"revision_number": 2,
|
|
21
|
+
},
|
|
22
|
+
"links": {
|
|
23
|
+
"self": "https://example.invalid/" + identifier,
|
|
24
|
+
"origin": "https://example.invalid/" + identifier,
|
|
25
|
+
},
|
|
26
|
+
"relationships": {
|
|
27
|
+
"rule_components": {"links": {"related": "mock:components"}},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
if kind == "data_elements":
|
|
31
|
+
data["attributes"].update(
|
|
32
|
+
settings="{}",
|
|
33
|
+
delegate_descriptor_id="core::dataElements::custom-code",
|
|
34
|
+
storage_duration="pageview",
|
|
35
|
+
force_lower_case=False,
|
|
36
|
+
default_value="",
|
|
37
|
+
clean_text=False,
|
|
38
|
+
)
|
|
39
|
+
data["relationships"]["extension"] = {
|
|
40
|
+
"data": {"id": "EXbase", "type": "extensions"},
|
|
41
|
+
}
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SynchronizerTests(unittest.TestCase):
|
|
46
|
+
def make_sync(
|
|
47
|
+
self, kind: str, source_enabled: bool = False, target_enabled: bool = True
|
|
48
|
+
) -> tuple[Synchronizer, dict, dict]:
|
|
49
|
+
prefix = "RL" if kind == "rules" else "DE"
|
|
50
|
+
source = component(kind, prefix + "base", source_enabled)
|
|
51
|
+
target = component(kind, prefix + "target", target_enabled)
|
|
52
|
+
key = "rules" if kind == "rules" else "dataElements"
|
|
53
|
+
source_api = Mock(spec=Property)
|
|
54
|
+
source_api.connector = Mock()
|
|
55
|
+
target_api = Mock(spec=Property)
|
|
56
|
+
source_api.getRessource.return_value = {"data": []}
|
|
57
|
+
target_api.getRessource.return_value = {"data": []}
|
|
58
|
+
source_api.getRevisions.return_value = [source]
|
|
59
|
+
target_api.getRevisions.return_value = [target]
|
|
60
|
+
source_api.getLatestPublishedVersion.return_value = source
|
|
61
|
+
target_api.getLatestPublishedVersion.return_value = target
|
|
62
|
+
target_api.updateDataElement.side_effect = (
|
|
63
|
+
lambda dataElement_id, attr_dict: self.updated(target, attr_dict)
|
|
64
|
+
)
|
|
65
|
+
target_api.updateRule.side_effect = (
|
|
66
|
+
lambda rule_id, attr_dict: self.updated(target, attr_dict)
|
|
67
|
+
)
|
|
68
|
+
sync = Synchronizer.__new__(Synchronizer)
|
|
69
|
+
sync.base = {
|
|
70
|
+
"name": "Base",
|
|
71
|
+
"api": source_api,
|
|
72
|
+
"rules": [],
|
|
73
|
+
"dataElements": [],
|
|
74
|
+
"extensions": [],
|
|
75
|
+
}
|
|
76
|
+
sync.base[key] = [source]
|
|
77
|
+
sync.targets = {
|
|
78
|
+
"Target": {
|
|
79
|
+
"api": target_api,
|
|
80
|
+
"rules": [],
|
|
81
|
+
"dataElements": [],
|
|
82
|
+
"libraryStack": {"rules": [], "dataElements": [], "extensions": []},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
sync.targets["Target"][key] = [target]
|
|
86
|
+
sync.target_configs = {}
|
|
87
|
+
sync._base_lock = threading.Lock()
|
|
88
|
+
sync._target_locks = {"Target": threading.Lock()}
|
|
89
|
+
sync.translator = Translator()
|
|
90
|
+
sync.translator.setBaseExtensions(
|
|
91
|
+
[{"id": "EXbase", "attributes": {"name": "core"}}], "Base"
|
|
92
|
+
)
|
|
93
|
+
sync.translator.extendExtensions(
|
|
94
|
+
[{"id": "EXtarget", "attributes": {"name": "core"}}], "Target"
|
|
95
|
+
)
|
|
96
|
+
sync.translator.setBaseRules(sync.base["rules"], "Base")
|
|
97
|
+
sync.translator.extendRules(sync.targets["Target"]["rules"], "Target")
|
|
98
|
+
return sync, source, target
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def updated(target: dict, attributes: dict) -> dict:
|
|
102
|
+
result = deepcopy(target)
|
|
103
|
+
result["attributes"].update(attributes)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
def test_created_data_element_update_uses_api_attributes_and_tracks_response(self):
|
|
107
|
+
for source_enabled in (False, True):
|
|
108
|
+
with self.subTest(source_enabled=source_enabled):
|
|
109
|
+
sync, source, created = self.make_sync(
|
|
110
|
+
"data_elements", source_enabled, not source_enabled
|
|
111
|
+
)
|
|
112
|
+
target = sync.targets["Target"]
|
|
113
|
+
target["dataElements"] = []
|
|
114
|
+
api = target["api"]
|
|
115
|
+
api.createDataElement.return_value = created
|
|
116
|
+
attributes = {
|
|
117
|
+
"name": source["attributes"]["name"],
|
|
118
|
+
"enabled": source_enabled,
|
|
119
|
+
"delegate_descriptor_id": source["attributes"]["delegate_descriptor_id"],
|
|
120
|
+
"storage_duration": source["attributes"]["storage_duration"],
|
|
121
|
+
"force_lower_case": source["attributes"]["force_lower_case"],
|
|
122
|
+
"clean_text": source["attributes"]["clean_text"],
|
|
123
|
+
"settings": source["attributes"]["settings"],
|
|
124
|
+
"default_value": source["attributes"]["default_value"],
|
|
125
|
+
}
|
|
126
|
+
updated = self.updated(created, attributes)
|
|
127
|
+
updated["attributes"]["revision_number"] += 1
|
|
128
|
+
api.updateDataElement.side_effect = None
|
|
129
|
+
api.updateDataElement.return_value = updated
|
|
130
|
+
|
|
131
|
+
result = sync.syncComponent(componentId=source["id"], forceCreation=True)
|
|
132
|
+
|
|
133
|
+
api.updateDataElement.assert_called_once_with(
|
|
134
|
+
dataElement_id=created["id"], attr_dict=attributes
|
|
135
|
+
)
|
|
136
|
+
self.assertEqual(
|
|
137
|
+
result,
|
|
138
|
+
{"Target": 'Data element "Sample component" created in "Target".'},
|
|
139
|
+
)
|
|
140
|
+
self.assertEqual(len(target["dataElements"]), 1)
|
|
141
|
+
self.assertEqual(len(target["libraryStack"]["dataElements"]), 1)
|
|
142
|
+
self.assertIs(target["dataElements"][0], updated)
|
|
143
|
+
self.assertIs(target["libraryStack"]["dataElements"][0], updated)
|
|
144
|
+
self.assertIs(created["attributes"]["enabled"], not source_enabled)
|
|
145
|
+
self.assertEqual(
|
|
146
|
+
sync.checkComponentSync(componentId=source["id"])["Target"], "Similar"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def test_created_data_element_needs_no_update_when_enabled_state_matches(self):
|
|
150
|
+
for enabled in (False, True):
|
|
151
|
+
with self.subTest(enabled=enabled):
|
|
152
|
+
sync, source, created = self.make_sync("data_elements", enabled, enabled)
|
|
153
|
+
target = sync.targets["Target"]
|
|
154
|
+
target["dataElements"] = []
|
|
155
|
+
api = target["api"]
|
|
156
|
+
api.createDataElement.return_value = created
|
|
157
|
+
|
|
158
|
+
sync.syncComponent(componentId=source["id"], forceCreation=True)
|
|
159
|
+
|
|
160
|
+
api.updateDataElement.assert_not_called()
|
|
161
|
+
self.assertEqual(target["dataElements"], [created])
|
|
162
|
+
self.assertEqual(target["libraryStack"]["dataElements"], [created])
|
|
163
|
+
|
|
164
|
+
def test_created_data_element_update_failure_is_not_cached_as_success(self):
|
|
165
|
+
sync, source, created = self.make_sync("data_elements")
|
|
166
|
+
target = sync.targets["Target"]
|
|
167
|
+
target["dataElements"] = []
|
|
168
|
+
api = target["api"]
|
|
169
|
+
api.createDataElement.return_value = created
|
|
170
|
+
api.updateDataElement.side_effect = RuntimeError("Data element update failed")
|
|
171
|
+
|
|
172
|
+
with self.assertRaisesRegex(RuntimeError, "Data element update failed"):
|
|
173
|
+
sync.syncComponent(componentId=source["id"], forceCreation=True)
|
|
174
|
+
|
|
175
|
+
self.assertEqual(target["dataElements"], [])
|
|
176
|
+
self.assertEqual(target["libraryStack"]["dataElements"], [])
|
|
177
|
+
|
|
178
|
+
def test_enabled_mismatches_are_reported_in_all_comparison_modes(self):
|
|
179
|
+
for kind, label in (("rules", "Rule"), ("data_elements", "Data Element")):
|
|
180
|
+
for enabled in (False, True):
|
|
181
|
+
for published in (False, True):
|
|
182
|
+
for exclude_similar in (False, True):
|
|
183
|
+
with self.subTest(
|
|
184
|
+
kind=kind, enabled=enabled, published=published,
|
|
185
|
+
exclude_similar=exclude_similar,
|
|
186
|
+
):
|
|
187
|
+
sync, source, target = self.make_sync(
|
|
188
|
+
kind, enabled, not enabled
|
|
189
|
+
)
|
|
190
|
+
result = sync.checkComponentSync(
|
|
191
|
+
componentId=source["id"],
|
|
192
|
+
publishedVersion=published,
|
|
193
|
+
excludeSimilar=exclude_similar,
|
|
194
|
+
)
|
|
195
|
+
self.assertEqual(
|
|
196
|
+
result["Target"], f"{label} enabled status is different"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def test_matching_components_preserve_similar_and_exclude_similar_results(self):
|
|
200
|
+
for kind in ("rules", "data_elements"):
|
|
201
|
+
for published in (False, True):
|
|
202
|
+
for exclude_similar in (False, True):
|
|
203
|
+
with self.subTest(
|
|
204
|
+
kind=kind, published=published, exclude_similar=exclude_similar,
|
|
205
|
+
):
|
|
206
|
+
sync, source, target = self.make_sync(kind, False, False)
|
|
207
|
+
result = sync.checkComponentSync(
|
|
208
|
+
componentId=source["id"],
|
|
209
|
+
publishedVersion=published,
|
|
210
|
+
excludeSimilar=exclude_similar,
|
|
211
|
+
)
|
|
212
|
+
self.assertEqual(result["Target"], "" if exclude_similar else "Similar")
|
|
213
|
+
|
|
214
|
+
def test_data_element_comparison_retains_all_differences(self):
|
|
215
|
+
sync, source, target = self.make_sync("data_elements")
|
|
216
|
+
source["attributes"]["published"] = True
|
|
217
|
+
target["attributes"]["settings"] = '{"different": true}'
|
|
218
|
+
result = sync.checkComponentSync(componentId=source["id"], publishedVersion=True)
|
|
219
|
+
self.assertEqual(
|
|
220
|
+
result["Target"],
|
|
221
|
+
"Data Element enabled status is different,"
|
|
222
|
+
"Data Element published status is different,"
|
|
223
|
+
"Data Element settings are different",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def test_data_element_published_difference_is_not_overwritten_by_similar_settings(self):
|
|
227
|
+
sync, source, target = self.make_sync("data_elements", False, False)
|
|
228
|
+
source["attributes"]["published"] = True
|
|
229
|
+
result = sync.checkComponentSync(componentId=source["id"], publishedVersion=True)
|
|
230
|
+
self.assertEqual(result["Target"], "Data Element published status is different")
|
|
231
|
+
|
|
232
|
+
def test_comparison_reports_enabled_difference_when_target_has_no_published_revision(self):
|
|
233
|
+
for kind, label, suffix in (
|
|
234
|
+
("rules", "Rule", " (not published)"),
|
|
235
|
+
("data_elements", "Data Element", " (draft)"),
|
|
236
|
+
):
|
|
237
|
+
with self.subTest(kind=kind):
|
|
238
|
+
sync, source, target = self.make_sync(kind)
|
|
239
|
+
sync.targets["Target"]["api"].getLatestPublishedVersion.side_effect = (
|
|
240
|
+
IndexError("No published revision")
|
|
241
|
+
)
|
|
242
|
+
result = sync.checkComponentSync(
|
|
243
|
+
componentId=source["id"], publishedVersion=True
|
|
244
|
+
)
|
|
245
|
+
self.assertEqual(
|
|
246
|
+
result["Target"], f"{label} enabled status is different{suffix}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def test_library_dry_run_reports_enabled_differences(self):
|
|
250
|
+
for kind, label in (("rules", "Rule"), ("data_elements", "Data Element")):
|
|
251
|
+
for published in (False, True):
|
|
252
|
+
with self.subTest(kind=kind, published=published):
|
|
253
|
+
sync, source, target = self.make_sync(kind)
|
|
254
|
+
api = sync.base["api"]
|
|
255
|
+
api.getLibraries.return_value = [
|
|
256
|
+
{"id": "LBbase", "attributes": {"name": "Base library"}},
|
|
257
|
+
]
|
|
258
|
+
api.getRule.return_value = source
|
|
259
|
+
api.getDataElement.return_value = source
|
|
260
|
+
library = Mock()
|
|
261
|
+
library.getFullLibrary.return_value = {
|
|
262
|
+
"rules": [source] if kind == "rules" else [],
|
|
263
|
+
"data_elements": [source] if kind == "data_elements" else [],
|
|
264
|
+
"extensions": [],
|
|
265
|
+
}
|
|
266
|
+
with patch("launchpy.synchronizer.Library", return_value=library):
|
|
267
|
+
result = sync.syncFromLibrary(
|
|
268
|
+
library="LBbase", dryRun=True, publishedVersion=published
|
|
269
|
+
)
|
|
270
|
+
self.assertEqual(
|
|
271
|
+
result[source["attributes"]["name"]]["Target"],
|
|
272
|
+
f"{label} enabled status is different",
|
|
273
|
+
)
|
|
274
|
+
sync.targets["Target"]["api"].updateRule.assert_not_called()
|
|
275
|
+
sync.targets["Target"]["api"].updateDataElement.assert_not_called()
|
|
276
|
+
|
|
277
|
+
def test_bulk_sync_defaults_to_including_disabled_components(self):
|
|
278
|
+
for kind in ("rules", "data_elements"):
|
|
279
|
+
for options in ({}, {"ignoreDisabled": False}):
|
|
280
|
+
with self.subTest(kind=kind, options=options):
|
|
281
|
+
sync, source, target = self.make_sync(kind)
|
|
282
|
+
method = sync.syncRules if kind == "rules" else sync.syncDataElements
|
|
283
|
+
result = method(**options)
|
|
284
|
+
self.assertIn("Target", result[source["attributes"]["name"]])
|
|
285
|
+
update = (
|
|
286
|
+
sync.targets["Target"]["api"].updateRule
|
|
287
|
+
if kind == "rules"
|
|
288
|
+
else sync.targets["Target"]["api"].updateDataElement
|
|
289
|
+
)
|
|
290
|
+
self.assertIs(update.call_args.kwargs["attr_dict"]["enabled"], False)
|
|
291
|
+
|
|
292
|
+
def test_bulk_sync_ignores_disabled_components_without_touching_targets(self):
|
|
293
|
+
for kind in ("rules", "data_elements"):
|
|
294
|
+
for force_creation in (False, True):
|
|
295
|
+
for target_exists in (False, True):
|
|
296
|
+
with self.subTest(
|
|
297
|
+
kind=kind, force_creation=force_creation,
|
|
298
|
+
target_exists=target_exists,
|
|
299
|
+
):
|
|
300
|
+
sync, source, target = self.make_sync(kind)
|
|
301
|
+
if not target_exists:
|
|
302
|
+
key = "rules" if kind == "rules" else "dataElements"
|
|
303
|
+
sync.targets["Target"][key] = []
|
|
304
|
+
method = sync.syncRules if kind == "rules" else sync.syncDataElements
|
|
305
|
+
result = method(ignoreDisabled=True, forceCreation=force_creation)
|
|
306
|
+
self.assertEqual(result, {source["attributes"]["name"]: {}})
|
|
307
|
+
self.assertEqual(sync.targets["Target"]["api"].mock_calls, [])
|
|
308
|
+
self.assertEqual(sync.base["api"].mock_calls, [])
|
|
309
|
+
self.assertEqual(
|
|
310
|
+
sync.targets["Target"]["libraryStack"],
|
|
311
|
+
{"rules": [], "dataElements": [], "extensions": []},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def test_bulk_sync_only_updates_enabled_components_in_a_mixed_batch(self):
|
|
315
|
+
for kind in ("rules", "data_elements"):
|
|
316
|
+
with self.subTest(kind=kind):
|
|
317
|
+
sync, source, target = self.make_sync(kind)
|
|
318
|
+
key = "rules" if kind == "rules" else "dataElements"
|
|
319
|
+
enabled_source = deepcopy(source)
|
|
320
|
+
enabled_source["id"] += "enabled"
|
|
321
|
+
enabled_source["attributes"].update(name="Enabled component", enabled=True)
|
|
322
|
+
enabled_target = deepcopy(target)
|
|
323
|
+
enabled_target["id"] += "enabled"
|
|
324
|
+
enabled_target["attributes"].update(name="Enabled component", enabled=False)
|
|
325
|
+
sync.base[key].append(enabled_source)
|
|
326
|
+
sync.targets["Target"][key].append(enabled_target)
|
|
327
|
+
api = sync.targets["Target"]["api"]
|
|
328
|
+
update = api.updateRule if kind == "rules" else api.updateDataElement
|
|
329
|
+
update.side_effect = lambda **kwargs: self.updated(
|
|
330
|
+
enabled_target, kwargs["attr_dict"]
|
|
331
|
+
)
|
|
332
|
+
method = sync.syncRules if kind == "rules" else sync.syncDataElements
|
|
333
|
+
result = method(ignoreDisabled=True)
|
|
334
|
+
self.assertEqual(result[source["attributes"]["name"]], {})
|
|
335
|
+
self.assertIn("Target", result["Enabled component"])
|
|
336
|
+
update.assert_called_once()
|
|
337
|
+
self.assertIs(update.call_args.kwargs["attr_dict"]["enabled"], True)
|
|
338
|
+
|
|
339
|
+
def test_bulk_sync_still_syncs_enabled_components_to_disabled_targets(self):
|
|
340
|
+
for kind in ("rules", "data_elements"):
|
|
341
|
+
with self.subTest(kind=kind):
|
|
342
|
+
sync, source, target = self.make_sync(kind, True, False)
|
|
343
|
+
method = sync.syncRules if kind == "rules" else sync.syncDataElements
|
|
344
|
+
result = method(ignoreDisabled=True)
|
|
345
|
+
self.assertIn("Target", result[source["attributes"]["name"]])
|
|
346
|
+
key = "rules" if kind == "rules" else "dataElements"
|
|
347
|
+
self.assertIs(
|
|
348
|
+
sync.targets["Target"][key][0]["attributes"]["enabled"], True
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def test_bulk_sync_filters_the_selected_published_revision(self):
|
|
352
|
+
for kind in ("rules", "data_elements"):
|
|
353
|
+
for published_enabled in (False, True):
|
|
354
|
+
with self.subTest(kind=kind, published_enabled=published_enabled):
|
|
355
|
+
sync, source, target = self.make_sync(
|
|
356
|
+
kind, not published_enabled, not published_enabled
|
|
357
|
+
)
|
|
358
|
+
revision = deepcopy(source)
|
|
359
|
+
revision["id"] += "revision"
|
|
360
|
+
revision["attributes"]["enabled"] = published_enabled
|
|
361
|
+
revision["attributes"]["published"] = True
|
|
362
|
+
sync.base["api"].getLatestPublishedVersion.return_value = revision
|
|
363
|
+
method = sync.syncRules if kind == "rules" else sync.syncDataElements
|
|
364
|
+
result = method(ignoreDisabled=True, publishedVersion=True)
|
|
365
|
+
sync.base["api"].getRevisions.assert_called_once_with(source)
|
|
366
|
+
if published_enabled:
|
|
367
|
+
self.assertIn("Target", result[source["attributes"]["name"]])
|
|
368
|
+
else:
|
|
369
|
+
self.assertEqual(result[source["attributes"]["name"]], {})
|
|
370
|
+
self.assertEqual(sync.targets["Target"]["api"].mock_calls, [])
|
|
371
|
+
|
|
372
|
+
def test_bulk_sync_keeps_regex_filtering(self):
|
|
373
|
+
for kind in ("rules", "data_elements"):
|
|
374
|
+
with self.subTest(kind=kind):
|
|
375
|
+
sync, source, target = self.make_sync(kind, True, False)
|
|
376
|
+
method = sync.syncRules if kind == "rules" else sync.syncDataElements
|
|
377
|
+
self.assertEqual(method(regex="Does not match", ignoreDisabled=True), {})
|
|
378
|
+
self.assertEqual(sync.targets["Target"]["api"].mock_calls, [])
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class SynchronizerCLITests(unittest.TestCase):
|
|
382
|
+
def test_ignore_disabled_accepts_optional_boolean_and_bare_flags(self):
|
|
383
|
+
options = (
|
|
384
|
+
("", False),
|
|
385
|
+
("-i", True),
|
|
386
|
+
("--ignore_disabled", True),
|
|
387
|
+
("-i True", True),
|
|
388
|
+
("--ignore_disabled true", True),
|
|
389
|
+
("-i False", False),
|
|
390
|
+
("--ignore_disabled false", False),
|
|
391
|
+
("--ignore_disabled=False", False),
|
|
392
|
+
)
|
|
393
|
+
for command, method_name in (
|
|
394
|
+
("do_sync_rules", "syncRules"),
|
|
395
|
+
("do_sync_data_elements", "syncDataElements"),
|
|
396
|
+
):
|
|
397
|
+
for arguments, expected in options:
|
|
398
|
+
with self.subTest(command=command, arguments=arguments):
|
|
399
|
+
sync = Mock(spec=Synchronizer)
|
|
400
|
+
sync.base = {"name": "Base"}
|
|
401
|
+
method = getattr(sync, method_name)
|
|
402
|
+
method.return_value = {}
|
|
403
|
+
shell = SynchronizerCLI(sync)
|
|
404
|
+
with patch("launchpy.cli.__main__.console") as console:
|
|
405
|
+
getattr(shell, command)(arguments + " -r Sample -c -p")
|
|
406
|
+
method.assert_called_once_with(
|
|
407
|
+
regex="Sample", forceCreation=True, publishedVersion=True,
|
|
408
|
+
ignoreDisabled=expected,
|
|
409
|
+
)
|
|
410
|
+
console.print.assert_not_called()
|
|
411
|
+
|
|
412
|
+
def test_ignore_disabled_rejects_invalid_boolean_without_syncing(self):
|
|
413
|
+
for command, method_name in (
|
|
414
|
+
("do_sync_rules", "syncRules"),
|
|
415
|
+
("do_sync_data_elements", "syncDataElements"),
|
|
416
|
+
):
|
|
417
|
+
with self.subTest(command=command):
|
|
418
|
+
sync = Mock(spec=Synchronizer)
|
|
419
|
+
sync.base = {"name": "Base"}
|
|
420
|
+
shell = SynchronizerCLI(sync)
|
|
421
|
+
with patch("sys.stderr") as stderr:
|
|
422
|
+
getattr(shell, command)("--ignore_disabled invalid")
|
|
423
|
+
getattr(sync, method_name).assert_not_called()
|
|
424
|
+
self.assertTrue(stderr.write.called)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
if __name__ == "__main__":
|
|
428
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|