Skip to content

Commit 2d2c0e4

Browse files
committed
✨ New CommandPolicy class
xtl.jobs.policies - New BaseCommandPolicy abstract base class that supersedes the previous xtl.automate.priority_system:PrioritySystem - Two concrete implementations: DefaultCommandPolicy and NiceCommandPolicy - New CommandPolicy enum for enumeration and comparison of the built-in policies - New CommandPolicyType type alias docs/xtl.jobs.policies - Added documentation for command policies
1 parent a46d752 commit 2d2c0e4

File tree

4 files changed

+262
-0
lines changed

4 files changed

+262
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
``xtl.jobs.policies`` module
2+
============================
3+
4+
.. automodule:: xtl.jobs.policies
5+
:no-members:
6+
7+
Contents
8+
--------
9+
10+
.. autoclass:: xtl.jobs.policies.CommandPolicy
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+
:exclude-members: __new__
15+
16+
.. autoclass:: xtl.jobs.policies.BaseCommandPolicy
17+
:members:
18+
:undoc-members:
19+
:show-inheritance:
20+
:exclude-members: __init__, __new__
21+
22+
.. autoclass:: xtl.jobs.policies.DefaultCommandPolicy
23+
:members:
24+
:undoc-members:
25+
:show-inheritance:
26+
:exclude-members: __init__, __new__
27+
28+
.. autoclass:: xtl.jobs.policies.NiceCommandPolicy
29+
:members:
30+
:undoc-members:
31+
:show-inheritance:
32+
:exclude-members: __init__, __new__
33+
34+
.. autoclass:: xtl.jobs.policies.CommandPolicyType
35+
:members:
36+
:undoc-members:
37+
:show-inheritance:

docs/source/api/xtl.jobs.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ Submodules
1616
:maxdepth: 3
1717

1818
xtl.jobs.shells
19+
xtl.jobs.policies
1920
xtl.jobs.batchfiles

src/xtl/jobs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .shells import Shell, ShellType, BashShell, CmdShell, PowerShell
2+
from .policies import CommandPolicy, CommandPolicyType

