total-perspective-vortex 3.3.0__tar.gz → 3.3.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.
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/PKG-INFO +4 -4
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/README.md +3 -3
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/pyproject.toml +1 -1
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_entity.py +47 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_explain.py +18 -2
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_helpers.py +85 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_resource_requirements.py +7 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_rules.py +59 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_resource_pool.py +139 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_shell.py +15 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/PKG-INFO +4 -4
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/entities.py +3 -1
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/helpers.py +16 -28
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/mapper.py +11 -17
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/LICENSE +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/setup.cfg +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/setup.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_dryrunner_resource_pool.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_dump.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_basic.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_context.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_destinations.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_inheritance.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_merge_multiple.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_params_specific.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_rank.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_resource_pool_integration.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_resubmit.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_role.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_sample.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_user.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_weighted_choice.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_scenarios.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/SOURCES.txt +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/dependency_links.txt +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/entry_points.txt +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/requires.txt +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/top_level.txt +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/__init__.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/__init__.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/dryrunner.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/dumper.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/formatter.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/linter.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/mypychecker.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/shell.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/test/__init__.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/test/mock_galaxy.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/type_check_template.j2 +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/__init__.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/evaluator.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/explain.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/loader.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/resource_pool.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/resource_requirements.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/util.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/rules/__init__.py +0 -0
- {total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/rules/gateway.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: total-perspective-vortex
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: A library for routing entities (jobs, users or groups) to destinations in Galaxy
|
|
5
5
|
Author: Galaxy Contributors
|
|
6
6
|
Maintainer-email: Galaxy Committers <galaxy-committers@lists.galaxyproject.org>
|
|
@@ -63,12 +63,12 @@ GTN](https://training.galaxyproject.org/training-material/topics/admin/tutorials
|
|
|
63
63
|
### Installing latest released version
|
|
64
64
|
|
|
65
65
|
Once configured in the Galaxy application, TPV will be automatically installed
|
|
66
|
-
in the Galaxy environment. If you would like to manually install it
|
|
67
|
-
can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
66
|
+
in the Galaxy environment. If you would like to manually install it for independent
|
|
67
|
+
CLI use instead, you can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
68
68
|
the following command:
|
|
69
69
|
|
|
70
70
|
```python
|
|
71
|
-
pip install total-perspective-vortex
|
|
71
|
+
pip install "total-perspective-vortex[cli]"
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
|
|
@@ -23,12 +23,12 @@ GTN](https://training.galaxyproject.org/training-material/topics/admin/tutorials
|
|
|
23
23
|
### Installing latest released version
|
|
24
24
|
|
|
25
25
|
Once configured in the Galaxy application, TPV will be automatically installed
|
|
26
|
-
in the Galaxy environment. If you would like to manually install it
|
|
27
|
-
can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
26
|
+
in the Galaxy environment. If you would like to manually install it for independent
|
|
27
|
+
CLI use instead, you can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
28
28
|
the following command:
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
|
-
pip install total-perspective-vortex
|
|
31
|
+
pip install "total-perspective-vortex[cli]"
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
|
|
@@ -11,6 +11,53 @@ from tpv.rules import gateway
|
|
|
11
11
|
|
|
12
12
|
class TestEntity(unittest.TestCase):
|
|
13
13
|
|
|
14
|
+
def test_rule_context_uses_local_copy_and_keeps_self_alias(self):
|
|
15
|
+
loader = TPVConfigLoader(
|
|
16
|
+
{
|
|
17
|
+
"tools": {
|
|
18
|
+
"test": {
|
|
19
|
+
"cores": 1,
|
|
20
|
+
"params": {},
|
|
21
|
+
"rules": [
|
|
22
|
+
{
|
|
23
|
+
"if": "self is entity and entity.cores == 1",
|
|
24
|
+
"execute": "entity.params['local_change'] = 'preserved'",
|
|
25
|
+
},
|
|
26
|
+
{"if": True, "cores": 4},
|
|
27
|
+
{
|
|
28
|
+
"if": "self is entity and entity.cores == 4",
|
|
29
|
+
"execute": "entity.mem = 8",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
original = loader.config.tools["test"]
|
|
37
|
+
context = {"entity": original, "self": original}
|
|
38
|
+
evaluated = original.evaluate_rules(context)
|
|
39
|
+
self.assertEqual(evaluated.cores, 4)
|
|
40
|
+
self.assertEqual(evaluated.mem, 8)
|
|
41
|
+
self.assertEqual(evaluated.params, {"local_change": "preserved"})
|
|
42
|
+
self.assertEqual(original.cores, 1)
|
|
43
|
+
self.assertIsNone(original.mem)
|
|
44
|
+
self.assertEqual(original.params, {})
|
|
45
|
+
self.assertIs(context["entity"], evaluated)
|
|
46
|
+
self.assertIs(context["self"], evaluated)
|
|
47
|
+
self.assertIsNot(evaluated, original)
|
|
48
|
+
|
|
49
|
+
def test_rule_context_initialized_when_no_rules_match(self):
|
|
50
|
+
for rules in ([], [{"if": False, "cores": 4}]):
|
|
51
|
+
with self.subTest(rules=rules):
|
|
52
|
+
loader = TPVConfigLoader({"tools": {"test": {"cores": 1, "rules": rules}}})
|
|
53
|
+
original = loader.config.tools["test"]
|
|
54
|
+
context = {}
|
|
55
|
+
evaluated = original.evaluate_rules(context)
|
|
56
|
+
self.assertIs(context["entity"], evaluated)
|
|
57
|
+
self.assertIs(context["self"], evaluated)
|
|
58
|
+
self.assertIsNot(evaluated, original)
|
|
59
|
+
self.assertEqual(evaluated.cores, 1)
|
|
60
|
+
|
|
14
61
|
@staticmethod
|
|
15
62
|
def _map_to_destination(app, job, tool, user, referrer=None):
|
|
16
63
|
tpv_config = os.path.join(os.path.dirname(__file__), "fixtures/mapping-rule-argument-based.yml")
|
|
@@ -242,6 +242,20 @@ class TestDryRunExplain(unittest.TestCase):
|
|
|
242
242
|
self.assertIn("Destination Ranking", trace)
|
|
243
243
|
self.assertIn("Final Result", trace)
|
|
244
244
|
|
|
245
|
+
def test_dry_run_explain_unconfigured_tool_without_a_default(self):
|
|
246
|
+
"""A tool with no entity in a config that sets no default_inherits still maps, via the
|
|
247
|
+
entity TPV builds from the Galaxy tool itself -- and the trace names that entity, which is
|
|
248
|
+
how an admin learns the tool got injected defaults rather than configured ones."""
|
|
249
|
+
dry_runner = TPVDryRunner.from_params(
|
|
250
|
+
job_conf=self._fixture_path("job_conf_dry_run.yml"),
|
|
251
|
+
tool_id="a_tool_nobody_configured",
|
|
252
|
+
user_email="fairycake@vortex.org",
|
|
253
|
+
tpv_confs=[self._fixture_path("mapping-inheritance-no-default.yml")],
|
|
254
|
+
)
|
|
255
|
+
destination, collector = dry_runner.run(explain=True)
|
|
256
|
+
self.assertIsNotNone(destination)
|
|
257
|
+
self.assertIn("matched entity 'tool_provided_resources_a_tool_nobody_configured'", collector.render())
|
|
258
|
+
|
|
245
259
|
def test_dry_run_explain_shows_matched_entities(self):
|
|
246
260
|
"""The trace should show which tool and user entities matched."""
|
|
247
261
|
dry_runner = TPVDryRunner.from_params(
|
|
@@ -501,7 +515,6 @@ class TestDryRunExplain(unittest.TestCase):
|
|
|
501
515
|
self.assertIsNone(runner.tool)
|
|
502
516
|
|
|
503
517
|
|
|
504
|
-
|
|
505
518
|
class TestGatewayExplainOnFailure(unittest.TestCase):
|
|
506
519
|
"""Tests for the tpv_explain_on_failure gateway config option."""
|
|
507
520
|
|
|
@@ -516,7 +529,10 @@ class TestGatewayExplainOnFailure(unittest.TestCase):
|
|
|
516
529
|
job = mock_galaxy.Job()
|
|
517
530
|
user = mock_galaxy.User("gargravarr", "fairycake@vortex.org")
|
|
518
531
|
return gateway.map_tool_to_destination(
|
|
519
|
-
app,
|
|
532
|
+
app,
|
|
533
|
+
job,
|
|
534
|
+
tool,
|
|
535
|
+
user,
|
|
520
536
|
referrer=referrer,
|
|
521
537
|
tpv_config_files=[self._fixture_path("mapping-basic.yml")],
|
|
522
538
|
explain_collector=explain_collector,
|
|
@@ -6,11 +6,15 @@ from unittest.mock import patch
|
|
|
6
6
|
|
|
7
7
|
from tpv.commands.test import mock_galaxy
|
|
8
8
|
from tpv.core.helpers import (
|
|
9
|
+
concurrent_job_count_for_tool,
|
|
9
10
|
get_dataset_attributes,
|
|
10
11
|
get_input_dataset,
|
|
11
12
|
get_input_datasets,
|
|
12
13
|
get_input_size,
|
|
13
14
|
input_size,
|
|
15
|
+
job_args_match,
|
|
16
|
+
tool_version_eq,
|
|
17
|
+
tool_version_gte,
|
|
14
18
|
weighted_choice,
|
|
15
19
|
weighted_random_sampling,
|
|
16
20
|
)
|
|
@@ -97,6 +101,13 @@ class TestHelpers(unittest.TestCase):
|
|
|
97
101
|
# the existing input_size helper deduplicates the same way
|
|
98
102
|
self.assertEqual(input_size(job), 8)
|
|
99
103
|
|
|
104
|
+
def test_get_input_size_skips_inputs_whose_dataset_is_gone(self):
|
|
105
|
+
# An input association can outlive its dataset (deleted or purged). It must contribute
|
|
106
|
+
# nothing rather than raise mid-scheduling.
|
|
107
|
+
job = self._job_with_multiple_data_param()
|
|
108
|
+
job.add_input_dataset(mock_galaxy.DatasetAssociation("gone", None))
|
|
109
|
+
self.assertEqual(get_input_size(job), 8)
|
|
110
|
+
|
|
100
111
|
def test_get_input_size_by_param_name(self):
|
|
101
112
|
job = self._job_with_multiple_data_param()
|
|
102
113
|
job.add_input_dataset(
|
|
@@ -263,3 +274,77 @@ class TestHelpers(unittest.TestCase):
|
|
|
263
274
|
|
|
264
275
|
self.assertIsInstance(result, dict)
|
|
265
276
|
self.assertEqual(result, items[1])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class TestJobArgsMatch(unittest.TestCase):
|
|
280
|
+
"""job_args_match decides whether a rule fires on a job's parameters. Its only test so far is
|
|
281
|
+
the positive case, which a helper that always returned True would pass."""
|
|
282
|
+
|
|
283
|
+
def setUp(self):
|
|
284
|
+
self.app = mock_galaxy.App(create_model=True)
|
|
285
|
+
self.job = mock_galaxy.Job()
|
|
286
|
+
self.job.param_values = {
|
|
287
|
+
"input_opts": {"db_selector": "db", "tabs_to_spaces": False},
|
|
288
|
+
"files": [{"name": "reads.fq"}],
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
def test_matches_only_when_every_given_value_matches(self):
|
|
292
|
+
self.assertTrue(job_args_match(self.job, self.app, {"input_opts": {"db_selector": "db"}}))
|
|
293
|
+
self.assertFalse(job_args_match(self.job, self.app, {"input_opts": {"db_selector": "other"}}))
|
|
294
|
+
# one matching and one differing value is not a match
|
|
295
|
+
self.assertFalse(
|
|
296
|
+
job_args_match(self.job, self.app, {"input_opts": {"db_selector": "db", "tabs_to_spaces": True}})
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def test_a_parameter_the_job_does_not_have_does_not_match(self):
|
|
300
|
+
self.assertFalse(job_args_match(self.job, self.app, {"input_opts": {"no_such_option": 1}}))
|
|
301
|
+
self.assertFalse(job_args_match(self.job, self.app, {"no_such_section": {"x": 1}}))
|
|
302
|
+
# a nested dict in args where the job's parameter is a scalar is a mismatch, not an error
|
|
303
|
+
self.assertFalse(job_args_match(self.job, self.app, {"files": {"name": "reads.fq"}}))
|
|
304
|
+
|
|
305
|
+
def test_nothing_to_match_against_never_matches(self):
|
|
306
|
+
for args in (None, {}, "input_opts", ["input_opts"]):
|
|
307
|
+
with self.subTest(args=args):
|
|
308
|
+
self.assertFalse(job_args_match(self.job, self.app, args))
|
|
309
|
+
|
|
310
|
+
def test_a_list_valued_parameter_is_compared_as_a_whole(self):
|
|
311
|
+
# Repeats and multi-selects are lists. The list is the value; keys inside its items are
|
|
312
|
+
# not a path into it.
|
|
313
|
+
self.assertTrue(job_args_match(self.job, self.app, {"files": [{"name": "reads.fq"}]}))
|
|
314
|
+
self.assertFalse(job_args_match(self.job, self.app, {"files": [{"name": "other.fq"}]}))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class TestConcurrentJobCount(unittest.TestCase):
|
|
318
|
+
def test_plain_tool_ids_match_exactly_and_do_not_count_prefix_collisions(self):
|
|
319
|
+
# Tool Shed ids are matched by prefix so every installed version is counted together.
|
|
320
|
+
# A plain id must not get that treatment: "bwa" must not count "bwa_mem" jobs.
|
|
321
|
+
app = mock_galaxy.App(create_model=True)
|
|
322
|
+
session = app.model.context
|
|
323
|
+
user = app.model.User(username="arthur", email="arthur@vortex.org", password="x")
|
|
324
|
+
session.add(user)
|
|
325
|
+
session.flush()
|
|
326
|
+
for tool_id in ("bwa", "bwa", "bwa_mem"):
|
|
327
|
+
job = app.model.Job()
|
|
328
|
+
job.user = user
|
|
329
|
+
job.tool_id = tool_id
|
|
330
|
+
job.state = "running"
|
|
331
|
+
session.add(job)
|
|
332
|
+
session.flush()
|
|
333
|
+
|
|
334
|
+
self.assertEqual(concurrent_job_count_for_tool(app, mock_galaxy.Tool("bwa")), 2)
|
|
335
|
+
self.assertEqual(concurrent_job_count_for_tool(app, mock_galaxy.Tool("bwa_mem")), 1)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class TestToolVersionComparison(unittest.TestCase):
|
|
339
|
+
def test_versions_are_compared_as_versions_not_strings(self):
|
|
340
|
+
tool = mock_galaxy.Tool("t", version="1.2.0")
|
|
341
|
+
self.assertTrue(tool_version_eq(tool, "1.2")) # PEP 440: 1.2.0 == 1.2
|
|
342
|
+
self.assertTrue(tool_version_eq(tool, "1.2.0"))
|
|
343
|
+
self.assertFalse(tool_version_eq(tool, "1.3"))
|
|
344
|
+
self.assertTrue(tool_version_gte(tool, "1.10") is False) # not a string compare: "1.2" < "1.10"
|
|
345
|
+
|
|
346
|
+
def test_comparison_is_unknown_not_false_when_a_version_is_missing(self):
|
|
347
|
+
# A rule can tell "no version to compare" (None) from "compared and did not match" (False).
|
|
348
|
+
unversioned = mock_galaxy.Tool("t")
|
|
349
|
+
self.assertIsNone(tool_version_eq(unversioned, "1.0"))
|
|
350
|
+
self.assertIsNone(tool_version_gte(mock_galaxy.Tool("t", version="1.0"), None))
|
|
@@ -40,6 +40,13 @@ class TestMapperResourceRequirements(unittest.TestCase):
|
|
|
40
40
|
result = extract_resource_requirements_from_tool(tool)
|
|
41
41
|
self.assertEqual(result, {})
|
|
42
42
|
|
|
43
|
+
def test_requirement_types_tpv_does_not_map_are_ignored(self):
|
|
44
|
+
# Tools may declare tmpdir_min, outdir_min etc. TPV maps only cores and RAM; the rest must
|
|
45
|
+
# be skipped, not fail the tool.
|
|
46
|
+
reqs = [ResourceRequirement("4", "cores_min"), ResourceRequirement("10", "tmpdir_min")]
|
|
47
|
+
tool = mock_galaxy.Tool("test_tool", resource_requirements=reqs)
|
|
48
|
+
self.assertEqual(extract_resource_requirements_from_tool(tool), {"cores": 4})
|
|
49
|
+
|
|
43
50
|
def test_extract_resource_requirements_from_tool_cores(self):
|
|
44
51
|
cores_req = ResourceRequirement("4", "cores_min")
|
|
45
52
|
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_rules.py
RENAMED
|
@@ -8,11 +8,45 @@ from galaxy.jobs import JobDestination
|
|
|
8
8
|
from galaxy.jobs.mapper import JobMappingException, JobNotReadyException
|
|
9
9
|
|
|
10
10
|
from tpv.commands.test import mock_galaxy
|
|
11
|
+
from tpv.core.entities import TryNextDestinationOrFail, TryNextDestinationOrWait
|
|
12
|
+
from tpv.core.loader import TPVConfigLoader
|
|
13
|
+
from tpv.core.mapper import EntityToDestinationMapper
|
|
11
14
|
from tpv.rules import gateway
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class TestMapperRules(unittest.TestCase):
|
|
15
18
|
|
|
19
|
+
def _destination_context_mapper(self):
|
|
20
|
+
config = os.path.join(os.path.dirname(__file__), "fixtures/mapping-destination-context.yml")
|
|
21
|
+
return EntityToDestinationMapper(TPVConfigLoader.from_url_or_path(config))
|
|
22
|
+
|
|
23
|
+
def _map_destination_context(self, mapper):
|
|
24
|
+
app = mock_galaxy.App(job_conf=os.path.join(os.path.dirname(__file__), "fixtures/job_conf.yml"))
|
|
25
|
+
return mapper.map_to_destination(app, mock_galaxy.Tool("test"), None, mock_galaxy.Job())
|
|
26
|
+
|
|
27
|
+
def test_first_destination_rule_condition_sees_combined_destination(self):
|
|
28
|
+
mapper = self._destination_context_mapper()
|
|
29
|
+
destination = self._map_destination_context(mapper)
|
|
30
|
+
self.assertEqual(destination.id, "condor")
|
|
31
|
+
self.assertEqual(destination.params["inspected_destination"], "condor")
|
|
32
|
+
self.assertEqual(mapper.global_context["attempts"], ["condor"])
|
|
33
|
+
|
|
34
|
+
def test_first_destination_rule_execute_sees_combined_destination(self):
|
|
35
|
+
mapper = self._destination_context_mapper()
|
|
36
|
+
mapper.destinations["condor"].rules["inspect_destination"].if_condition = True
|
|
37
|
+
destination = self._map_destination_context(mapper)
|
|
38
|
+
self.assertEqual(destination.params["inspected_destination"], "condor")
|
|
39
|
+
|
|
40
|
+
def test_destination_fallback_refreshes_rule_context(self):
|
|
41
|
+
for rejection in (TryNextDestinationOrFail, TryNextDestinationOrWait):
|
|
42
|
+
with self.subTest(rejection=rejection.__name__):
|
|
43
|
+
mapper = self._destination_context_mapper()
|
|
44
|
+
mapper.global_context["rejection"] = rejection
|
|
45
|
+
destination = self._map_destination_context(mapper)
|
|
46
|
+
self.assertEqual(destination.id, "fallback")
|
|
47
|
+
self.assertEqual(destination.params["inspected_destination"], "fallback")
|
|
48
|
+
self.assertEqual(mapper.global_context["attempts"], ["condor", "fallback"])
|
|
49
|
+
|
|
16
50
|
@staticmethod
|
|
17
51
|
def _map_to_destination(
|
|
18
52
|
tool,
|
|
@@ -128,6 +162,31 @@ class TestMapperRules(unittest.TestCase):
|
|
|
128
162
|
["8"],
|
|
129
163
|
)
|
|
130
164
|
|
|
165
|
+
def test_invalid_edit_keeps_the_previous_rules_active(self):
|
|
166
|
+
"""Hot reload must not take scheduling down on a bad edit: if the rewritten file fails to
|
|
167
|
+
load, the previously loaded rules stay in force and the failure is logged."""
|
|
168
|
+
with tempfile.NamedTemporaryFile("w+t") as tmp_file:
|
|
169
|
+
shutil.copy2(os.path.join(os.path.dirname(__file__), "fixtures/mapping-rules.yml"), tmp_file.name)
|
|
170
|
+
tool = mock_galaxy.Tool("bwa")
|
|
171
|
+
user = mock_galaxy.User("gargravarr", "fairycake@vortex.org")
|
|
172
|
+
datasets = [mock_galaxy.DatasetAssociation("test", mock_galaxy.Dataset("test.txt", file_size=5 * 1024**3))]
|
|
173
|
+
|
|
174
|
+
destination = self._map_to_destination(
|
|
175
|
+
tool, user, datasets, tpv_config_files=[tmp_file.name], reset_mappers=True
|
|
176
|
+
)
|
|
177
|
+
self.assertEqual([e["value"] for e in destination.env if e["name"] == "TEST_JOB_SLOTS_USER"], ["4"])
|
|
178
|
+
|
|
179
|
+
with open(tmp_file.name, "w") as f:
|
|
180
|
+
f.write("tools:\n default:\n cores: [this is not valid config\n")
|
|
181
|
+
with self.assertLogs("tpv.rules.gateway", level="WARNING") as logs:
|
|
182
|
+
time.sleep(2) # wait for the watcher to attempt the reload
|
|
183
|
+
self.assertTrue(any("Failed to reload" in line for line in logs.output), logs.output)
|
|
184
|
+
|
|
185
|
+
destination = self._map_to_destination(
|
|
186
|
+
tool, user, datasets, tpv_config_files=[tmp_file.name], reset_mappers=False
|
|
187
|
+
)
|
|
188
|
+
self.assertEqual([e["value"] for e in destination.env if e["name"] == "TEST_JOB_SLOTS_USER"], ["4"])
|
|
189
|
+
|
|
131
190
|
def test_multiple_files_automatically_reload_on_update(self):
|
|
132
191
|
with tempfile.NamedTemporaryFile("w+t") as tmp_file1, tempfile.NamedTemporaryFile("w+t") as tmp_file2:
|
|
133
192
|
rule_file = os.path.join(os.path.dirname(__file__), "fixtures/mapping-rules.yml")
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_resource_pool.py
RENAMED
|
@@ -182,6 +182,16 @@ class TestStoreConfig(unittest.TestCase):
|
|
|
182
182
|
self.assertEqual(StoreConfig().store_class, "tpv.core.resource_pool.ValkeyAllocationStore")
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
class TestStoreConfigErrors(unittest.TestCase):
|
|
186
|
+
def test_bare_class_name_is_rejected_with_a_clear_message(self):
|
|
187
|
+
from tpv.core.resource_pool import StoreConfig
|
|
188
|
+
|
|
189
|
+
# `class: InMemoryAllocationStore` (no module) is the likely typo; it must fail loudly at
|
|
190
|
+
# load, naming the problem, rather than with an AttributeError deep in importlib.
|
|
191
|
+
with self.assertRaisesRegex(ValueError, "dotted class path"):
|
|
192
|
+
StoreConfig.model_validate({"class": "InMemoryAllocationStore"}).build_store()
|
|
193
|
+
|
|
194
|
+
|
|
185
195
|
class TestTerminalJobIds(unittest.TestCase):
|
|
186
196
|
def test_returns_only_terminal_jobs(self):
|
|
187
197
|
app = mock_galaxy.App(create_model=True)
|
|
@@ -589,6 +599,131 @@ class TestResourcePoolBranches(unittest.TestCase):
|
|
|
589
599
|
EntityToDestinationMapper(loader)
|
|
590
600
|
|
|
591
601
|
|
|
602
|
+
class TestValkeyStoreBackendErrors(unittest.TestCase):
|
|
603
|
+
"""Fail-closed rests on the adapter turning backend errors into StoreUnavailable, which the
|
|
604
|
+
mapper turns into deferral. Every other fail-closed test raises StoreUnavailable directly;
|
|
605
|
+
this one drives real redis exceptions through the adapter, so the translation itself is
|
|
606
|
+
what is under test."""
|
|
607
|
+
|
|
608
|
+
@staticmethod
|
|
609
|
+
def _store_whose_backend_raises(error):
|
|
610
|
+
class Client:
|
|
611
|
+
def hgetall(self, key):
|
|
612
|
+
raise error
|
|
613
|
+
|
|
614
|
+
def register_script(self, script):
|
|
615
|
+
def run(keys, args):
|
|
616
|
+
raise error
|
|
617
|
+
|
|
618
|
+
return run
|
|
619
|
+
|
|
620
|
+
return ValkeyAllocationStore(client=Client())
|
|
621
|
+
|
|
622
|
+
@staticmethod
|
|
623
|
+
def _admission():
|
|
624
|
+
return PoolAdmission("p", ResourceUsage(1, 0, 0), NORMAL, Budget(), 0, False, set())
|
|
625
|
+
|
|
626
|
+
def test_redis_errors_become_store_unavailable(self):
|
|
627
|
+
import redis
|
|
628
|
+
|
|
629
|
+
for error in (redis.exceptions.ConnectionError("refused"), redis.exceptions.TimeoutError("slow")):
|
|
630
|
+
with self.subTest(error=type(error).__name__):
|
|
631
|
+
store = self._store_whose_backend_raises(error)
|
|
632
|
+
with self.assertRaises(StoreUnavailable):
|
|
633
|
+
store.read("p", 1)
|
|
634
|
+
with self.assertRaises(StoreUnavailable):
|
|
635
|
+
store.admit_many(1, 1, [self._admission()])
|
|
636
|
+
|
|
637
|
+
def test_other_errors_are_not_reported_as_an_outage(self):
|
|
638
|
+
# A bug in our own code must surface as itself. Reporting it as "store unavailable" would
|
|
639
|
+
# silently defer every governed job with a misleading warning.
|
|
640
|
+
store = self._store_whose_backend_raises(TypeError("bug"))
|
|
641
|
+
with self.assertRaises(TypeError):
|
|
642
|
+
store.read("p", 1)
|
|
643
|
+
with self.assertRaises(TypeError):
|
|
644
|
+
store.admit_many(1, 1, [self._admission()])
|
|
645
|
+
|
|
646
|
+
def test_valkey_url_schemes_map_to_the_redis_schemes_redis_py_understands(self):
|
|
647
|
+
import redis
|
|
648
|
+
|
|
649
|
+
cases = [
|
|
650
|
+
("valkey://host:6379/0", "redis://host:6379/0"),
|
|
651
|
+
("valkeys://host:6379/0", "rediss://host:6379/0"), # TLS
|
|
652
|
+
("redis://host:6379/0", "redis://host:6379/0"),
|
|
653
|
+
]
|
|
654
|
+
for given, expected in cases:
|
|
655
|
+
with self.subTest(url=given), patch.object(redis, "from_url") as from_url:
|
|
656
|
+
ValkeyAllocationStore(url=given)
|
|
657
|
+
self.assertEqual(from_url.call_args.args[0], expected)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class TestResourcePoolExplain(unittest.TestCase):
|
|
661
|
+
"""The explain trace is how an operator finds out why a job is stuck. The happy path and
|
|
662
|
+
permanent rejection are covered by the dry-run tests; these pin the messages for the
|
|
663
|
+
outcomes that actually leave a job waiting -- a full pool and a store outage -- and that the
|
|
664
|
+
fail_open variant says so rather than looking like a normal admission."""
|
|
665
|
+
|
|
666
|
+
def _mapper(self):
|
|
667
|
+
return EntityToDestinationMapper(TPVConfigLoader.from_url_or_path(FIXTURE))
|
|
668
|
+
|
|
669
|
+
@staticmethod
|
|
670
|
+
def _job(job_id):
|
|
671
|
+
job = mock_galaxy.Job()
|
|
672
|
+
job.id = job_id
|
|
673
|
+
return job
|
|
674
|
+
|
|
675
|
+
def _trace(self, mapper, tool, expect=None):
|
|
676
|
+
from tpv.core.explain import ExplainCollector
|
|
677
|
+
|
|
678
|
+
collector = ExplainCollector()
|
|
679
|
+
user = mock_galaxy.User("arthur", "arthur@vortex.org", id=1)
|
|
680
|
+
call = lambda: mapper.map_to_destination( # noqa: E731
|
|
681
|
+
mock_galaxy.App(create_model=True), mock_galaxy.Tool(tool), user, self._job(1), explain_collector=collector
|
|
682
|
+
)
|
|
683
|
+
if expect is None:
|
|
684
|
+
call()
|
|
685
|
+
else:
|
|
686
|
+
with self.assertRaises(expect):
|
|
687
|
+
call()
|
|
688
|
+
return collector.render()
|
|
689
|
+
|
|
690
|
+
def test_deferral_names_the_full_pool(self):
|
|
691
|
+
mapper = self._mapper()
|
|
692
|
+
_seed(mapper.resource_pools.store, "default", 1, 900, ResourceUsage(30, 0, 0))
|
|
693
|
+
trace = self._trace(mapper, "default", expect=JobNotReadyException)
|
|
694
|
+
self.assertIn("DEFERRED", trace)
|
|
695
|
+
self.assertIn("default", trace)
|
|
696
|
+
|
|
697
|
+
def test_store_outage_is_reported_with_the_policy_that_decided_it(self):
|
|
698
|
+
for fail_open, expect, message in (
|
|
699
|
+
(False, JobNotReadyException, "fail-closed -> deferred"),
|
|
700
|
+
(True, None, "fail_open -> admitted"),
|
|
701
|
+
):
|
|
702
|
+
with self.subTest(fail_open=fail_open):
|
|
703
|
+
mapper = self._mapper()
|
|
704
|
+
mapper.resource_pools.store = _RaisingStore()
|
|
705
|
+
mapper.pools["default"].fail_open = fail_open
|
|
706
|
+
trace = self._trace(mapper, "default", expect=expect)
|
|
707
|
+
self.assertIn("store unavailable", trace)
|
|
708
|
+
self.assertIn(message, trace)
|
|
709
|
+
|
|
710
|
+
def test_outage_during_the_batch_write_is_reported(self):
|
|
711
|
+
# The read succeeded and every pool resolved; the single atomic write then failed. The
|
|
712
|
+
# trace must say the store was the problem, not that the pool was full.
|
|
713
|
+
for all_fail_open, expect, message in (
|
|
714
|
+
(False, JobNotReadyException, "fail-closed -> deferred"),
|
|
715
|
+
(True, None, "every pool is fail_open -> admitted"),
|
|
716
|
+
):
|
|
717
|
+
with self.subTest(all_fail_open=all_fail_open):
|
|
718
|
+
mapper = self._mapper()
|
|
719
|
+
mapper.pools["default"].fail_open = all_fail_open
|
|
720
|
+
with patch.object(mapper.resource_pools.store, "admit_many", side_effect=StoreUnavailable("down")):
|
|
721
|
+
trace = self._trace(mapper, "default", expect=expect)
|
|
722
|
+
self.assertIn("Store unavailable", trace)
|
|
723
|
+
self.assertIn(message, trace)
|
|
724
|
+
self.assertNotIn("DEFERRED: user", trace)
|
|
725
|
+
|
|
726
|
+
|
|
592
727
|
def _build_valkey_store():
|
|
593
728
|
"""A ValkeyAllocationStore backed by fakeredis, or None when fakeredis+Lua is unavailable.
|
|
594
729
|
|
|
@@ -669,6 +804,10 @@ PARITY_SCENARIOS = {
|
|
|
669
804
|
_op(1, ResourceUsage(64, 0, 0), kind=OVERSIZE, max_oversize=1, reserve_pool=True),
|
|
670
805
|
_op(2, ResourceUsage(1, 0, 0), kind=NORMAL, max_oversize=1, reserve_pool=True),
|
|
671
806
|
],
|
|
807
|
+
"reserve_pool_blocks_oversize_while_normal_runs": [
|
|
808
|
+
_op(1, ResourceUsage(1, 0, 0), kind=NORMAL, max_oversize=1, reserve_pool=True),
|
|
809
|
+
_op(2, ResourceUsage(64, 0, 0), kind=OVERSIZE, max_oversize=1, reserve_pool=True),
|
|
810
|
+
],
|
|
672
811
|
"drop_releases": [_op(1, ResourceUsage(30, 0, 0)), _op(2, ResourceUsage(8, 0, 0), drop=(1,))],
|
|
673
812
|
}
|
|
674
813
|
|
|
@@ -152,6 +152,13 @@ class TPVShellTestCase(unittest.TestCase):
|
|
|
152
152
|
f"Expected Name 'mem2' is not defined but output was: {output}",
|
|
153
153
|
)
|
|
154
154
|
|
|
155
|
+
def test_lint_types_checks_role_rules(self):
|
|
156
|
+
# Every other type-check test puts the bad expression on a tool. A regression that skipped
|
|
157
|
+
# roles when gathering code blocks would leave role rules unchecked and pass those tests.
|
|
158
|
+
tpv_config = os.path.join(os.path.dirname(__file__), "fixtures/linter/linter-types-role-rule.yml")
|
|
159
|
+
output = self.call_shell_command("tpv", "-vv", "lint", tpv_config)
|
|
160
|
+
self.assertIn('error: Name "training_quota_exceeded" is not defined', output, output)
|
|
161
|
+
|
|
155
162
|
def test_lint_types_legacy_tagset_reference(self):
|
|
156
163
|
tpv_config = os.path.join(
|
|
157
164
|
os.path.dirname(__file__),
|
|
@@ -773,6 +780,14 @@ class TPVShellTestCase(unittest.TestCase):
|
|
|
773
780
|
self.assertIn("bwa:", output)
|
|
774
781
|
self.assertIn("local:", output)
|
|
775
782
|
|
|
783
|
+
def test_dump_includes_resource_pools(self):
|
|
784
|
+
# dump is how an admin inspects the merged config; pools are a top-level section like tools.
|
|
785
|
+
tpv_config = os.path.join(os.path.dirname(__file__), "fixtures/mapping-resource-pool.yml")
|
|
786
|
+
output = self.call_shell_command("tpv", "dump", tpv_config)
|
|
787
|
+
self.assertIn("--- Pools ---", output, output)
|
|
788
|
+
self.assertIn("gpu:", output)
|
|
789
|
+
self.assertIn("max_concurrent_gpus", output)
|
|
790
|
+
|
|
776
791
|
def test_dump_multiple_configs(self):
|
|
777
792
|
remote = os.path.join(os.path.dirname(__file__), "fixtures/mapping-merge-multiple-remote.yml")
|
|
778
793
|
local = os.path.join(os.path.dirname(__file__), "fixtures/mapping-merge-multiple-local.yml")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: total-perspective-vortex
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: A library for routing entities (jobs, users or groups) to destinations in Galaxy
|
|
5
5
|
Author: Galaxy Contributors
|
|
6
6
|
Maintainer-email: Galaxy Committers <galaxy-committers@lists.galaxyproject.org>
|
|
@@ -63,12 +63,12 @@ GTN](https://training.galaxyproject.org/training-material/topics/admin/tutorials
|
|
|
63
63
|
### Installing latest released version
|
|
64
64
|
|
|
65
65
|
Once configured in the Galaxy application, TPV will be automatically installed
|
|
66
|
-
in the Galaxy environment. If you would like to manually install it
|
|
67
|
-
can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
66
|
+
in the Galaxy environment. If you would like to manually install it for independent
|
|
67
|
+
CLI use instead, you can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
68
68
|
the following command:
|
|
69
69
|
|
|
70
70
|
```python
|
|
71
|
-
pip install total-perspective-vortex
|
|
71
|
+
pip install "total-perspective-vortex[cli]"
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
|
|
@@ -565,6 +565,8 @@ class EntityWithRules(Entity):
|
|
|
565
565
|
def evaluate_rules(self, context: dict[str, Any]) -> Self:
|
|
566
566
|
new_entity = copy.deepcopy(self)
|
|
567
567
|
context.update(new_entity.context or {})
|
|
568
|
+
# Conditions and execute blocks must see the local entity, including for each destination candidate.
|
|
569
|
+
context.update({"entity": new_entity, "self": new_entity})
|
|
568
570
|
explain = ExplainCollector.from_context(context)
|
|
569
571
|
for rule in self.rules.values():
|
|
570
572
|
if rule.is_matching(context):
|
|
@@ -590,7 +592,7 @@ class EntityWithRules(Entity):
|
|
|
590
592
|
new_entity.cores = rule.cores or new_entity.cores
|
|
591
593
|
new_entity.mem = rule.mem or new_entity.mem
|
|
592
594
|
new_entity.id = f"{new_entity.id}, Rule: {rule.id}"
|
|
593
|
-
context.update({"entity": new_entity})
|
|
595
|
+
context.update({"entity": new_entity, "self": new_entity})
|
|
594
596
|
else:
|
|
595
597
|
if explain:
|
|
596
598
|
explain.add_step(
|
|
@@ -123,7 +123,9 @@ def get_input_size(
|
|
|
123
123
|
"""
|
|
124
124
|
total = 0.0
|
|
125
125
|
for dataset in get_input_datasets(job, param_name):
|
|
126
|
-
|
|
126
|
+
# get_input_datasets already drops associations without a dataset; this narrows the
|
|
127
|
+
# Optional type for mypy and cannot run.
|
|
128
|
+
if not dataset.dataset: # pragma: no cover
|
|
127
129
|
continue
|
|
128
130
|
multiplier = 1.0
|
|
129
131
|
if estimate_uncompressed_size and (dataset.extension or "").endswith(COMPRESSED_EXTENSION_SUFFIXES):
|
|
@@ -166,38 +168,24 @@ def weighted_choice(items: list[WeightedT]) -> WeightedT:
|
|
|
166
168
|
return _weighted_draw(items, [item.get("weight", 1) for item in items], k=1)[0]
|
|
167
169
|
|
|
168
170
|
|
|
169
|
-
def
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
__get_keys_from_dict(x, keys_list)
|
|
171
|
+
def __args_match(args: Any, params: Any) -> bool:
|
|
172
|
+
# Every key in `args` must be present in `params`, recursing through nested dicts. Anything
|
|
173
|
+
# that is not a dict -- scalars and lists alike -- is a leaf and must compare equal, so a
|
|
174
|
+
# list-valued parameter (a repeat or multi-select) is matched as a whole.
|
|
175
|
+
if isinstance(args, dict):
|
|
176
|
+
if not isinstance(params, dict):
|
|
177
|
+
return False
|
|
178
|
+
return all(key in params and __args_match(value, params[key]) for key, value in args.items())
|
|
179
|
+
return bool(args == params)
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
def job_args_match(job: Job, app: UniverseApplication, args: dict[str, Any] | None) -> bool:
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
"""Whether every argument in ``args`` matches the job's parameters, e.g.
|
|
184
|
+
``{'input_opts': {'db_selector': 'db'}}`` matches a job whose ``input_opts|db_selector`` is
|
|
185
|
+
``db``. Nested dicts are matched key by key; lists and scalars must be equal."""
|
|
184
186
|
if not args or not isinstance(args, dict):
|
|
185
187
|
return False
|
|
186
|
-
|
|
187
|
-
matched = True
|
|
188
|
-
# check if the args in the config file are available
|
|
189
|
-
for arg in args:
|
|
190
|
-
arg_dict = {arg: args[arg]}
|
|
191
|
-
arg_keys_list: list[str] = []
|
|
192
|
-
__get_keys_from_dict(arg_dict, arg_keys_list)
|
|
193
|
-
try:
|
|
194
|
-
options_value = reduce(dict.__getitem__, arg_keys_list, options)
|
|
195
|
-
arg_value = reduce(dict.__getitem__, arg_keys_list, arg_dict)
|
|
196
|
-
if arg_value != options_value:
|
|
197
|
-
matched = False
|
|
198
|
-
except KeyError:
|
|
199
|
-
matched = False
|
|
200
|
-
return matched
|
|
188
|
+
return __args_match(args, job.get_param_values(app)) # type: ignore[no-untyped-call]
|
|
201
189
|
|
|
202
190
|
|
|
203
191
|
def concurrent_job_count_for_tool(
|
|
@@ -171,10 +171,10 @@ class EntityToDestinationMapper(object):
|
|
|
171
171
|
def __apply_default_destination_inheritance(
|
|
172
172
|
self, entity_list: dict[str, Destination], context: Mapping[str, Any]
|
|
173
173
|
) -> list[Destination]:
|
|
174
|
+
# Never empty: _get_common_inherits always contributes the tool_type_secure_defaults
|
|
175
|
+
# destination, so every destination inherits at least that.
|
|
174
176
|
inherited_defaults = self._get_common_inherits(context, entity_list, Destination)
|
|
175
|
-
|
|
176
|
-
return [self.inherit_entities([*inherited_defaults, entity]) for entity in entity_list.values()]
|
|
177
|
-
return list(entity_list.values())
|
|
177
|
+
return [self.inherit_entities([*inherited_defaults, entity]) for entity in entity_list.values()]
|
|
178
178
|
|
|
179
179
|
def inherit_entities(self, entities: list[EntityType]) -> EntityType:
|
|
180
180
|
return functools.reduce(lambda a, b: b.inherit(a), entities)
|
|
@@ -270,21 +270,15 @@ class EntityToDestinationMapper(object):
|
|
|
270
270
|
tool_id = f"{tool.tool_type}-{tool.dynamic_tool.uuid}"
|
|
271
271
|
else:
|
|
272
272
|
tool_id = tool.id or "unknown_tool_id"
|
|
273
|
+
# Never None: _get_common_inherits always contributes the tool_provided_resources entity
|
|
274
|
+
# built from the Galaxy tool itself, so an unconfigured tool still resolves to an entity.
|
|
273
275
|
tool_entity = self.inherit_matching_entities(context, Tool, "tools", tool_id)
|
|
274
|
-
|
|
275
|
-
if
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
f"Tool '{tool_id}': no explicit match, using default",
|
|
281
|
-
)
|
|
282
|
-
else:
|
|
283
|
-
if explain:
|
|
284
|
-
explain.add_step(
|
|
285
|
-
ExplainPhase.ENTITY_MATCHING,
|
|
286
|
-
f"Tool '{tool_id}': matched entity '{tool_entity.id}'",
|
|
287
|
-
)
|
|
276
|
+
assert tool_entity is not None
|
|
277
|
+
if explain:
|
|
278
|
+
explain.add_step(
|
|
279
|
+
ExplainPhase.ENTITY_MATCHING,
|
|
280
|
+
f"Tool '{tool_id}': matched entity '{tool_entity.id}'",
|
|
281
|
+
)
|
|
288
282
|
|
|
289
283
|
entity_list: list[EntityWithRules] = [tool_entity]
|
|
290
284
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_basic.py
RENAMED
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_context.py
RENAMED
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_destinations.py
RENAMED
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_inheritance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_resubmit.py
RENAMED
|
File without changes
|
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tests/test_mapper_sample.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
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/mypychecker.py
RENAMED
|
File without changes
|
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/test/__init__.py
RENAMED
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/commands/test/mock_galaxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{total_perspective_vortex-3.3.0 → total_perspective_vortex-3.3.1}/tpv/core/resource_requirements.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|