warlock-manager 2.2.4__tar.gz → 2.2.5__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.
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/PKG-INFO +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/pyproject.toml +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_base_service.py +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_unreal_config.py +81 -15
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/apps/base_app.py +8 -8
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/apps/steam_app.py +2 -2
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/unreal_config.py +73 -57
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/formatters/cli_formatter.py +42 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/cache.py +3 -3
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/firewall.py +36 -15
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/utils.py +13 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/mods/base_mod.py +7 -3
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/nexus/nexus.py +12 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/services/base_service.py +14 -4
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager.egg-info/PKG-INFO +1 -1
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/LICENSE +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/README.md +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/setup.cfg +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_app.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_base_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_cli_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_cli_formatter.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_cmd.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_config_key.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_ini_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_sensitive_data_filter.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_socket_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_unreal_config_ark_spawn_entities.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_unreal_save.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_version.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/apps/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/apps/manual_app.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/base_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/cli_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/config_key.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/ini_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/json_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/config/properties_config.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/formatters/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/app.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/app_runner.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/cmd.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/download.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/get_wan_ip.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/java.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/meta.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/ports.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/sensitive_data_filter.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/tui.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/version.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/mods/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/mods/warlock_nexus_mod.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/services/__init__.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/services/http_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/services/rcon_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/services/socket_service.py +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager.egg-info/SOURCES.txt +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager.egg-info/dependency_links.txt +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager.egg-info/requires.txt +0 -0
- {warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager.egg-info/top_level.txt +0 -0
|
@@ -61,4 +61,4 @@ class TestBaseService(unittest.TestCase):
|
|
|
61
61
|
self.assertIn('Type=simple', data_new)
|
|
62
62
|
self.assertIn('ExecStart=%s' % svc.get_executable(), data_new)
|
|
63
63
|
self.assertIn('WorkingDirectory=%s' % svc.get_app_directory(), data_new)
|
|
64
|
-
self.assertIn('EnvironmentFile=%s/Environments/%s.env' % (utils.
|
|
64
|
+
self.assertIn('EnvironmentFile=%s/Environments/%s.env' % (utils.get_base_directory(), svc.service), data_new)
|
|
@@ -23,32 +23,32 @@ class TestUnrealConfig(unittest.TestCase):
|
|
|
23
23
|
cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
|
|
24
24
|
# Configs are grouped by named parameters, so let's add some options
|
|
25
25
|
cfg.add_option({
|
|
26
|
-
'name': '
|
|
26
|
+
'name': 'Key 1',
|
|
27
27
|
'section': 'SomeSection',
|
|
28
28
|
'key': 'Key1',
|
|
29
29
|
})
|
|
30
30
|
cfg.add_option({
|
|
31
|
-
'name': '
|
|
31
|
+
'name': 'Key 2',
|
|
32
32
|
'section': 'SomeSection',
|
|
33
33
|
'key': 'Key2',
|
|
34
34
|
'type': 'int'
|
|
35
35
|
})
|
|
36
36
|
cfg.add_option({
|
|
37
|
-
'name': '
|
|
37
|
+
'name': 'Key 3',
|
|
38
38
|
'section': 'SomeSection',
|
|
39
39
|
'key': 'Key3',
|
|
40
40
|
'type': 'bool'
|
|
41
41
|
})
|
|
42
42
|
cfg.load()
|
|
43
43
|
|
|
44
|
-
self.assertEqual(cfg.get_value('
|
|
45
|
-
self.assertEqual(cfg.get_value('
|
|
46
|
-
self.assertEqual(cfg.get_value('
|
|
44
|
+
self.assertEqual(cfg.get_value('Key 1'), 'Value1')
|
|
45
|
+
self.assertEqual(cfg.get_value('Key 2'), 42)
|
|
46
|
+
self.assertEqual(cfg.get_value('Key 3'), True)
|
|
47
47
|
|
|
48
48
|
# These values should exist
|
|
49
|
-
self.assertTrue(cfg.has_value('
|
|
50
|
-
self.assertTrue(cfg.has_value('
|
|
51
|
-
self.assertTrue(cfg.has_value('
|
|
49
|
+
self.assertTrue(cfg.has_value('Key 1'))
|
|
50
|
+
self.assertTrue(cfg.has_value('Key 2'))
|
|
51
|
+
self.assertTrue(cfg.has_value('Key 3'))
|
|
52
52
|
|
|
53
53
|
# This value should not
|
|
54
54
|
self.assertFalse(cfg.has_value('NonExistentKey'))
|
|
@@ -58,14 +58,14 @@ class TestUnrealConfig(unittest.TestCase):
|
|
|
58
58
|
expected = f.read()
|
|
59
59
|
self.assertEqual(expected, cfg.fetch())
|
|
60
60
|
|
|
61
|
-
cfg.set_value('
|
|
62
|
-
self.assertEqual(cfg.get_value('
|
|
61
|
+
cfg.set_value('Key 1', 'NewValue')
|
|
62
|
+
self.assertEqual(cfg.get_value('Key 1'), 'NewValue')
|
|
63
63
|
|
|
64
|
-
cfg.set_value('
|
|
65
|
-
self.assertEqual(cfg.get_value('
|
|
64
|
+
cfg.set_value('Key 2', 100)
|
|
65
|
+
self.assertEqual(cfg.get_value('Key 2'), 100)
|
|
66
66
|
|
|
67
|
-
cfg.set_value('
|
|
68
|
-
self.assertEqual(cfg.get_value('
|
|
67
|
+
cfg.set_value('Key 3', False)
|
|
68
|
+
self.assertEqual(cfg.get_value('Key 3'), False)
|
|
69
69
|
|
|
70
70
|
# Ensure the generated data matches expectations
|
|
71
71
|
expected = '''; This is a simple config file for testing purposes.
|
|
@@ -80,6 +80,61 @@ Key3=False
|
|
|
80
80
|
'''
|
|
81
81
|
self.assertEqual(expected, cfg.fetch())
|
|
82
82
|
|
|
83
|
+
def test_always_escape_strings(self):
|
|
84
|
+
cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
|
|
85
|
+
cfg.always_escape_strings = True
|
|
86
|
+
# Configs are grouped by named parameters, so let's add some options
|
|
87
|
+
cfg.add_option({
|
|
88
|
+
'name': 'Key 1',
|
|
89
|
+
'section': 'SomeSection',
|
|
90
|
+
'key': 'Group/Key1',
|
|
91
|
+
})
|
|
92
|
+
cfg.add_option({
|
|
93
|
+
'name': 'Key 2',
|
|
94
|
+
'section': 'SomeSection',
|
|
95
|
+
'key': 'Group/Key2',
|
|
96
|
+
'type': 'int'
|
|
97
|
+
})
|
|
98
|
+
cfg.add_option({
|
|
99
|
+
'name': 'Key 3',
|
|
100
|
+
'section': 'SomeSection',
|
|
101
|
+
'key': 'Group/Key3',
|
|
102
|
+
'type': 'bool'
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
cfg.set_value('Key 1', 'Value1')
|
|
106
|
+
cfg.set_value('Key 2', 42)
|
|
107
|
+
cfg.set_value('Key 3', True)
|
|
108
|
+
|
|
109
|
+
self.assertEqual(cfg.get_value('Key 1'), 'Value1')
|
|
110
|
+
self.assertEqual(cfg.get_value('Key 2'), 42)
|
|
111
|
+
self.assertEqual(cfg.get_value('Key 3'), True)
|
|
112
|
+
|
|
113
|
+
# These values should exist
|
|
114
|
+
self.assertTrue(cfg.has_value('Key 1'))
|
|
115
|
+
self.assertTrue(cfg.has_value('Key 2'))
|
|
116
|
+
self.assertTrue(cfg.has_value('Key 3'))
|
|
117
|
+
|
|
118
|
+
# This value should not
|
|
119
|
+
self.assertFalse(cfg.has_value('NonExistentKey'))
|
|
120
|
+
|
|
121
|
+
# Ensure the generated data matches expectations
|
|
122
|
+
expected = '[SomeSection]\nGroup=(Key1="Value1",Key2=42,Key3=True)\n'
|
|
123
|
+
self.assertEqual(expected, cfg.fetch())
|
|
124
|
+
|
|
125
|
+
cfg.set_value('Key 1', 'NewValue')
|
|
126
|
+
self.assertEqual(cfg.get_value('Key 1'), 'NewValue')
|
|
127
|
+
|
|
128
|
+
cfg.set_value('Key 2', 100)
|
|
129
|
+
self.assertEqual(cfg.get_value('Key 2'), 100)
|
|
130
|
+
|
|
131
|
+
cfg.set_value('Key 3', False)
|
|
132
|
+
self.assertEqual(cfg.get_value('Key 3'), False)
|
|
133
|
+
|
|
134
|
+
# Ensure the generated data matches expectations
|
|
135
|
+
expected = '[SomeSection]\nGroup=(Key1="NewValue",Key2=100,Key3=False)\n'
|
|
136
|
+
self.assertEqual(expected, cfg.fetch())
|
|
137
|
+
|
|
83
138
|
def test_simple_create(self):
|
|
84
139
|
cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
|
|
85
140
|
# Configs are grouped by named parameters, so let's add some options
|
|
@@ -247,6 +302,9 @@ PlayedMaps=NewMap2_WP
|
|
|
247
302
|
})
|
|
248
303
|
cfg.load()
|
|
249
304
|
|
|
305
|
+
self.assertTrue(cfg.has_value('Difficulty'))
|
|
306
|
+
self.assertFalse(cfg.has_value('I do not exist'))
|
|
307
|
+
|
|
250
308
|
self.assertEqual(cfg.get_value('Difficulty'), 'None')
|
|
251
309
|
self.assertEqual(cfg.get_value('Randomizer Seed'), '')
|
|
252
310
|
self.assertEqual(cfg.get_value('Randomizer Pal Level Random'), False)
|
|
@@ -256,6 +314,14 @@ PlayedMaps=NewMap2_WP
|
|
|
256
314
|
expected = f.read()
|
|
257
315
|
self.assertEqual(expected, cfg.fetch())
|
|
258
316
|
|
|
317
|
+
cfg.set_value('Difficulty', 'Super Duper Hard')
|
|
318
|
+
cfg.set_value('Randomizer Seed', 'Random Seed')
|
|
319
|
+
cfg.set_value('Randomizer Pal Level Random', True)
|
|
320
|
+
|
|
321
|
+
self.assertEqual(cfg.get_value('Difficulty'), 'Super Duper Hard')
|
|
322
|
+
self.assertEqual(cfg.get_value('Randomizer Seed'), 'Random Seed')
|
|
323
|
+
self.assertEqual(cfg.get_value('Randomizer Pal Level Random'), True)
|
|
324
|
+
|
|
259
325
|
def test_palworld_empty(self):
|
|
260
326
|
"""
|
|
261
327
|
Test that the Palworld format works even when the ini is empty.
|
|
@@ -576,7 +576,7 @@ class BaseApp(ABC):
|
|
|
576
576
|
except urllib_error.HTTPError as e:
|
|
577
577
|
print('Could not notify Discord: %s' % e)
|
|
578
578
|
|
|
579
|
-
@deprecated("Please use
|
|
579
|
+
@deprecated("Please use get_base_directory() from utils instead.")
|
|
580
580
|
def get_app_directory(self) -> str:
|
|
581
581
|
"""
|
|
582
582
|
Get the base directory for this game installation.
|
|
@@ -585,7 +585,7 @@ class BaseApp(ABC):
|
|
|
585
585
|
|
|
586
586
|
:return:
|
|
587
587
|
"""
|
|
588
|
-
return utils.
|
|
588
|
+
return utils.get_base_directory()
|
|
589
589
|
|
|
590
590
|
@deprecated("Please use get_home_directory() from utils instead")
|
|
591
591
|
def get_home_directory(self) -> str:
|
|
@@ -663,8 +663,8 @@ class BaseApp(ABC):
|
|
|
663
663
|
logging.info('Removed config file for %s at %s' % (self.name, config.path))
|
|
664
664
|
|
|
665
665
|
# Cleanup directory structure
|
|
666
|
-
shutil.rmtree(os.path.join(utils.
|
|
667
|
-
shutil.rmtree(os.path.join(utils.
|
|
666
|
+
shutil.rmtree(os.path.join(utils.get_base_directory(), 'AppFiles'))
|
|
667
|
+
shutil.rmtree(os.path.join(utils.get_base_directory(), 'Environments'))
|
|
668
668
|
|
|
669
669
|
def remove_service(self, service_name: str):
|
|
670
670
|
"""
|
|
@@ -689,7 +689,7 @@ class BaseApp(ABC):
|
|
|
689
689
|
Try to detect available services for this game.
|
|
690
690
|
:return:
|
|
691
691
|
"""
|
|
692
|
-
envs = os.path.join(utils.
|
|
692
|
+
envs = os.path.join(utils.get_base_directory(), 'Environments')
|
|
693
693
|
if os.path.exists(envs):
|
|
694
694
|
# Each service should have a file here, named as {service}.env
|
|
695
695
|
services = []
|
|
@@ -724,9 +724,9 @@ class BaseApp(ABC):
|
|
|
724
724
|
"""
|
|
725
725
|
|
|
726
726
|
# Ensure some baseline directories exist with the correct ownership and permissions
|
|
727
|
-
utils.makedirs(os.path.join(utils.
|
|
728
|
-
utils.makedirs(os.path.join(utils.
|
|
729
|
-
utils.makedirs(os.path.join(utils.
|
|
727
|
+
utils.makedirs(os.path.join(utils.get_base_directory(), 'Backups'))
|
|
728
|
+
utils.makedirs(os.path.join(utils.get_base_directory(), 'AppFiles'))
|
|
729
|
+
utils.makedirs(os.path.join(utils.get_base_directory(), 'Environments'))
|
|
730
730
|
|
|
731
731
|
return True
|
|
732
732
|
|
|
@@ -317,7 +317,7 @@ class SteamApp(BaseApp, ABC):
|
|
|
317
317
|
|
|
318
318
|
:return:
|
|
319
319
|
"""
|
|
320
|
-
app_manifest = os.path.join(utils.
|
|
320
|
+
app_manifest = os.path.join(utils.get_base_directory(), 'AppFiles', 'steamapps', 'appmanifest_%s.acf' % self.steam_id)
|
|
321
321
|
|
|
322
322
|
if not os.path.exists(app_manifest):
|
|
323
323
|
print(f"App manifest file {app_manifest} does not exist.", file=sys.stderr)
|
|
@@ -395,7 +395,7 @@ class SteamApp(BaseApp, ABC):
|
|
|
395
395
|
cmd = Cmd([
|
|
396
396
|
guess_steamcmd_path(),
|
|
397
397
|
'+force_install_dir',
|
|
398
|
-
os.path.join(utils.
|
|
398
|
+
os.path.join(utils.get_base_directory(), 'AppFiles'),
|
|
399
399
|
'+login',
|
|
400
400
|
'anonymous',
|
|
401
401
|
'+app_update',
|
|
@@ -12,9 +12,64 @@ class UnrealConfig(BaseConfig):
|
|
|
12
12
|
super().__init__(group_name)
|
|
13
13
|
self.path = path
|
|
14
14
|
self._data = []
|
|
15
|
-
self._values = {}
|
|
16
15
|
self._use_array_operators = False
|
|
17
16
|
self._is_changed = False
|
|
17
|
+
self.always_escape_strings = False
|
|
18
|
+
"""
|
|
19
|
+
Set to True to always escape strings in Configuration files. Some games require this.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def _get_raw_value(self, name: str):
|
|
23
|
+
"""
|
|
24
|
+
Get the raw value, possibly in a nested dictionary.
|
|
25
|
+
|
|
26
|
+
useful for get_value and has_value.
|
|
27
|
+
|
|
28
|
+
:param name: Name of the option
|
|
29
|
+
:return:
|
|
30
|
+
"""
|
|
31
|
+
if name not in self.options:
|
|
32
|
+
return None
|
|
33
|
+
opt = self.options[name]
|
|
34
|
+
|
|
35
|
+
# Check if this section exists, also serves to find the section.
|
|
36
|
+
section = None
|
|
37
|
+
for sec in self._data:
|
|
38
|
+
if sec[0]['type'] == 'section' and sec[0]['value'] == opt.section:
|
|
39
|
+
section = sec[1:]
|
|
40
|
+
break
|
|
41
|
+
if section is None:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if '/' in opt.key:
|
|
45
|
+
# Look for a key inside structured data (aka a dict)
|
|
46
|
+
group = opt.key.split('/')[0]
|
|
47
|
+
for sec in section:
|
|
48
|
+
if sec['key'] == group:
|
|
49
|
+
current = sec['value']
|
|
50
|
+
for part in opt.key.split('/')[1:]:
|
|
51
|
+
if part in current:
|
|
52
|
+
current = current[part]
|
|
53
|
+
else:
|
|
54
|
+
# Subkey does not exist in the value of this section config; doesn't exist.
|
|
55
|
+
return None
|
|
56
|
+
return current
|
|
57
|
+
else:
|
|
58
|
+
# Simple key.
|
|
59
|
+
ret = None
|
|
60
|
+
for sec in section:
|
|
61
|
+
if sec['type'] == 'keyvalue' and sec['key'] == opt.key:
|
|
62
|
+
# Unreal supports duplicate keys; these should be exposed as a list.
|
|
63
|
+
if ret is None:
|
|
64
|
+
ret = sec['value']
|
|
65
|
+
elif isinstance(ret, list):
|
|
66
|
+
ret.append(sec['value'])
|
|
67
|
+
else:
|
|
68
|
+
ret = [ret]
|
|
69
|
+
ret.append(sec['value'])
|
|
70
|
+
return ret
|
|
71
|
+
|
|
72
|
+
return None
|
|
18
73
|
|
|
19
74
|
def get_value(self, name: str) -> Union[str, int, bool, list]:
|
|
20
75
|
"""
|
|
@@ -28,27 +83,11 @@ class UnrealConfig(BaseConfig):
|
|
|
28
83
|
return ''
|
|
29
84
|
|
|
30
85
|
opt = self.options[name]
|
|
31
|
-
|
|
32
|
-
if
|
|
33
|
-
|
|
86
|
+
raw_value = self._get_raw_value(name)
|
|
87
|
+
if raw_value is None:
|
|
88
|
+
return opt.default
|
|
34
89
|
else:
|
|
35
|
-
|
|
36
|
-
# Struct key
|
|
37
|
-
parts = opt.key.split('/')
|
|
38
|
-
current = self._values[opt.section]
|
|
39
|
-
for part in parts:
|
|
40
|
-
if part in current:
|
|
41
|
-
current = current[part]
|
|
42
|
-
else:
|
|
43
|
-
current = opt.default
|
|
44
|
-
break
|
|
45
|
-
val = current
|
|
46
|
-
elif opt.key not in self._values[opt.section]:
|
|
47
|
-
val = opt.default
|
|
48
|
-
else:
|
|
49
|
-
val = self._values[opt.section][opt.key]
|
|
50
|
-
|
|
51
|
-
return opt.to_system_type(val)
|
|
90
|
+
return opt.to_system_type(raw_value)
|
|
52
91
|
|
|
53
92
|
def _find_or_create_value(self, section: list, key: str, str_value: Union[str, list]) -> list:
|
|
54
93
|
"""
|
|
@@ -136,9 +175,14 @@ class UnrealConfig(BaseConfig):
|
|
|
136
175
|
opt = self.options[name]
|
|
137
176
|
str_value = self.from_system_type(name, value)
|
|
138
177
|
|
|
139
|
-
|
|
178
|
+
# Create the section if necessary
|
|
179
|
+
exists = False
|
|
180
|
+
for sec in self._data:
|
|
181
|
+
if sec[0]['type'] == 'section' and sec[0]['value'] == opt.section:
|
|
182
|
+
exists = True
|
|
183
|
+
break
|
|
184
|
+
if not exists:
|
|
140
185
|
# Create the section
|
|
141
|
-
self._values[opt.section] = {}
|
|
142
186
|
self._data.append([{'type': 'section', 'value': opt.section}])
|
|
143
187
|
|
|
144
188
|
# Ensure the updated value is in the data structure
|
|
@@ -157,7 +201,6 @@ class UnrealConfig(BaseConfig):
|
|
|
157
201
|
new_data.append(sec)
|
|
158
202
|
|
|
159
203
|
self._data = new_data
|
|
160
|
-
self._values[opt.section][opt.key] = str_value
|
|
161
204
|
self._is_changed = True
|
|
162
205
|
|
|
163
206
|
def has_value(self, name: str) -> bool:
|
|
@@ -167,17 +210,8 @@ class UnrealConfig(BaseConfig):
|
|
|
167
210
|
:param name: Name of the option
|
|
168
211
|
:return:
|
|
169
212
|
"""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
opt = self.options[name]
|
|
173
|
-
|
|
174
|
-
if opt.section not in self._values:
|
|
175
|
-
return False
|
|
176
|
-
else:
|
|
177
|
-
if opt.key not in self._values[opt.section]:
|
|
178
|
-
return False
|
|
179
|
-
else:
|
|
180
|
-
return self._values[opt.section][opt.key] != ''
|
|
213
|
+
raw_value = self._get_raw_value(name)
|
|
214
|
+
return raw_value is not None
|
|
181
215
|
|
|
182
216
|
def exists(self) -> bool:
|
|
183
217
|
"""
|
|
@@ -194,7 +228,6 @@ class UnrealConfig(BaseConfig):
|
|
|
194
228
|
if os.path.exists(self.path):
|
|
195
229
|
with open(self.path, 'r', encoding='utf-8') as f:
|
|
196
230
|
section = []
|
|
197
|
-
last_section = ''
|
|
198
231
|
for line in f.readlines():
|
|
199
232
|
data = None
|
|
200
233
|
stripped = line.strip()
|
|
@@ -232,27 +265,6 @@ class UnrealConfig(BaseConfig):
|
|
|
232
265
|
self._data.append(section)
|
|
233
266
|
section = []
|
|
234
267
|
section.append(data)
|
|
235
|
-
last_section = data['value']
|
|
236
|
-
elif data['type'] == 'keystruct':
|
|
237
|
-
section.append(data)
|
|
238
|
-
if last_section not in self._values:
|
|
239
|
-
self._values[last_section] = {}
|
|
240
|
-
self._values[last_section][data['key']] = data['value']
|
|
241
|
-
elif data['type'] == 'keyvalue':
|
|
242
|
-
section.append(data)
|
|
243
|
-
if last_section not in self._values:
|
|
244
|
-
self._values[last_section] = {}
|
|
245
|
-
# Auto-handle duplicate keys by converting them to a list.
|
|
246
|
-
# UE is weird.
|
|
247
|
-
if data['key'] in self._values[last_section]:
|
|
248
|
-
# Existing key, convert to list
|
|
249
|
-
existing_value = self._values[last_section][data['key']]
|
|
250
|
-
if not isinstance(existing_value, list):
|
|
251
|
-
existing_value = [existing_value]
|
|
252
|
-
existing_value.append(data['value'])
|
|
253
|
-
self._values[last_section][data['key']] = existing_value
|
|
254
|
-
else:
|
|
255
|
-
self._values[last_section][data['key']] = data['value']
|
|
256
268
|
else:
|
|
257
269
|
section.append(data)
|
|
258
270
|
if len(section) > 0:
|
|
@@ -289,6 +301,10 @@ class UnrealConfig(BaseConfig):
|
|
|
289
301
|
# Boolean strings do not require quoting
|
|
290
302
|
return False
|
|
291
303
|
|
|
304
|
+
if self.always_escape_strings:
|
|
305
|
+
# Game requested to always escape strings
|
|
306
|
+
return True
|
|
307
|
+
|
|
292
308
|
if re.match(r'^[A-Za-z0-9]+$', s) is not None:
|
|
293
309
|
# Simple strings do not require quoting
|
|
294
310
|
return False
|
|
@@ -10,6 +10,48 @@ def cli_formatter(
|
|
|
10
10
|
true_value: str | bool = 'True',
|
|
11
11
|
false_value: str | bool = 'False',
|
|
12
12
|
) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Format a given Configuration object as CLI arguments.
|
|
15
|
+
|
|
16
|
+
## True/False Formatting
|
|
17
|
+
|
|
18
|
+
The most complicated part of this is handling true/false boolean values.
|
|
19
|
+
|
|
20
|
+
The default is to render bool TRUE values as -key_name=True and bool FALSE values as -key_name=False
|
|
21
|
+
|
|
22
|
+
Render bool TRUE values as -key_name and bool FALSE values are omitted completely
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
cli_formatter(..., prefix='-', true_value=True, false_value=False)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The inverse is possible too, to omit TRUE values and only render FALSE values.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
cli_formatter(..., prefix='-', true_value=False, false_value=True)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Render bool TRUE values as -key_name=true and bool FALSE values as -key_name=false
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
cli_formatter(..., prefix='-', true_value='true', false_value='false')
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Render bool TRUE values as ?key_name:YUP and bool FALSE values as ?key_name:LULZNOPE
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
cli_formatter(..., prefix='?', sep=':', true_value='YUP', false_value='LULZNOPE')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
:param data:
|
|
47
|
+
:param section:
|
|
48
|
+
:param prefix:
|
|
49
|
+
:param sep:
|
|
50
|
+
:param joiner:
|
|
51
|
+
:param true_value:
|
|
52
|
+
:param false_value:
|
|
53
|
+
:return:
|
|
54
|
+
"""
|
|
13
55
|
values = []
|
|
14
56
|
for opt in data.options.values():
|
|
15
57
|
if opt.section != section:
|
|
@@ -8,7 +8,7 @@ from warlock_manager.libs import utils
|
|
|
8
8
|
def get_cache(some_string: str, expires: int = 3600) -> str | None:
|
|
9
9
|
# Check cache prior to running the command.
|
|
10
10
|
cmd_hash = hashlib.sha256(some_string.encode()).hexdigest()
|
|
11
|
-
cache_path = os.path.join(utils.
|
|
11
|
+
cache_path = os.path.join(utils.get_base_directory(), '.cache', cmd_hash)
|
|
12
12
|
if os.path.exists(cache_path) and os.path.getmtime(cache_path) > time.time() - expires:
|
|
13
13
|
with open(cache_path, "r") as f:
|
|
14
14
|
return f.read()
|
|
@@ -17,13 +17,13 @@ def get_cache(some_string: str, expires: int = 3600) -> str | None:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def save_cache(some_string: str, content: str):
|
|
20
|
-
cache_path = os.path.join(utils.
|
|
20
|
+
cache_path = os.path.join(utils.get_base_directory(), '.cache')
|
|
21
21
|
if not os.path.exists(cache_path):
|
|
22
22
|
os.makedirs(cache_path)
|
|
23
23
|
utils.ensure_file_ownership(cache_path)
|
|
24
24
|
|
|
25
25
|
cmd_hash = hashlib.sha256(some_string.encode()).hexdigest()
|
|
26
|
-
cache_path = os.path.join(utils.
|
|
26
|
+
cache_path = os.path.join(utils.get_base_directory(), '.cache', cmd_hash)
|
|
27
27
|
with open(cache_path, "w") as f:
|
|
28
28
|
f.write(content)
|
|
29
29
|
utils.ensure_file_ownership(cache_path)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from warlock_manager.libs.cmd import Cmd
|
|
2
|
+
import logging
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class Firewall:
|
|
@@ -79,7 +80,7 @@ class Firewall:
|
|
|
79
80
|
return None
|
|
80
81
|
|
|
81
82
|
@classmethod
|
|
82
|
-
def allow(cls, port: int, protocol: str = 'tcp', comment: str = None) ->
|
|
83
|
+
def allow(cls, port: int, protocol: str = 'tcp', comment: str = None) -> bool:
|
|
83
84
|
"""
|
|
84
85
|
Allows a specific port through the system's firewall.
|
|
85
86
|
Supports UFW, Firewalld, and iptables.
|
|
@@ -90,30 +91,41 @@ class Firewall:
|
|
|
90
91
|
comment (str, optional): An optional comment for the rule. Defaults to None.
|
|
91
92
|
"""
|
|
92
93
|
|
|
94
|
+
if port <= 0 or port >= 65536:
|
|
95
|
+
logging.error(f"Invalid port number: {port}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
93
98
|
firewall = cls.get_available()
|
|
94
99
|
|
|
95
100
|
if firewall == 'ufw':
|
|
101
|
+
logging.info(f"Allowing {port}/{protocol} via UFW")
|
|
96
102
|
cmd = Cmd(['ufw', 'allow', f'{port}/{protocol}'])
|
|
97
103
|
if comment:
|
|
98
104
|
cmd.extend(['comment', comment])
|
|
99
|
-
cmd.
|
|
105
|
+
return cmd.success
|
|
100
106
|
|
|
101
107
|
elif firewall == 'firewalld':
|
|
102
|
-
|
|
103
|
-
Cmd(['firewall-cmd', '--
|
|
108
|
+
logging.info(f"Allowing {port}/{protocol} via Firewalld")
|
|
109
|
+
if Cmd(['firewall-cmd', '--permanent', '--add-port', f'{port}/{protocol}']).success:
|
|
110
|
+
Cmd(['firewall-cmd', '--reload']).run()
|
|
111
|
+
return True
|
|
104
112
|
|
|
105
113
|
elif firewall == 'iptables':
|
|
114
|
+
logging.info(f"Allowing {port}/{protocol} via iptables")
|
|
106
115
|
cmd = Cmd(['iptables', '-A', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT'])
|
|
107
116
|
if comment:
|
|
108
117
|
cmd.extend(['-m', 'comment', '--comment', comment])
|
|
109
|
-
cmd.
|
|
110
|
-
|
|
118
|
+
if cmd.success:
|
|
119
|
+
Cmd(['service', 'iptables', 'save']).run()
|
|
120
|
+
return True
|
|
111
121
|
|
|
112
122
|
else:
|
|
113
|
-
|
|
123
|
+
logging.error('No supported firewall found on the system.')
|
|
124
|
+
|
|
125
|
+
return False
|
|
114
126
|
|
|
115
127
|
@classmethod
|
|
116
|
-
def remove(cls, port: int, protocol: str = 'tcp') ->
|
|
128
|
+
def remove(cls, port: int, protocol: str = 'tcp') -> bool:
|
|
117
129
|
"""
|
|
118
130
|
Removes a specific port from the system's firewall.
|
|
119
131
|
Supports UFW, Firewalld, and iptables.
|
|
@@ -123,21 +135,29 @@ class Firewall:
|
|
|
123
135
|
protocol (str, optional): The protocol to use ('tcp' or 'udp'). Defaults to 'tcp'.
|
|
124
136
|
"""
|
|
125
137
|
|
|
138
|
+
if port <= 0 or port >= 65536:
|
|
139
|
+
logging.error(f"Invalid port number: {port}")
|
|
140
|
+
return False
|
|
141
|
+
|
|
126
142
|
firewall = cls.get_available()
|
|
127
143
|
|
|
128
144
|
if firewall == 'ufw':
|
|
129
|
-
Cmd(['ufw', 'delete', 'allow', f'{port}/{protocol}']).
|
|
145
|
+
return Cmd(['ufw', 'delete', 'allow', f'{port}/{protocol}']).success
|
|
130
146
|
|
|
131
147
|
elif firewall == 'firewalld':
|
|
132
|
-
Cmd(['firewall-cmd', '--permanent', '--remove-port', f'{port}/{protocol}']).
|
|
133
|
-
|
|
148
|
+
if Cmd(['firewall-cmd', '--permanent', '--remove-port', f'{port}/{protocol}']).success:
|
|
149
|
+
Cmd(['firewall-cmd', '--reload']).run()
|
|
150
|
+
return True
|
|
134
151
|
|
|
135
152
|
elif firewall == 'iptables':
|
|
136
|
-
Cmd(['iptables', '-D', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT']).
|
|
137
|
-
|
|
153
|
+
if Cmd(['iptables', '-D', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT']).success:
|
|
154
|
+
Cmd(['service', 'iptables', 'save']).run()
|
|
155
|
+
return True
|
|
138
156
|
|
|
139
157
|
else:
|
|
140
|
-
|
|
158
|
+
logging.error('No supported firewall found on the system.')
|
|
159
|
+
|
|
160
|
+
return False
|
|
141
161
|
|
|
142
162
|
@classmethod
|
|
143
163
|
def is_global_open(cls, port: int, protocol: str = 'tcp') -> bool:
|
|
@@ -188,4 +208,5 @@ class Firewall:
|
|
|
188
208
|
return False
|
|
189
209
|
|
|
190
210
|
else:
|
|
191
|
-
|
|
211
|
+
logging.error('No supported firewall found on the system.')
|
|
212
|
+
return True # No firewall means it's probably enabled by default, so we return true.
|
|
@@ -2,8 +2,10 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import pwd
|
|
4
4
|
import sys
|
|
5
|
+
from typing_extensions import deprecated
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
@deprecated('Please use utils.get_base_directory instead to avoid confusion')
|
|
7
9
|
def get_app_directory() -> str:
|
|
8
10
|
"""
|
|
9
11
|
Get the base directory for this game installation.
|
|
@@ -15,6 +17,17 @@ def get_app_directory() -> str:
|
|
|
15
17
|
return os.path.dirname(os.path.realpath(sys.argv[0]))
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
def get_base_directory() -> str:
|
|
21
|
+
"""
|
|
22
|
+
Get the base directory for this game installation.
|
|
23
|
+
|
|
24
|
+
This directory usually will contain manage.py, AppFiles, Backups, and other related files.
|
|
25
|
+
|
|
26
|
+
:return:
|
|
27
|
+
"""
|
|
28
|
+
return os.path.dirname(os.path.realpath(sys.argv[0]))
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
def get_home_directory() -> str:
|
|
19
32
|
"""
|
|
20
33
|
Get the home directory of the user running this application
|
|
@@ -195,7 +195,7 @@ class BaseMod:
|
|
|
195
195
|
logging.error('Mod install package not found!')
|
|
196
196
|
return False
|
|
197
197
|
|
|
198
|
-
target_archive = os.path.join(utils.
|
|
198
|
+
target_archive = os.path.join(utils.get_base_directory(), 'Packages', self.package)
|
|
199
199
|
if not os.path.exists(target_archive):
|
|
200
200
|
download_file(self.source, target_archive)
|
|
201
201
|
else:
|
|
@@ -242,7 +242,7 @@ class BaseMod:
|
|
|
242
242
|
Get all registered mods, eg all mods which are present in the registration file
|
|
243
243
|
:return:
|
|
244
244
|
"""
|
|
245
|
-
mods_path = os.path.join(utils.
|
|
245
|
+
mods_path = os.path.join(utils.get_base_directory(), 'Packages', 'mods.json')
|
|
246
246
|
if not os.path.exists(mods_path):
|
|
247
247
|
# No mods installed; mods cache is empty.
|
|
248
248
|
return []
|
|
@@ -264,7 +264,11 @@ class BaseMod:
|
|
|
264
264
|
:param mods:
|
|
265
265
|
:return:
|
|
266
266
|
"""
|
|
267
|
-
|
|
267
|
+
mods_directory = os.path.join(utils.get_base_directory(), 'Packages')
|
|
268
|
+
if not os.path.exists(mods_directory):
|
|
269
|
+
utils.makedirs(mods_directory)
|
|
270
|
+
|
|
271
|
+
mods_path = os.path.join(utils.get_base_directory(), 'Packages', 'mods.json')
|
|
268
272
|
|
|
269
273
|
flat_mods = []
|
|
270
274
|
for mod in mods:
|
|
@@ -275,6 +275,12 @@ class Nexus:
|
|
|
275
275
|
'X-Host-Token': self.host_auth,
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
if self.game is None:
|
|
279
|
+
return {
|
|
280
|
+
'success': False,
|
|
281
|
+
'message': 'Game not set',
|
|
282
|
+
}
|
|
283
|
+
|
|
278
284
|
try:
|
|
279
285
|
url = self.base_url + '/mod/search/' + self.game
|
|
280
286
|
params = {
|
|
@@ -306,6 +312,12 @@ class Nexus:
|
|
|
306
312
|
'X-Host-Token': self.host_auth,
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
if self.game is None:
|
|
316
|
+
return {
|
|
317
|
+
'success': False,
|
|
318
|
+
'message': 'Game not set',
|
|
319
|
+
}
|
|
320
|
+
|
|
309
321
|
try:
|
|
310
322
|
url = self.base_url + '/mod/get/' + self.game + '/' + str(provider) + '/' + str(mod_id)
|
|
311
323
|
params = {}
|
|
@@ -49,7 +49,7 @@ class BaseService(ABC):
|
|
|
49
49
|
used for checking existence and loading configuration options from the file
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
self._env_file = os.path.join(utils.
|
|
52
|
+
self._env_file = os.path.join(utils.get_base_directory(), 'Environments', '%s.env' % service)
|
|
53
53
|
"""
|
|
54
54
|
:type str:
|
|
55
55
|
Fully resolved path on the filesystem for the environmental variable for this service
|
|
@@ -1274,13 +1274,23 @@ class BaseService(ABC):
|
|
|
1274
1274
|
continue
|
|
1275
1275
|
|
|
1276
1276
|
port = self.get_option_value(port_config[0])
|
|
1277
|
+
port_default = self.get_option_default(port_config[0])
|
|
1277
1278
|
if port == 0:
|
|
1278
1279
|
# This port does not have a default value, probably not enabled by default.
|
|
1279
1280
|
continue
|
|
1280
1281
|
new_port = self.game.get_next_available_port(self, port, port_config[1])
|
|
1281
1282
|
|
|
1283
|
+
if port_default == new_port:
|
|
1284
|
+
# New installations where the default port may not trigger the 'change' logic
|
|
1285
|
+
# as the port _technically_ didn't change, therefore the firewall rules won't be added.
|
|
1286
|
+
logging.info('Setting %s to 0 to force firewall change' % port_config[0])
|
|
1287
|
+
self.set_option(port_config[0], 0)
|
|
1288
|
+
|
|
1282
1289
|
self.set_option(port_config[0], new_port)
|
|
1283
|
-
|
|
1290
|
+
if new_port != port:
|
|
1291
|
+
logging.info('Set %s to %s to try to avoid conflicts' % (port_config[0], new_port))
|
|
1292
|
+
else:
|
|
1293
|
+
logging.info('Set %s to %s' % (port_config[0], new_port))
|
|
1284
1294
|
|
|
1285
1295
|
# Reload systemd to pick up the new service
|
|
1286
1296
|
self.reload()
|
|
@@ -1715,14 +1725,14 @@ class BaseService(ABC):
|
|
|
1715
1725
|
utils.makedirs(target_file)
|
|
1716
1726
|
elif source == '@':
|
|
1717
1727
|
# Source is the package itself; this just copies the entire mod into the destination.
|
|
1718
|
-
source_file = os.path.join(utils.
|
|
1728
|
+
source_file = os.path.join(utils.get_base_directory(), 'Packages', mod.package)
|
|
1719
1729
|
logging.info('Copying %s -> %s' % (source_file, target_file))
|
|
1720
1730
|
shutil.copy(source_file, target_file)
|
|
1721
1731
|
utils.ensure_file_ownership(target_file)
|
|
1722
1732
|
elif source.startswith('@:'):
|
|
1723
1733
|
# Source is a file within the package, (usually a ZIP archive)
|
|
1724
1734
|
source_file = source[2:]
|
|
1725
|
-
source_archive = os.path.join(utils.
|
|
1735
|
+
source_archive = os.path.join(utils.get_base_directory(), 'Packages', mod.package)
|
|
1726
1736
|
if source_archive.endswith('.zip'):
|
|
1727
1737
|
logging.info('Extracting %s -> %s' % (source_file, target_file))
|
|
1728
1738
|
with ZipFile(source_archive, 'r') as zip_ref:
|
|
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
|
{warlock_manager-2.2.4 → warlock_manager-2.2.5}/tests/test_unreal_config_ark_spawn_entities.py
RENAMED
|
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
|
{warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager/libs/sensitive_data_filter.py
RENAMED
|
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
|
{warlock_manager-2.2.4 → warlock_manager-2.2.5}/warlock_manager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|