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