outerbounds 0.3.180rc5__py3-none-any.whl → 0.3.181__py3-none-any.whl

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.
@@ -1,358 +0,0 @@
1
- from typing import List, Tuple, Dict, Union
2
-
3
-
4
- class _dagNode:
5
- def __init__(self, name: str):
6
- self.name = name
7
- self.incoming_nodes: List["_dagNode"] = []
8
- self.outgoing_nodes: List["_dagNode"] = []
9
-
10
- def goto(self, *nodes: "_dagNode"):
11
- for node in nodes:
12
- self.outgoing_nodes.append(node)
13
- node.incoming_nodes.append(self)
14
- return self
15
-
16
- def arrives_from(self, *nodes: "_dagNode"):
17
- for node in nodes:
18
- node.outgoing_nodes.append(self)
19
- self.incoming_nodes.append(node)
20
- return self
21
-
22
- def __repr__(self):
23
- return self.name
24
-
25
- def __str__(self):
26
- return self.name
27
-
28
-
29
- class _capsuleDeployerStateMachine:
30
- def __init__(self):
31
- # -- (your existing setup) --
32
- start_state = _dagNode("start")
33
- fail_state = _dagNode("fail")
34
- success_state = _dagNode("success")
35
- upgrade_state = _dagNode("upgrade")
36
- first_time_create_state = _dagNode("first_time_create")
37
- end_state = _dagNode("end")
38
-
39
- capsule_deploy_api_call = _dagNode("capsule_deploy_api_call")
40
- capsule_deploy_api_call_rejected = _dagNode("capsule_deploy_api_call_rejected")
41
- capsule_worker_pending = _dagNode("capsule_worker_pending")
42
-
43
- capsule_single_worker_ready = _dagNode("capsule_single_worker_ready")
44
- capsule_multiple_workers_ready = _dagNode("capsule_all_workers_ready")
45
- current_deployment_deployed_worker_crashed = _dagNode(
46
- "current_deployment_deployed_worker_crashed"
47
- )
48
- current_deployment_workers_pending_beyond_timeout = _dagNode(
49
- "current_deployment_workers_pending_beyond_timeout"
50
- )
51
-
52
- start_state.goto(first_time_create_state, upgrade_state)
53
-
54
- capsule_deploy_api_call.arrives_from(
55
- first_time_create_state, upgrade_state
56
- ).goto(capsule_deploy_api_call_rejected, capsule_worker_pending)
57
-
58
- capsule_worker_pending.goto(
59
- capsule_single_worker_ready,
60
- capsule_multiple_workers_ready,
61
- current_deployment_deployed_worker_crashed,
62
- current_deployment_workers_pending_beyond_timeout,
63
- )
64
- success_state.arrives_from(
65
- capsule_single_worker_ready, capsule_multiple_workers_ready
66
- ).goto(end_state)
67
- fail_state.arrives_from(
68
- capsule_deploy_api_call_rejected,
69
- current_deployment_deployed_worker_crashed,
70
- current_deployment_workers_pending_beyond_timeout,
71
- ).goto(end_state)
72
-
73
- self._states = [
74
- start_state,
75
- fail_state,
76
- success_state,
77
- upgrade_state,
78
- first_time_create_state,
79
- end_state,
80
- capsule_single_worker_ready,
81
- capsule_multiple_workers_ready,
82
- current_deployment_deployed_worker_crashed,
83
- current_deployment_workers_pending_beyond_timeout,
84
- capsule_deploy_api_call,
85
- capsule_deploy_api_call_rejected,
86
- capsule_worker_pending,
87
- ]
88
-
89
- def get_edges(self) -> List[Tuple["_dagNode", "_dagNode"]]:
90
- """
91
- Returns a list of (src_node, dst_node) tuples for all transitions.
92
- """
93
- edges = []
94
- for node in self._states:
95
- for out in node.outgoing_nodes:
96
- edges.append((node, out))
97
- return edges
98
-
99
- def to_dot(self, graph_name="StateMachine"):
100
- """
101
- Emit a Graphviz DOT description of the state machine.
102
- """
103
- lines = [f"digraph {graph_name} {{"]
104
- # optional: rankdir=LR for left-to-right layout
105
- lines.append(" rankdir=LR;")
106
- for src, dst in self.get_edges():
107
- lines.append(f' "{src}" -> "{dst}";')
108
- lines.append("}")
109
- return "\n".join(lines)
110
-
111
- def adjacency_list(self):
112
- """
113
- Returns a dict mapping each node to list of its outgoing nodes.
114
- """
115
- return {node: list(node.outgoing_nodes) for node in self._states}
116
-
117
- def __str__(self):
118
- # Default to DOT format; you could swap this out for something else
119
- return self.to_dot()
120
-
121
- def to_diagraph(self):
122
- from graphviz import Digraph
123
-
124
- # Create a new Digraph
125
- dot = Digraph(name="StateMachine", format="png")
126
- dot.attr(rankdir="LR") # left-to-right layout
127
-
128
- # Add one edge per transition in your SM
129
- for src, dst in self.get_edges():
130
- # src and dst are _dagNode instances; use their .name (or str(src))
131
- dot.edge(src.name, dst.name)
132
-
133
- # Render to file (e.g. "state_machine.png") and optionally view it:
134
- dot.render("state_machine", view=False)
135
-
136
-
137
- from typing import TypedDict
138
-
139
-
140
- class WorkerStatus(TypedDict):
141
- workerId: str
142
- phase: str
143
- activity: int
144
- activityDataAvailable: bool
145
- version: str
146
-
147
-
148
- from typing import Dict, List, TypedDict
149
-
150
-
151
- class WorkerInfoDict(TypedDict):
152
- # TODO : Check if we need to account for the `Terminating` state
153
- pending: Dict[str, List[WorkerStatus]]
154
- running: Dict[str, List[WorkerStatus]]
155
- crashlooping: Dict[str, List[WorkerStatus]]
156
-
157
-
158
- class CurrentWorkerInfo(TypedDict):
159
- # TODO : Check if we need to account for the `Terminating` state
160
- pending: int
161
- running: int
162
- crashlooping: int
163
-
164
-
165
- class DEPLOYMENT_READY_CONDITIONS:
166
- """
167
- Deployment ready conditions define what is considered a successful completion of a deployment.
168
- This allows users or platform designers to configure the criteria for deployment readiness.
169
-
170
- Reasons for different deployment modes include:
171
- 1) [at_least_one_running] Some endpoints may be deployed ephemerally and are considered ready when at least one instance is running; additional instances are for load management.
172
- 2) [all_running] Operators may require that all replicas are available, running, and only the current deployment version is serving traffic.
173
- 3) [fully_finished] Operators may only care that the minimum number of replicas for the current rollout are present in the cluster.
174
- """
175
-
176
- # `ATLEAST_ONE_RUNNING` implies that atleast one worker of the current deployment instance's version has started running.
177
- ATLEAST_ONE_RUNNING = "at_least_one_running"
178
-
179
- # `ALL_RUNNING` implies that all workers of the current deployment instance's version have started running (i.e. all workers aligning to the minimum number of replicas).
180
- # It doesn't imply that all the workers relating to other deployments have been torn down.
181
- ALL_RUNNING = "all_running"
182
-
183
- # `FULLY_FINISHED` implies that the deployment has the minimum number of replicas and all the workers are related to the current deployment instance's version.
184
- FULLY_FINISHED = "fully_finished"
185
-
186
- @classmethod
187
- def docstring(cls):
188
- return cls.__doc__
189
-
190
- @classmethod
191
- def enums(cls):
192
- return [
193
- cls.ATLEAST_ONE_RUNNING,
194
- cls.ALL_RUNNING,
195
- cls.FULLY_FINISHED,
196
- ]
197
-
198
-
199
- class CapsuleWorkerStatusDict(TypedDict):
200
- at_least_one_pending: bool
201
- at_least_one_running: bool
202
- at_least_one_crashlooping: bool
203
- all_running: bool
204
- fully_finished: bool
205
- none_present: bool
206
- current_info: CurrentWorkerInfo
207
-
208
-
209
- class CapsuleWorkerSemanticStatus(TypedDict):
210
- final_version: str
211
- status: CapsuleWorkerStatusDict
212
- worker_info: WorkerInfoDict
213
-
214
-
215
- def _capsule_worker_status_diff(
216
- current_status: CapsuleWorkerSemanticStatus,
217
- previous_status: Union[CapsuleWorkerSemanticStatus, None],
218
- ) -> List[str]:
219
- """
220
- The goal of this function is to return a status string that will be used to update the user the
221
- change in status of the different capsules.
222
- """
223
- if previous_status is None:
224
- # Check if the current status has pending workers or crashlooping workers
225
- curr = current_status["status"]["current_info"]
226
- version = current_status["final_version"]
227
- changes = []
228
-
229
- if curr["pending"] > 0:
230
- changes.append(f"⏳ {curr['pending']} worker(s) pending")
231
-
232
- if curr["running"] > 0:
233
- changes.append(f"🚀 {curr['running']} worker(s) currently running")
234
-
235
- if curr["crashlooping"] > 0:
236
- changes.append(f"💥 {curr['crashlooping']} worker(s) currently crashlooping")
237
-
238
- return changes
239
-
240
- curr = current_status["status"]["current_info"]
241
- prev = previous_status["status"]["current_info"]
242
- version = current_status["final_version"]
243
-
244
- changes = []
245
-
246
- # Track worker count changes for the target version
247
- pending_diff = curr["pending"] - prev["pending"]
248
- running_diff = curr["running"] - prev["running"]
249
- crash_diff = curr["crashlooping"] - prev["crashlooping"]
250
-
251
- # Worker count changes
252
- if pending_diff > 0:
253
- changes.append(
254
- f"⏳ {pending_diff} new worker(s) pending. Total pending ({curr['pending']})"
255
- )
256
-
257
- if running_diff > 0:
258
- changes.append(
259
- f"🚀 {running_diff} worker(s) started running. Total running ({curr['running']})"
260
- )
261
- elif running_diff < 0:
262
- changes.append(
263
- f"🛑 {abs(running_diff)} worker(s) stopped running. Total running ({curr['running']})"
264
- )
265
-
266
- if crash_diff > 0:
267
- changes.append(
268
- f"💥 {crash_diff} worker(s) started crashlooping. Total crashlooping ({curr['crashlooping']})"
269
- )
270
- elif crash_diff < 0:
271
- changes.append(f"🔧 {abs(crash_diff)} worker(s) recovered from crashlooping")
272
-
273
- # Significant state transitions
274
- if (
275
- not previous_status["status"]["at_least_one_running"]
276
- and current_status["status"]["at_least_one_running"]
277
- ):
278
- changes.append(f"✅ First worker came online")
279
-
280
- if (
281
- not previous_status["status"]["all_running"]
282
- and current_status["status"]["all_running"]
283
- ):
284
- changes.append(f"🎉 All workers are now running")
285
-
286
- if (
287
- not previous_status["status"]["at_least_one_crashlooping"]
288
- and current_status["status"]["at_least_one_crashlooping"]
289
- ):
290
- changes.append(f"⚠️ Worker crash detected")
291
-
292
- # Current state summary
293
-
294
- return changes
295
-
296
-
297
- def _capsule_worker_semantic_status(
298
- workers: List[WorkerStatus], version: str, min_replicas: int
299
- ) -> CapsuleWorkerSemanticStatus:
300
- def _make_version_dict(
301
- _workers: List[WorkerStatus], phase: str
302
- ) -> Dict[str, List[WorkerStatus]]:
303
- xx: Dict[str, List[WorkerStatus]] = {}
304
- for w in _workers:
305
- if w.get("phase") != phase:
306
- continue
307
- if w.get("version") not in xx:
308
- xx[w.get("version")] = []
309
- xx[w.get("version")].append(w)
310
- return xx
311
-
312
- pending_workers = _make_version_dict(workers, "Pending")
313
- running_workers = _make_version_dict(workers, "Running")
314
- crashlooping_workers = _make_version_dict(workers, "CrashLoopBackOff")
315
-
316
- # current_status (formulated basis):
317
- # - atleast one pods are pending for `_end_state_capsule_version`
318
- # - atleast one pod is in Running state for `_end_state_capsule_version` (maybe terminal) [Might require heath-check thing here]
319
- # - alteast one pod is crashlooping for `_end_state_capsule_version` (maybe terminal)
320
- # - all pods are running for `_end_state_capsule_version` that match the minimum number of replicas
321
- # - all pods are running for `_end_state_capsule_version` that match the maximum number of replicas and no other pods of older versions are running
322
- # - no pods relating to `_end_state_capsule_version` are pending/running/crashlooping
323
-
324
- # Helper to count pods for the final version in each state
325
- def count_for_version(workers_dict):
326
- return len(workers_dict.get(version, []))
327
-
328
- status_dict: CapsuleWorkerStatusDict = {
329
- "at_least_one_pending": count_for_version(pending_workers) > 0,
330
- "at_least_one_running": count_for_version(running_workers) > 0,
331
- "at_least_one_crashlooping": count_for_version(crashlooping_workers) > 0,
332
- "none_present": (
333
- count_for_version(running_workers) == 0
334
- and count_for_version(pending_workers) == 0
335
- and count_for_version(crashlooping_workers) == 0
336
- ),
337
- "all_running": count_for_version(running_workers) == min_replicas,
338
- "fully_finished": count_for_version(running_workers) == min_replicas
339
- and len(pending_workers) == 0
340
- and len(crashlooping_workers) == 0,
341
- "current_info": {
342
- "pending": count_for_version(pending_workers),
343
- "running": count_for_version(running_workers),
344
- "crashlooping": count_for_version(crashlooping_workers),
345
- },
346
- }
347
-
348
- worker_info: WorkerInfoDict = {
349
- "pending": pending_workers,
350
- "running": running_workers,
351
- "crashlooping": crashlooping_workers,
352
- }
353
-
354
- return {
355
- "final_version": version,
356
- "status": status_dict,
357
- "worker_info": worker_info,
358
- }