src/xtl/jobs/policies.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""
2+
.. |BatchFile| replace:: :class:`BatchFile <xtl.jobs.batchfiles.BatchFile>`
3+
.. |CommandPolicy| replace:: :class:`CommandPolicy <xtl.jobs.policies.CommandPolicy>`
4+
.. |CommandPolicyType| replace:: :class:`CommandPolicyType <xtl.jobs.policies.CommandPolicyType>`
5+
.. |BaseCommandPolicy| replace:: :class:`BaseCommandPolicy <xtl.jobs.policies.BaseCommandPolicy>`
6+
.. |DefaultCommandPolicy| replace:: :class:`DefaultCommandPolicy <xtl.jobs.policies.DefaultCommandPolicy>`
7+
.. |NiceCommandPolicy| replace:: :class:`NiceCommandPolicy <xtl.jobs.policies.NiceCommandPolicy>`
8+
9+
Modifiers for the contents of |BatchFile| instances.
10+
11+
This module defines modifiers that can be applied to the contents of the |BatchFile|
12+
instances before they are executed on a compute site. The module defines an abstract
13+
base class |BaseCommandPolicy| that specifies the interface for command policies.
14+
15+
An example implementation that checks for the presence of ``rm`` commands and raises an
16+
exception if any are found could be as follows:
17+
18+
.. code-block:: python
19+
20+
import shlex
21+
from xtl.jobs.policies import BaseCommandPolicy
22+
23+
class NoRmPolicy(BaseCommandPolicy):
24+
name: str = 'no_rm'
25+
26+
def intercept_preamble(self, preamble: str) -> str:
27+
return preamble
28+
29+
def intercept_command(self, command: str) -> str:
30+
# Tokenize the input command
31+
tokens = shlex.split(command)
32+
for token in tokens:
33+
if token in ['rm', 'del', 'erase', 'rmdir', 'Remove-Item']:
34+
# The policy is agnostic to the shell being used
35+
raise ValueError("Usage of 'rm' command is not allowed.")
36+
return command
37+
38+
def intercept_content(self, content: str) -> str:
39+
return content
40+
41+
def intercept_postamble(self, postamble: str) -> str:
42+
return postamble
43+
44+
Two built-in command policies are provided:
45+
46+
- |DefaultCommandPolicy|: A policy that does not modify any part of the batch file
47+
contents.
48+
- |NiceCommandPolicy|: A policy that modifies commands to be executed with the ``nice``
49+
command to set their priority level on Unix-like systems.
50+
51+
The |CommandPolicy| enumeration provides a convenient way to select and instantiate
52+
these built-in command policies.
53+
54+
.. code-block:: python
55+
56+
from xtl.jobs.policies import CommandPolicy
57+
58+
# Instantiate the default command policy
59+
policy = CommandPolicy.DEFAULT.get()
60+
61+
# Check policy type
62+
policy == CommandPolicy.DEFAULT # True
63+
"""
64+
65+
from __future__ import annotations
66+
from abc import ABC, abstractmethod
67+
from dataclasses import dataclass, field
68+
from enum import Enum
69+
70+
from xtl.common.compatibility import PY310_OR_LESS
71+
72+
if PY310_OR_LESS:
73+
class StrEnum(str, Enum): ...
74+
else:
75+
from enum import StrEnum
76+
77+
78+
__all__ = ['CommandPolicy', 'CommandPolicyType',
79+
'BaseCommandPolicy', 'DefaultCommandPolicy', 'NiceCommandPolicy']
80+
81+
82+
class CommandPolicy(StrEnum):
83+
"""
84+
Enumeration of supported command policies for compute sites.
85+
"""
86+
87+
DEFAULT = 'default'
88+
"""The default command policy that does not modify commands."""
89+
NICE = 'nice'
90+
"""A command policy that uses the ``nice`` command to set process priority
91+
on Unix-like systems."""
92+
93+
def get(self) -> CommandPolicyType:
94+
"""
95+
Get an instance of the command policy corresponding to this enum member.
96+
"""
97+
mapping = {
98+
CommandPolicy.DEFAULT: DefaultCommandPolicy,
99+
CommandPolicy.NICE: NiceCommandPolicy,
100+
}
101+
return mapping[self]()
102+
103+
def __eq__(self, other):
104+
# Compare by type of the underlying command policy
105+
if isinstance(other, CommandPolicyType):
106+
return isinstance(other, self.get().__class__)
107+
elif isinstance(other, type):
108+
return other == self.get().__class__
109+
return super().__eq__(other)
110+
111+
def __hash__(self):
112+
return super().__hash__()
113+
114+
115+
@dataclass
116+
class BaseCommandPolicy(ABC):
117+
name: str = field(init=False, repr=False)
118+
"""The name of the command policy."""
119+
120+
def __init__(self):
121+
# Concrete classes should have default values for all the fields. The __init__
122+
# method is only defined here to avoid dataclass auto-generating one.
123+
pass
124+
125+
@abstractmethod
126+
def intercept_preamble(self, preamble: str) -> str:
127+
"""
128+
Intercepts the preamble of the batch file before any commands are added.
129+
130+
:param preamble: The preamble string to be intercepted.
131+
:return: The modified preamble string.
132+
"""
133+
...
134+
135+
@abstractmethod
136+
def intercept_command(self, command: str) -> str:
137+
"""
138+
Intercepts a single command/line before it is added to the batch file.
139+
140+
:param command: The command string to be intercepted.
141+
:return: The modified command string.
142+
"""
143+
...
144+
145+
@abstractmethod
146+
def intercept_content(self, content: str) -> str:
147+
"""
148+
Intercepts the entire content (*i.e.* all added commands) of the batch file
149+
before it is finalized.
150+
151+
:param content: The content string to be intercepted.
152+
:return: The modified content string.
153+
"""
154+
...
155+
156+
@abstractmethod
157+
def intercept_postamble(self, postamble: str) -> str:
158+
"""
159+
Intercepts the postamble of the batch file after all commands have been added.
160+
161+
:param postamble: The postamble string to be intercepted.
162+
:return: The modified postamble string.
163+
"""
164+
...
165+
166+
167+
class DefaultCommandPolicy(BaseCommandPolicy):
168+
"""
169+
A command policy that does not modify any part of the batch file contents. All
170+
``intercept_*`` methods return their input unmodified.
171+
"""
172+
173+
name: str = 'default'
174+
175+
def intercept_preamble(self, preamble: str) -> str:
176+
return preamble
177+
178+
def intercept_command(self, command: str) -> str:
179+
return command
180+
181+
def intercept_content(self, content: str) -> str:
182+
return content
183+
184+
def intercept_postamble(self, postamble: str) -> str:
185+
return postamble
186+
187+
188+
class NiceCommandPolicy(BaseCommandPolicy):
189+
"""
190+
A command policy that modifies commands to be executed with the
191+
`nice <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/nice.html>`_
192+
command to set their priority level. The niceness value can be configured via the
193+
``nice_value`` attribute (default: 10).
194+
"""
195+
196+
name: str = 'nice'
197+
nice_value: int = 10
198+
"""The niceness value to use with the ``nice`` command. This value should be between
199+
-20 (highest priority) and 19 (lowest priority)."""
200+
201+
def intercept_preamble(self, preamble: str) -> str:
202+
return preamble
203+
204+
def intercept_command(self, command: str) -> str:
205+
"""
206+
Pad commands with ``nice`` to set their priority level.
207+
208+
:param command: The command string to be intercepted.
209+
:return: The modified command string.
210+
"""
211+
return f'nice -n {self.nice_value} {command}'
212+
213+
def intercept_content(self, content: str) -> str:
214+
return content
215+
216+
def intercept_postamble(self, postamble: str) -> str:
217+
return postamble
218+
219+
220+
CommandPolicyType = BaseCommandPolicy | DefaultCommandPolicy | NiceCommandPolicy
221+
"""
222+
A type alias for all supported command policy types.
223+
"""

0 commit comments

Comments
 (0)