launchpy 0.4.5.post5__tar.gz → 0.4.6__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.4.5.post5/launchpy.egg-info → launchpy-0.4.6}/PKG-INFO +1 -1
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/releases.md +6 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/synchronizer.md +52 -3
- launchpy-0.4.6/launchpy/__version__.py +1 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/connector.py +6 -6
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/launchpy.py +20 -4
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/library.py +22 -5
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/property.py +12 -6
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/synchronizer.py +23 -12
- {launchpy-0.4.5.post5 → launchpy-0.4.6/launchpy.egg-info}/PKG-INFO +1 -1
- launchpy-0.4.5.post5/launchpy/__version__.py +0 -1
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/LICENSE +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/README.md +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/admin.md +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/getstarted.md +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/library.md +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/main.md +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/property.md +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/docs/translator.md +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/__init__.py +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/admin.py +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/config.py +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy/configs.py +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy.egg-info/SOURCES.txt +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy.egg-info/dependency_links.txt +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy.egg-info/requires.txt +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/launchpy.egg-info/top_level.txt +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/pyproject.toml +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/requirements.txt +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/setup.cfg +0 -0
- {launchpy-0.4.5.post5 → launchpy-0.4.6}/setup.py +0 -0
|
@@ -3,6 +3,9 @@
|
|
|
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.6
|
|
7
|
+
* Allow Synchronizer to be used in multi IMS organization setup.
|
|
8
|
+
|
|
6
9
|
## 0.4.5
|
|
7
10
|
* adding `getExtension` in `Property` class
|
|
8
11
|
* improve `upgradeExtension` in `Property` class
|
|
@@ -15,6 +18,9 @@ Patch:
|
|
|
15
18
|
* Support of extension added in `createTargetsLibrary`
|
|
16
19
|
* Fixing dependency to `aepp` `som` module
|
|
17
20
|
* Fixing renaming path to use `renameComponent` and using `syncComponent`
|
|
21
|
+
* Adding capability to set environment to a lib in `Library` instance via `setEnvironment`
|
|
22
|
+
* Capability to get a `Library` class from the `getLibrary` method in `Property` instance
|
|
23
|
+
* Fixing code in `deleteLibrary`
|
|
18
24
|
|
|
19
25
|
## 0.4.4
|
|
20
26
|
* adding a `getBuilds` method on the `Library` class instance
|
|
@@ -13,7 +13,6 @@ The Synchronizer will allow you to specify a template property and target proper
|
|
|
13
13
|
This class is currently requiring the following elements on your side:
|
|
14
14
|
|
|
15
15
|
* You have imported the config file (via `importConfigFile`) or configure your module via `configure` method.
|
|
16
|
-
* Your template property and target properties are located in the same organization
|
|
17
16
|
* You have unique name for your rule and data elements
|
|
18
17
|
* You have the same extension installed in both the template and target properties.\
|
|
19
18
|
The Synchronizer will not sync the extensions configuration.
|
|
@@ -24,8 +23,8 @@ If a rule and a data element have the same name, by passing that name in the `sy
|
|
|
24
23
|
|
|
25
24
|
In order to instantiate the `Synchronizer` class, you will need to provide 2 elements:
|
|
26
25
|
|
|
27
|
-
* The name of the base property
|
|
28
|
-
* The names of the target properties (as a list of strings)
|
|
26
|
+
* The name of the base property, as `string` or `launchpy.Property` instance
|
|
27
|
+
* The names of the target properties (as a list of `strings`, or `launchpy.Property` instances)
|
|
29
28
|
|
|
30
29
|
Example:
|
|
31
30
|
|
|
@@ -38,6 +37,56 @@ synchronizor = lp.Synchronizer(base='Prop1',targets=['Prop2'])
|
|
|
38
37
|
|
|
39
38
|
```
|
|
40
39
|
|
|
40
|
+
### Multi Organization environments
|
|
41
|
+
|
|
42
|
+
For some clients, they may want to synchronize the rules and data elements across organization setup.\
|
|
43
|
+
That is possible, but the requirements are still needed regarding the following elements:
|
|
44
|
+
* You have unique name for your rule and data elements
|
|
45
|
+
* You have the same extension installed in both the template and target properties.
|
|
46
|
+
|
|
47
|
+
The synchronizer will use the environment credential lastly loaded in the system.\
|
|
48
|
+
We would recommend to load first the target organization that you want to sync to, and then the base organization.\
|
|
49
|
+
You will be able to use the Property instance of the target organization in order to sync the element afterwards.
|
|
50
|
+
|
|
51
|
+
Example of setup
|
|
52
|
+
|
|
53
|
+
```py
|
|
54
|
+
import launchpy as lp
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### targeted org
|
|
58
|
+
lp.importConfigFile('config_target.json')
|
|
59
|
+
admin_target = lp.Admin()
|
|
60
|
+
targetId = admin_target.getCompanyId()
|
|
61
|
+
target_props = admin_target.getProperties(targetId)
|
|
62
|
+
|
|
63
|
+
target_prop = lp.Property(target_props[1]) ## selecting one of the property
|
|
64
|
+
|
|
65
|
+
### Source org for base
|
|
66
|
+
lp.importConfigFile('config_source.json')
|
|
67
|
+
admin = lp.Admin()
|
|
68
|
+
sourceId = admin_target.getCompanyId()
|
|
69
|
+
source_props = admin.getProperties(sourceId)
|
|
70
|
+
|
|
71
|
+
base = lp.Property(source_props[0]) ### selecting the first one as base - it can be any
|
|
72
|
+
|
|
73
|
+
synchronizor = lp.Synchronizer(base=base,targets=['another Property Name to sync',target_prop])
|
|
74
|
+
|
|
75
|
+
### rest is as usual
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If you have private extension that are not released between organization, you can pass a mapping to the synchronizer to tell that these extensions are to be treated as one.\
|
|
79
|
+
You would need to pass the target extension name and the base extension name so that it is mapped as a single one.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```py
|
|
83
|
+
|
|
84
|
+
synchronizor = lp.Synchronizer(base=base,targets=['another Property Name to sync',target_prop],mapping_extensions={'extension_name_target':'extension_name_base'})
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
### Additional options
|
|
41
90
|
There is a possibility to setup some rules so you can filter when migrating from one property to another. See [dynamic Filtering](#dynamic-component-filter)
|
|
42
91
|
|
|
43
92
|
## SyncComponent Method
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.6"
|
|
@@ -31,7 +31,7 @@ class AdobeRequest:
|
|
|
31
31
|
raise Exception(
|
|
32
32
|
'You have to upload the configuration file with importConfigFile method.')
|
|
33
33
|
self.config = deepcopy(config_object)
|
|
34
|
-
self.header = header
|
|
34
|
+
self.header = deepcopy(header)
|
|
35
35
|
self.retry = retry
|
|
36
36
|
if self.config['token'] == '' or time.time() > self.config['date_limit']:
|
|
37
37
|
if 'scopes' in self.config.keys() and self.config.get('scopes',None) is not None:
|
|
@@ -42,9 +42,9 @@ class AdobeRequest:
|
|
|
42
42
|
token_and_expiry = self.get_jwt_token_and_expiry_for_config(config=self.config, verbose=verbose)
|
|
43
43
|
token = token_and_expiry['token']
|
|
44
44
|
expiry = token_and_expiry['expiry']
|
|
45
|
-
self.token = token
|
|
46
|
-
self.config['token'] = token
|
|
47
|
-
self.config['date_limit'] = time.time() + expiry - 500
|
|
45
|
+
self.token = deepcopy(token)
|
|
46
|
+
self.config['token'] = deepcopy(token)
|
|
47
|
+
self.config['date_limit'] = deepcopy(time.time() + expiry - 500)
|
|
48
48
|
self.header.update({'Authorization': f'Bearer {token}'})
|
|
49
49
|
|
|
50
50
|
def get_jwt_token_and_expiry_for_config(self,config: dict, verbose: bool = False, save: bool = False, *args, **kwargs) -> Dict[str, str]:
|
|
@@ -166,8 +166,8 @@ class AdobeRequest:
|
|
|
166
166
|
elif self.connectionType == 'jwt':
|
|
167
167
|
token_and_expiry = self.get_jwt_token_and_expiry_for_config(config=self.config)
|
|
168
168
|
token = token_and_expiry['token']
|
|
169
|
-
self.config['token'] = token
|
|
170
|
-
self.config['date_limit'] = time.time() + token_and_expiry['expiry'] - 500
|
|
169
|
+
self.config['token'] = deepcopy(token)
|
|
170
|
+
self.config['date_limit'] = deepcopy(time.time() + token_and_expiry['expiry'] - 500)
|
|
171
171
|
self.header.update({'Authorization': f'Bearer {token}'})
|
|
172
172
|
|
|
173
173
|
def getData(self, endpoint: str, params: dict = None, data: dict = None, headers: dict = None, *args, **kwargs):
|
|
@@ -405,14 +405,23 @@ class Translator:
|
|
|
405
405
|
--> You can use the translate method to translate Data Element settings
|
|
406
406
|
3. setBaseRules
|
|
407
407
|
4. extendRules
|
|
408
|
-
--> You can use the translate method to translate Rule Components settings
|
|
408
|
+
--> You can use the translate method to translate Rule Components settings
|
|
409
|
+
|
|
410
|
+
Possible arguments:
|
|
411
|
+
mapping_extensions : dictionary of extension names that should be replaced.
|
|
412
|
+
Such as: {'old_name':'new_name'}
|
|
409
413
|
"""
|
|
410
414
|
|
|
411
|
-
def __init__(self):
|
|
415
|
+
def __init__(self,**kwargs):
|
|
412
416
|
self.rules = {}
|
|
413
417
|
self.baseRuleIdName = {}
|
|
414
418
|
self.extensions = {}
|
|
415
419
|
self.baseExtensionIdName = {}
|
|
420
|
+
self.mapping_extension = {}
|
|
421
|
+
if kwargs.get('mapping_extensions') is not None:
|
|
422
|
+
if type(kwargs.get('mapping_extensions')) == dict:
|
|
423
|
+
for key, value in kwargs.get('mapping_extensions').items():
|
|
424
|
+
self.mapping_extension[key] = value
|
|
416
425
|
|
|
417
426
|
def setBaseExtensions(self, base_property_extensions: list, property_name: str):
|
|
418
427
|
"""
|
|
@@ -422,8 +431,12 @@ class Translator:
|
|
|
422
431
|
property_name : REQUIRED : name of your base property.
|
|
423
432
|
"""
|
|
424
433
|
self.baseExtensionIdName = {ext['id'] : ext['attributes']['name'] for ext in base_property_extensions}
|
|
434
|
+
for extId, extName in self.baseExtensionIdName.items():
|
|
435
|
+
if extName in self.mapping_extension.keys():
|
|
436
|
+
self.baseExtensionIdName[extId] = self.mapping_extension[extName]
|
|
425
437
|
self.extensions = {ext['attributes']['name']:{property_name:ext['id']} for ext in base_property_extensions}
|
|
426
438
|
|
|
439
|
+
|
|
427
440
|
def extendExtensions(self, new_property_extensions: list, new_prop_name: str)-> None:
|
|
428
441
|
"""
|
|
429
442
|
Add the extensions id from a target property.
|
|
@@ -432,8 +445,11 @@ class Translator:
|
|
|
432
445
|
new_prop_name : REQUIRED : target property name.
|
|
433
446
|
"""
|
|
434
447
|
for ext in new_property_extensions:
|
|
435
|
-
|
|
436
|
-
|
|
448
|
+
extName = ext['attributes']['name']
|
|
449
|
+
if extName in self.mapping_extension.keys():
|
|
450
|
+
extName = self.mapping_extension[extName]
|
|
451
|
+
if extName in list(self.extensions.keys()):
|
|
452
|
+
self.extensions[extName][new_prop_name] = ext['id']
|
|
437
453
|
return self.extensions
|
|
438
454
|
|
|
439
455
|
def setBaseRules(self, base_property_rules: list, property_name: str):
|
|
@@ -313,7 +313,7 @@ class Library:
|
|
|
313
313
|
|
|
314
314
|
def setEnvironments(self, environments_list: list, dev_name: str = None)->None:
|
|
315
315
|
"""
|
|
316
|
-
Save the different environments ids available.
|
|
316
|
+
Save the different environments ids available in a config variable.
|
|
317
317
|
It is required to use the library class.
|
|
318
318
|
Arguments :
|
|
319
319
|
environments_list : REQUIRED : list of environment retrieved by the getEnvironments method
|
|
@@ -340,6 +340,21 @@ class Library:
|
|
|
340
340
|
res = new_env
|
|
341
341
|
return res
|
|
342
342
|
|
|
343
|
+
def setEnvironment(self, env_id: str,verbose:bool=False)->None:
|
|
344
|
+
"""
|
|
345
|
+
Set the environment of the library.
|
|
346
|
+
Arguments:
|
|
347
|
+
env_id : REQUIRED : The environment id you want to set.
|
|
348
|
+
"""
|
|
349
|
+
obj = {
|
|
350
|
+
"data": {
|
|
351
|
+
"id": env_id,
|
|
352
|
+
"type": "environments"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
res = self._setEnvironment(obj,verbose=verbose)
|
|
356
|
+
return res
|
|
357
|
+
|
|
343
358
|
def _removeEnvironment(self)->None:
|
|
344
359
|
"""
|
|
345
360
|
Remove environment
|
|
@@ -347,7 +362,7 @@ class Library:
|
|
|
347
362
|
path = f'/libraries/{self.id}/relationships/environment'
|
|
348
363
|
new_env = self.connector.getData(self.endpoint+path)
|
|
349
364
|
return new_env
|
|
350
|
-
|
|
365
|
+
|
|
351
366
|
def updateLibrary(self,empty:bool=False)->dict:
|
|
352
367
|
"""
|
|
353
368
|
Update the library
|
|
@@ -383,12 +398,14 @@ class Library:
|
|
|
383
398
|
return res
|
|
384
399
|
|
|
385
400
|
|
|
386
|
-
def build(self,verbose:bool=False)->dict:
|
|
401
|
+
def build(self,timesleep:int=20, verbose:bool=False)->dict:
|
|
387
402
|
"""
|
|
388
403
|
Build the library.
|
|
389
404
|
Part of the code takes care of assigning the right environement before building the library.
|
|
390
405
|
Returns the build when it is completed (succeed or not).
|
|
391
|
-
|
|
406
|
+
Arguments:
|
|
407
|
+
timesleep : OPTIONAL : How many seconds to wait between each check of the build status. Default to 20 seconds.
|
|
408
|
+
It will check every 20 seconds for the build status, making sure it is not "pending".
|
|
392
409
|
"""
|
|
393
410
|
if self.build_required == False and self.state != 'approved':
|
|
394
411
|
return 'build is not required'
|
|
@@ -430,7 +447,7 @@ class Library:
|
|
|
430
447
|
build_status = build['data']['attributes']['status']
|
|
431
448
|
while build_status == 'pending':
|
|
432
449
|
print('pending...')
|
|
433
|
-
time.sleep(
|
|
450
|
+
time.sleep(timesleep)
|
|
434
451
|
# return the json directly
|
|
435
452
|
build = self.connector.getData(
|
|
436
453
|
config.endpoints['global']+'/builds/'+str(build_id))
|
|
@@ -603,7 +603,7 @@ class Property:
|
|
|
603
603
|
data = data + append_data
|
|
604
604
|
return data
|
|
605
605
|
|
|
606
|
-
def getLibrary(self,libraryId:str=None)->dict:
|
|
606
|
+
def getLibrary(self,libraryId:str=None,return_class:bool=False)->dict:
|
|
607
607
|
"""
|
|
608
608
|
get a library based on its ID.
|
|
609
609
|
Arguments:
|
|
@@ -614,6 +614,8 @@ class Property:
|
|
|
614
614
|
path = f"/libraries/{libraryId}"
|
|
615
615
|
res = self.connector.getData(self.endpoint+path)
|
|
616
616
|
if 'data' in res.keys():
|
|
617
|
+
if return_class:
|
|
618
|
+
return Library(res['data'],config_object=self.connector.config,header=self.header)
|
|
617
619
|
return res['data']
|
|
618
620
|
return res
|
|
619
621
|
|
|
@@ -1278,7 +1280,10 @@ class Property:
|
|
|
1278
1280
|
if library in librariesIds:
|
|
1279
1281
|
libraryId = library
|
|
1280
1282
|
else:
|
|
1281
|
-
|
|
1283
|
+
try:
|
|
1284
|
+
libraryId = librariesNameId[library]
|
|
1285
|
+
except KeyError:
|
|
1286
|
+
raise ValueError("Library name or ID provided not found.")
|
|
1282
1287
|
path = f"/libraries/{libraryId}"
|
|
1283
1288
|
if components==True:
|
|
1284
1289
|
myLib = self.getLibrary(libraryId)
|
|
@@ -1286,10 +1291,11 @@ class Property:
|
|
|
1286
1291
|
rules = libClass.getRules()
|
|
1287
1292
|
dataelements = libClass.getDataElements()
|
|
1288
1293
|
res = self.connector.deleteData('https://reactor.adobe.io/'+path)
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1294
|
+
if components==True:
|
|
1295
|
+
for rule in rules:
|
|
1296
|
+
self.deleteRule(rule['id'])
|
|
1297
|
+
for de in dataelements:
|
|
1298
|
+
self.deleteDataElement(de['id'])
|
|
1293
1299
|
return res
|
|
1294
1300
|
|
|
1295
1301
|
def getProductionEndpoint(self)->dict:
|
|
@@ -21,37 +21,48 @@ class Synchronizer:
|
|
|
21
21
|
It requires that you have imported a configuration file.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
def __init__(self,base:str=None,targets:list=None,**kwargs)->None:
|
|
24
|
+
def __init__(self,base:str | Property = None,targets:list=None,**kwargs)->None:
|
|
25
25
|
"""
|
|
26
26
|
Instantiating the Synchronizer taking 2 parameters, base property name, target property list.
|
|
27
27
|
Arguments:
|
|
28
|
-
base : REQUIRED : The name of your base/template property
|
|
28
|
+
base : REQUIRED : The name of your base/template property or the instance of Property class
|
|
29
29
|
targets : REQUIRED : The list of property names that you want to sync with.
|
|
30
|
+
the list of target can be string of property name or Property instance for other organization.
|
|
30
31
|
possible kwargs:
|
|
31
32
|
dynamicRuleComponent: A data element name that contains rule for synchronization on the property.
|
|
33
|
+
mapping_extensions : A dictionary of target extension name and base extension name when in different IMS org and 2 private extensions are name differently for the same purpose.
|
|
32
34
|
"""
|
|
33
35
|
tmp_admin = Admin()
|
|
34
36
|
cid = tmp_admin.getCompanyId()
|
|
35
37
|
properties = tmp_admin.getProperties(cid)
|
|
36
|
-
|
|
37
|
-
if len(base_property) ==0:
|
|
38
|
-
raise KeyError("The base property name has not been found in your account")
|
|
38
|
+
mapping_extensions = kwargs.get('mapping_extensions',None)
|
|
39
39
|
self.base = {}
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
if type(base) == str:
|
|
41
|
+
base_property = [prop for prop in properties if prop['attributes']['name'] == base]
|
|
42
|
+
if len(base_property) ==0:
|
|
43
|
+
raise KeyError("The base property name has not been found in your account")
|
|
44
|
+
self.base["name"] = base
|
|
45
|
+
self.base["api"]:Property = Property(base_property[0])
|
|
46
|
+
elif type(base) == Property:
|
|
47
|
+
self.base["name"] = base.name
|
|
48
|
+
self.base["api"]:Property = base
|
|
42
49
|
self.base['rules'] = self.base['api'].getRules()
|
|
43
50
|
self.base['dataElements'] = self.base['api'].getDataElements()
|
|
44
51
|
self.base['extensions'] = self.base['api'].getExtensions()
|
|
45
|
-
self.translator = Translator()
|
|
52
|
+
self.translator = Translator(mapping_extensions=mapping_extensions)
|
|
46
53
|
self.translator.setBaseExtensions(self.base['extensions'],self.base['name'])
|
|
47
54
|
self.translator.setBaseRules(self.base['rules'],self.base['name'])
|
|
48
55
|
self.targets = {}
|
|
49
56
|
self.target_configs = {}
|
|
50
57
|
for target in targets:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
if isinstance(target,str):
|
|
59
|
+
tmp_target = [prop for prop in properties if prop['attributes']['name'] == target]
|
|
60
|
+
if len(tmp_target) == 0:
|
|
61
|
+
raise KeyError(f"The target property : {target} cannot be found. Please, fix it")
|
|
62
|
+
self.targets[target] = {'api' : Property(tmp_target[0]),'name':target}
|
|
63
|
+
elif isinstance(target,Property):
|
|
64
|
+
self.targets[target.name] = {'api' : deepcopy(target),'name':target.name}
|
|
65
|
+
target = target.name
|
|
55
66
|
self.targets[target]['rules'] = self.targets[target]['api'].getRules()
|
|
56
67
|
self.targets[target]['extensions'] = self.targets[target]['api'].getExtensions()
|
|
57
68
|
self.targets[target]['dataElements'] = self.targets[target]['api'].getDataElements()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.5-5"
|
|
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
|