1 | from __future__ import ( |
||
0 ignored issues
–
show
|
|||
2 | absolute_import, |
||
3 | division, |
||
4 | print_function |
||
5 | ) |
||
6 | |||
7 | import weakref |
||
8 | import logging |
||
9 | import collections |
||
10 | |||
11 | from .state_status import ( |
||
12 | StateStatus, |
||
13 | StepStateStatus, |
||
14 | ) |
||
15 | from .step import Step |
||
16 | |||
17 | _LOGGER = logging.getLogger(__name__) |
||
18 | INIT_STEP = '$init' |
||
19 | END_STEP = '$end' |
||
20 | |||
21 | |||
22 | class DeciderStepResult(object): |
||
0 ignored issues
–
show
This class should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
23 | pass |
||
24 | |||
25 | |||
26 | class DeciderStep(Step): |
||
0 ignored issues
–
show
This class should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
27 | def run(self, _step_input): |
||
28 | return DeciderStepResult() |
||
29 | |||
30 | def prepare(self, _context): |
||
31 | return None |
||
32 | |||
33 | def render(self, output): |
||
34 | # The out of the INIT step is the input to the workflow |
||
35 | return output |
||
36 | |||
37 | |||
38 | class State(object): |
||
0 ignored issues
–
show
This class should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
39 | __slots__ = ( |
||
40 | 'status', |
||
41 | 'step_states', |
||
42 | '_context', |
||
43 | '_orphaned_steps', |
||
44 | '_init_step', |
||
45 | '_end_step', |
||
46 | ) |
||
47 | |||
48 | def __init__(self): |
||
49 | """Init state""" |
||
50 | self.status = StateStatus.init |
||
51 | self.step_states = {} |
||
52 | self._context = None |
||
53 | self._orphaned_steps = {} |
||
54 | # Add an INIT/END fake steps |
||
55 | self._init_step = StepState( |
||
56 | step=DeciderStep(INIT_STEP), |
||
57 | status='running', |
||
58 | context='__init__' |
||
59 | ) |
||
60 | self._end_step = StepState( |
||
61 | step=DeciderStep(END_STEP, |
||
62 | requires=[(INIT_STEP, 'completed')]), |
||
63 | context='__init__' |
||
64 | ) |
||
65 | self._stepstate_insert(self._init_step) |
||
66 | self._stepstate_insert(self._end_step) |
||
67 | |||
68 | def __repr__(self): |
||
69 | return '{ctype}(status={status},steps={steps})'.format( |
||
70 | ctype=self.__class__.__name__, |
||
71 | status=self.status.name, |
||
72 | steps=len(self.step_states) |
||
73 | ) |
||
74 | |||
75 | ################################################# |
||
76 | def is_in_state(self, state_name): |
||
0 ignored issues
–
show
This method should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
77 | desired_state = getattr(StateStatus, state_name) |
||
78 | return self.status.means(desired_state) |
||
79 | |||
80 | ################################################# |
||
81 | # Context management |
||
82 | def __call__(self, context): |
||
83 | self._context = context |
||
84 | return self |
||
85 | |||
86 | def __enter__(self): |
||
87 | assert self._context is not None |
||
88 | |||
89 | def __exit__(self, exc_type, exc_value, traceback): |
||
90 | assert self._context is not None |
||
91 | # Only clear the context is we didn't encounter an exception. Otherwise |
||
92 | # keep it for debug purposes |
||
93 | if exc_type is None: |
||
94 | self._context = None |
||
95 | |||
96 | ################################################# |
||
97 | # State manipulations |
||
98 | def set_abort(self): |
||
99 | """Abort the state machine. |
||
100 | """ |
||
101 | assert self._context is not None |
||
102 | self.step_update(END_STEP, 'aborted') |
||
103 | |||
104 | def set_input(self, input_data): |
||
105 | """Set the input of the state machine. |
||
106 | """ |
||
107 | assert self._context is not None |
||
108 | assert self.status is StateStatus.init |
||
109 | |||
110 | self.step_update(INIT_STEP, 'completed', new_data=input_data) |
||
111 | self.status = StateStatus.running |
||
112 | |||
113 | def step_update(self, step_name, new_status, new_data=None): |
||
114 | """Update a Step with new status and, optionally, output data. |
||
115 | """ |
||
116 | assert self._context is not None |
||
117 | step_state = self.step_states[step_name] |
||
118 | step_state.update(new_status, |
||
119 | context=self._context, |
||
120 | new_output=new_data) |
||
121 | |||
122 | if self._end_step.status is StepStateStatus.ready: |
||
123 | self.status = StateStatus.succeeded |
||
124 | elif self._end_step.status is StepStateStatus.aborted: |
||
125 | self.status = StateStatus.failed |
||
126 | |||
127 | def step_insert(self, step): |
||
128 | """Add a step definition to the state. |
||
129 | """ |
||
130 | assert self._context is not None |
||
131 | step_state = StepState(step, self._context) |
||
132 | _LOGGER.info('Defining new step %r in state', step_state) |
||
133 | |||
134 | # All steps are children of the root INIT_STEP step |
||
135 | self._init_step.children.add(step_state) |
||
136 | step_state.parents.add(self._init_step) |
||
137 | |||
138 | if self._stepstate_insert(step_state): |
||
139 | # All steps are a parent of END_STEP |
||
140 | # FIXME: Could be optimized so that only steps without children are |
||
0 ignored issues
–
show
|
|||
141 | # parented to END_STEP |
||
142 | self._end_step.parents.add(step_state) |
||
143 | self._end_step.step.requires[step_state.step.name] = \ |
||
144 | StepStateStatus.completed |
||
145 | |||
146 | step_state.children.add(self._end_step) |
||
147 | # See if we can re-parents some previously orphaned Step with the |
||
148 | # one we have just added. |
||
149 | orphans = self._orphaned_steps.pop(step.name, []) |
||
150 | for orphan in orphans: |
||
151 | self._stepstate_insert(orphan) |
||
152 | |||
153 | ################################################# |
||
154 | def step_next(self, hint=None): |
||
155 | """Search for steps ready to be ran. |
||
156 | |||
157 | :param str hint: |
||
158 | Place to start searching steps that are now ready to be scheduled |
||
159 | (usually, the last `completed` step). |
||
160 | """ |
||
161 | ready_steps = set() |
||
162 | # Were we given a place to start looking? |
||
163 | if hint: |
||
164 | hint_step = self.step_states[hint] |
||
165 | |||
166 | # Check that this is completed |
||
167 | if not hint_step.is_completed: |
||
168 | # FIXME: This should be an error |
||
0 ignored issues
–
show
|
|||
169 | return set() |
||
170 | |||
171 | for child in hint_step.children: |
||
172 | if child.status is StepStateStatus.ready: |
||
173 | ready_steps.add(child) |
||
174 | |||
175 | else: |
||
176 | # Walk the whole tree collecting ready StepState |
||
177 | for child in self._init_step.children: |
||
178 | if child.status is StepStateStatus.ready: |
||
179 | ready_steps.add(child) |
||
180 | else: |
||
181 | child_name = child.step.name |
||
182 | ready_steps |= self.step_next(child_name) |
||
183 | |||
184 | return ready_steps |
||
185 | |||
186 | def _stepstate_insert(self, step_state): |
||
0 ignored issues
–
show
This method should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
187 | if not step_state.step.requires: |
||
188 | self.step_states[step_state.name] = step_state |
||
189 | return True |
||
190 | |||
191 | elif all([required in self.step_states |
||
192 | for required in step_state.step.requires.keys()]): |
||
193 | # All the required steps are defined, update their children set and |
||
194 | # record them in this step_state's parents set |
||
195 | for required in step_state.step.requires.keys(): |
||
196 | self.step_states[required].children.add(step_state) |
||
197 | step_state.parents.add(self.step_states[required]) |
||
198 | |||
199 | self.step_states[step_state.name] = step_state |
||
200 | return True |
||
201 | |||
202 | else: |
||
203 | # We failed to insert this step_state |
||
204 | # Parent steps are missing, set it as orphaned |
||
205 | for required in step_state.step.requires.keys(): |
||
206 | if required not in self.step_states: |
||
207 | self._orphaned_steps.setdefault( |
||
208 | required, set() |
||
209 | ).add(step_state) |
||
210 | return False |
||
211 | |||
212 | |||
213 | class StepState(object): |
||
0 ignored issues
–
show
This class should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
214 | |||
215 | __slots__ = ('status', |
||
216 | 'step', |
||
217 | 'input', |
||
218 | 'output', |
||
219 | 'children', |
||
220 | 'parents', |
||
221 | 'history', |
||
222 | '__weakref__') |
||
223 | |||
224 | def __init__(self, step, context, |
||
225 | status=StepStateStatus.pending): |
||
226 | |||
227 | if not isinstance(status, StepStateStatus): |
||
228 | status = getattr(StepStateStatus, status) |
||
229 | |||
230 | self.step = step |
||
231 | self.status = status |
||
232 | self.input = None |
||
233 | self.output = None |
||
234 | self.children = weakref.WeakSet() |
||
235 | self.parents = weakref.WeakSet() |
||
236 | self.history = collections.deque() |
||
237 | self.history.append( |
||
238 | (status, context) |
||
239 | ) |
||
240 | |||
241 | def __repr__(self): |
||
242 | return 'StepState({name}:{status})'.format( |
||
243 | name=self.step.name, status=self.status.name |
||
244 | ) |
||
245 | |||
246 | @property |
||
247 | def name(self): |
||
0 ignored issues
–
show
This method should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
248 | return self.step.name |
||
249 | |||
250 | @property |
||
251 | def is_completed(self): |
||
252 | """`True` if the status is either `succeeded`, `failed` or `skipped`. |
||
253 | """ |
||
254 | return self.status.means(StepStateStatus.completed) |
||
255 | |||
256 | def _prepare(self): |
||
257 | """Prepare the input from the step input template and the parents data. |
||
258 | """ |
||
259 | assert self.status is StepStateStatus.ready |
||
260 | context = self._build_context() |
||
261 | render = self.step.prepare(context) |
||
262 | return render |
||
263 | |||
264 | def _build_context(self): |
||
0 ignored issues
–
show
This method should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
265 | context = {} |
||
266 | for parent in self.parents: |
||
267 | if parent.name is INIT_STEP: |
||
268 | context.update({'__input__': parent.output}) |
||
269 | else: |
||
270 | context.update({parent.name: parent.output}) |
||
271 | return context |
||
272 | |||
273 | def _record(self, output): |
||
274 | """Invoke the step rendering of the results.""" |
||
275 | attrs = self.step.render(output) |
||
276 | self.output = attrs |
||
277 | |||
278 | def check_requirements(self, context): |
||
279 | """`True` if the Step is ready to be evaluated. |
||
280 | |||
281 | This means the step hasn't ran yet and all its parents are completed. |
||
282 | """ |
||
283 | if self.status is StepStateStatus.ready: |
||
284 | return |
||
285 | # You cannot be ready if you are already completed/aborted |
||
286 | if self.status is not StepStateStatus.pending: |
||
287 | return |
||
288 | |||
289 | # Check that all our parents are completed per the step's requirements. |
||
290 | _LOGGER.info('Step %r requirements %r', self, self.step.requires) |
||
291 | |||
292 | ready = True |
||
293 | for parent in self.parents: |
||
294 | if parent.name is INIT_STEP: |
||
295 | continue |
||
296 | assert parent.name in self.step.requires |
||
297 | |||
298 | if not parent.is_completed: |
||
299 | ready = False |
||
300 | continue |
||
301 | |||
302 | req_status = self.step.requires[parent.name] |
||
303 | _LOGGER.info('Checking parent %r meets requirement %r', |
||
304 | parent, req_status) |
||
305 | |||
306 | if not parent.status.means(req_status): |
||
307 | self.update('skipped', context) |
||
308 | break |
||
309 | |||
310 | else: |
||
311 | if ready: |
||
312 | self.update('ready', context) |
||
313 | |||
314 | def update(self, new_status, context, new_output=None): |
||
0 ignored issues
–
show
This method should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
315 | if not isinstance(new_status, StepStateStatus): |
||
316 | new_status = getattr(StepStateStatus, new_status) |
||
317 | |||
318 | _LOGGER.info('Updating step %r status: %s -> %s', |
||
319 | self.name, self.status.name, new_status.name) |
||
320 | self.status = new_status |
||
321 | |||
322 | if self.status is StepStateStatus.ready: |
||
323 | self.input = self._prepare() |
||
324 | elif self.status is StepStateStatus.running: |
||
325 | pass |
||
326 | elif self.is_completed: |
||
327 | self._record(new_output) |
||
328 | for child in self.children: |
||
329 | child.check_requirements(context) |
||
330 | elif self.status is StepStateStatus.aborted: |
||
331 | # The workflow will abort, nothing else to do |
||
332 | pass |
||
333 | else: |
||
334 | # FIXME: cleanup |
||
0 ignored issues
–
show
|
|||
335 | raise Exception('Invalid update') |
||
336 | |||
337 | # Record change in history |
||
338 | self.history.append((self.status, context)) |
||
339 | |||
340 | def run(self): |
||
0 ignored issues
–
show
This method should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions. ![]() |
|||
341 | return self.step.run(self.input) |
||
342 |
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.