langfun 0.1.2.dev202509160805__py3-none-any.whl → 0.1.2.dev202509180804__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.
Potentially problematic release.
This version of langfun might be problematic. Click here for more details.
- langfun/env/__init__.py +39 -0
- langfun/env/base_environment.py +491 -0
- langfun/env/base_feature.py +165 -0
- langfun/env/base_sandbox.py +458 -0
- langfun/env/base_test.py +795 -0
- langfun/env/interface.py +843 -0
- langfun/env/interface_test.py +43 -0
- langfun/env/load_balancers.py +59 -0
- langfun/env/load_balancers_test.py +157 -0
- {langfun-0.1.2.dev202509160805.dist-info → langfun-0.1.2.dev202509180804.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202509160805.dist-info → langfun-0.1.2.dev202509180804.dist-info}/RECORD +14 -5
- {langfun-0.1.2.dev202509160805.dist-info → langfun-0.1.2.dev202509180804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202509160805.dist-info → langfun-0.1.2.dev202509180804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202509160805.dist-info → langfun-0.1.2.dev202509180804.dist-info}/top_level.txt +0 -0
langfun/env/interface.py
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Interfaces for environments, sandboxes and features."""
|
|
15
|
+
|
|
16
|
+
import abc
|
|
17
|
+
import contextlib
|
|
18
|
+
import dataclasses
|
|
19
|
+
import os
|
|
20
|
+
from typing import Annotated, Any, Callable, ContextManager, ClassVar, Iterator, Optional
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
import pyglove as pg
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# Environemnt identifiers.
|
|
27
|
+
#
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass(frozen=True)
|
|
31
|
+
class EnvironmentId:
|
|
32
|
+
"""Identifier for an environment."""
|
|
33
|
+
environment_id: str
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
return self.environment_id
|
|
37
|
+
|
|
38
|
+
def working_dir(self, root_dir: str | None) -> str | None:
|
|
39
|
+
"""Returns the download directory for the service."""
|
|
40
|
+
if root_dir is None:
|
|
41
|
+
return None
|
|
42
|
+
return os.path.join(root_dir, _make_path_compatible(self.environment_id))
|
|
43
|
+
|
|
44
|
+
# Enable automatic conversion from str to EnvironmentId.
|
|
45
|
+
pg.typing.register_converter(str, EnvironmentId, EnvironmentId)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass(frozen=True)
|
|
49
|
+
class SandboxId:
|
|
50
|
+
"""Identifier for a sandbox."""
|
|
51
|
+
environment_id: EnvironmentId
|
|
52
|
+
sandbox_id: str
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return f'{self.environment_id}/{self.sandbox_id}'
|
|
56
|
+
|
|
57
|
+
def working_dir(self, root_dir: str | None) -> str | None:
|
|
58
|
+
"""Returns the download directory for the sandbox."""
|
|
59
|
+
if root_dir is None:
|
|
60
|
+
return None
|
|
61
|
+
return os.path.join(
|
|
62
|
+
self.environment_id.working_dir(root_dir),
|
|
63
|
+
_make_path_compatible(self.sandbox_id)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _make_path_compatible(id_str: str) -> str:
|
|
68
|
+
"""Makes a path compatible with CNS."""
|
|
69
|
+
return id_str.translate(
|
|
70
|
+
str.maketrans({
|
|
71
|
+
'@': '_',
|
|
72
|
+
':': '_',
|
|
73
|
+
'#': '_',
|
|
74
|
+
' ': '',
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# Environment errors.
|
|
81
|
+
#
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class EnvironmentError(RuntimeError): # pylint: disable=redefined-builtin
|
|
85
|
+
"""Base class for environment errors."""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
message: str,
|
|
90
|
+
*args,
|
|
91
|
+
environment: 'Environment',
|
|
92
|
+
**kwargs
|
|
93
|
+
) -> None:
|
|
94
|
+
self.environment = environment
|
|
95
|
+
super().__init__(f'[{environment.id}] {message}.', *args, **kwargs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class EnvironmentOutageError(EnvironmentError):
|
|
99
|
+
"""Error that indicates environment is offline."""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
message: str | None = None,
|
|
104
|
+
*args,
|
|
105
|
+
offline_duration: float,
|
|
106
|
+
**kwargs
|
|
107
|
+
):
|
|
108
|
+
self.offline_duration = offline_duration
|
|
109
|
+
super().__init__(
|
|
110
|
+
message or f'Environment is offline for {offline_duration} seconds.',
|
|
111
|
+
*args,
|
|
112
|
+
**kwargs
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class EnvironmentOverloadError(EnvironmentError):
|
|
117
|
+
"""Error that indicates environment is overloaded."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
message: str | None = None,
|
|
122
|
+
*args,
|
|
123
|
+
**kwargs
|
|
124
|
+
):
|
|
125
|
+
super().__init__(
|
|
126
|
+
message or 'All sandboxes in the pool are either busy or dead.',
|
|
127
|
+
*args, **kwargs
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class SandboxError(RuntimeError):
|
|
132
|
+
"""Base class for sandbox errors."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
message: str,
|
|
137
|
+
*args,
|
|
138
|
+
sandbox: 'Sandbox',
|
|
139
|
+
**kwargs
|
|
140
|
+
) -> None:
|
|
141
|
+
self.sandbox = sandbox
|
|
142
|
+
super().__init__(f'[{sandbox.id}] {message}.', *args, **kwargs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SandboxStateError(SandboxError):
|
|
146
|
+
"""Error that indicates sandbox is in an unexpected state.
|
|
147
|
+
|
|
148
|
+
This error is raised when the sandbox is in an unexpected state and cannot
|
|
149
|
+
be recovered. As a result, the sandbox will be shutdown and user session
|
|
150
|
+
will be terminated.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
message: str | None = None,
|
|
156
|
+
*args,
|
|
157
|
+
code: str | None = None,
|
|
158
|
+
**kwargs
|
|
159
|
+
):
|
|
160
|
+
default_message = 'Sandbox is in an unexpected state'
|
|
161
|
+
if code is not None:
|
|
162
|
+
default_message = (
|
|
163
|
+
f'Sandbox is in an unexpected state after executing code: {code!r}'
|
|
164
|
+
)
|
|
165
|
+
super().__init__(message or default_message, *args, **kwargs)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#
|
|
169
|
+
# Event handler.
|
|
170
|
+
#
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class EnvironmentEventHandler:
|
|
174
|
+
"""Base class for environment event handlers."""
|
|
175
|
+
|
|
176
|
+
def on_environment_start(
|
|
177
|
+
self,
|
|
178
|
+
environment: 'Environment',
|
|
179
|
+
error: Exception | None
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Called when the environment is started."""
|
|
182
|
+
|
|
183
|
+
def on_environment_shutdown(
|
|
184
|
+
self,
|
|
185
|
+
environment: 'Environment',
|
|
186
|
+
error: Exception | None
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Called when the environment is shutdown."""
|
|
189
|
+
|
|
190
|
+
def on_sandbox_start(
|
|
191
|
+
self,
|
|
192
|
+
environment: 'Environment',
|
|
193
|
+
sandbox: 'Sandbox',
|
|
194
|
+
error: Exception | None
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Called when a sandbox is started."""
|
|
197
|
+
|
|
198
|
+
def on_sandbox_shutdown(
|
|
199
|
+
self,
|
|
200
|
+
environment: 'Environment',
|
|
201
|
+
sandbox: 'Sandbox',
|
|
202
|
+
error: Exception | None
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Called when a sandbox is shutdown."""
|
|
205
|
+
|
|
206
|
+
def on_sandbox_feature_setup(
|
|
207
|
+
self,
|
|
208
|
+
environment: 'Environment',
|
|
209
|
+
sandbox: 'Sandbox',
|
|
210
|
+
feature: 'Feature',
|
|
211
|
+
error: Exception | None
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Called when a sandbox feature is setup."""
|
|
214
|
+
|
|
215
|
+
def on_sandbox_feature_teardown(
|
|
216
|
+
self,
|
|
217
|
+
environment: 'Environment',
|
|
218
|
+
sandbox: 'Sandbox',
|
|
219
|
+
feature: 'Feature',
|
|
220
|
+
error: Exception | None
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Called when a sandbox feature is teardown."""
|
|
223
|
+
|
|
224
|
+
def on_sandbox_feature_housekeep(
|
|
225
|
+
self,
|
|
226
|
+
environment: 'Environment',
|
|
227
|
+
sandbox: 'Sandbox',
|
|
228
|
+
feature: 'Feature',
|
|
229
|
+
error: Exception | None
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Called when a sandbox feature is housekeeping."""
|
|
232
|
+
|
|
233
|
+
def on_sandbox_session_start(
|
|
234
|
+
self,
|
|
235
|
+
environment: 'Environment',
|
|
236
|
+
sandbox: 'Sandbox',
|
|
237
|
+
session_id: str,
|
|
238
|
+
error: Exception | None
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Called when a sandbox session starts."""
|
|
241
|
+
|
|
242
|
+
def on_sandbox_session_activity(
|
|
243
|
+
self,
|
|
244
|
+
environment: 'Environment',
|
|
245
|
+
sandbox: 'Sandbox',
|
|
246
|
+
session_id: str,
|
|
247
|
+
feature: Optional['Feature'],
|
|
248
|
+
error: Exception | None,
|
|
249
|
+
**kwargs
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Called when a sandbox activity is performed."""
|
|
252
|
+
|
|
253
|
+
def on_sandbox_session_end(
|
|
254
|
+
self,
|
|
255
|
+
environment: 'Environment',
|
|
256
|
+
sandbox: 'Sandbox',
|
|
257
|
+
session_id: str,
|
|
258
|
+
error: Exception | None
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Called when a sandbox session ends."""
|
|
261
|
+
|
|
262
|
+
def on_sandbox_ping(
|
|
263
|
+
self,
|
|
264
|
+
environment: 'Environment',
|
|
265
|
+
sandbox: 'Sandbox',
|
|
266
|
+
error: Exception | None
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Called when a sandbox is pinged."""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
#
|
|
272
|
+
# Interface for sandbox-based environment.
|
|
273
|
+
#
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class Environment(pg.Object):
|
|
277
|
+
"""Base class for an environment."""
|
|
278
|
+
|
|
279
|
+
features: Annotated[
|
|
280
|
+
dict[str, 'Feature'],
|
|
281
|
+
'Features to be exposed by the environment.'
|
|
282
|
+
] = {}
|
|
283
|
+
|
|
284
|
+
event_handler: Annotated[
|
|
285
|
+
EnvironmentEventHandler | None,
|
|
286
|
+
(
|
|
287
|
+
'User handler for the environment events.'
|
|
288
|
+
)
|
|
289
|
+
] = None
|
|
290
|
+
|
|
291
|
+
_ENV_STACK: Annotated[
|
|
292
|
+
ClassVar[list['Environment']],
|
|
293
|
+
'Recording the environments stacked through context managers.'
|
|
294
|
+
] = []
|
|
295
|
+
|
|
296
|
+
#
|
|
297
|
+
# Subclasses must implement:
|
|
298
|
+
#
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
@abc.abstractmethod
|
|
302
|
+
def id(self) -> EnvironmentId:
|
|
303
|
+
"""Returns the identifier for the environment."""
|
|
304
|
+
|
|
305
|
+
@abc.abstractmethod
|
|
306
|
+
def stats(self) -> dict[str, Any]:
|
|
307
|
+
"""Returns the stats of the environment."""
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
@abc.abstractmethod
|
|
311
|
+
def is_alive(self) -> bool:
|
|
312
|
+
"""Returns True if the environment is alive."""
|
|
313
|
+
|
|
314
|
+
@abc.abstractmethod
|
|
315
|
+
def start(self) -> None:
|
|
316
|
+
"""Starts the environment.
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
EnvironmentError: If the environment is not available.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
@abc.abstractmethod
|
|
323
|
+
def shutdown(self) -> None:
|
|
324
|
+
"""Shuts down the environment.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
EnvironmentError: If the environment is not available.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
@abc.abstractmethod
|
|
331
|
+
def acquire(self) -> 'Sandbox':
|
|
332
|
+
"""Acquires a free sandbox from the environment.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
A free sandbox from the environment.
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
EnvironmentOutageError: If the environment is out of service.
|
|
339
|
+
EnvironmentOverloadError: If the environment is overloaded.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
#
|
|
343
|
+
# Environment lifecycle.
|
|
344
|
+
#
|
|
345
|
+
|
|
346
|
+
def __enter__(self) -> 'Environment':
|
|
347
|
+
"""Enters the environment and sets it as the current environment."""
|
|
348
|
+
self.start()
|
|
349
|
+
Environment._ENV_STACK.append(self)
|
|
350
|
+
return self
|
|
351
|
+
|
|
352
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
353
|
+
"""Exits the environment and reset the current environment."""
|
|
354
|
+
assert Environment._ENV_STACK
|
|
355
|
+
Environment._ENV_STACK.pop()
|
|
356
|
+
self.shutdown()
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def current(cls) -> Optional['Environment']:
|
|
360
|
+
"""Returns the current environment."""
|
|
361
|
+
if not Environment._ENV_STACK:
|
|
362
|
+
return None
|
|
363
|
+
return Environment._ENV_STACK[-1]
|
|
364
|
+
|
|
365
|
+
#
|
|
366
|
+
# Environment operations.
|
|
367
|
+
#
|
|
368
|
+
|
|
369
|
+
def sandbox(
|
|
370
|
+
self,
|
|
371
|
+
session_id: str | None = None,
|
|
372
|
+
) -> ContextManager['Sandbox']:
|
|
373
|
+
"""Gets a sandbox from the environment and starts a new user session."""
|
|
374
|
+
return self.acquire().new_session(session_id)
|
|
375
|
+
|
|
376
|
+
def __getattr__(self, name: str) -> Any:
|
|
377
|
+
"""Gets a feature from a free sandbox from the environment.
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
```
|
|
381
|
+
with XboxEnvironment(
|
|
382
|
+
features={'selenium': SeleniumFeature()}
|
|
383
|
+
) as env:
|
|
384
|
+
driver = env.selenium.get_driver()
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
name: The name of the feature.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
A feature from a free sandbox from the environment.
|
|
392
|
+
"""
|
|
393
|
+
if name in self.features:
|
|
394
|
+
return self.acquire().features[name]
|
|
395
|
+
raise AttributeError(name)
|
|
396
|
+
|
|
397
|
+
#
|
|
398
|
+
# Event handlers subclasses can override.
|
|
399
|
+
#
|
|
400
|
+
|
|
401
|
+
def on_start(self, error: Exception | None = None) -> None:
|
|
402
|
+
"""Called when the environment is started."""
|
|
403
|
+
if self.event_handler:
|
|
404
|
+
self.event_handler.on_environment_start(self, error)
|
|
405
|
+
|
|
406
|
+
def on_shutdown(self, error: Exception | None = None) -> None:
|
|
407
|
+
"""Called when the environment is shutdown."""
|
|
408
|
+
if self.event_handler:
|
|
409
|
+
self.event_handler.on_environment_shutdown(self, error)
|
|
410
|
+
|
|
411
|
+
def on_sandbox_start(
|
|
412
|
+
self,
|
|
413
|
+
sandbox: 'Sandbox',
|
|
414
|
+
error: Exception | None = None
|
|
415
|
+
) -> None:
|
|
416
|
+
"""Called when a sandbox is started."""
|
|
417
|
+
if self.event_handler:
|
|
418
|
+
self.event_handler.on_sandbox_start(self, sandbox, error)
|
|
419
|
+
|
|
420
|
+
def on_sandbox_shutdown(
|
|
421
|
+
self,
|
|
422
|
+
sandbox: 'Sandbox',
|
|
423
|
+
error: Exception | None = None
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Called when a sandbox is shutdown."""
|
|
426
|
+
if self.event_handler:
|
|
427
|
+
self.event_handler.on_sandbox_shutdown(self, sandbox, error)
|
|
428
|
+
|
|
429
|
+
def on_sandbox_feature_setup(
|
|
430
|
+
self,
|
|
431
|
+
sandbox: 'Sandbox',
|
|
432
|
+
feature: 'Feature',
|
|
433
|
+
error: Exception | None = None
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Called when a sandbox feature is setup."""
|
|
436
|
+
if self.event_handler:
|
|
437
|
+
self.event_handler.on_sandbox_feature_setup(self, sandbox, feature, error)
|
|
438
|
+
|
|
439
|
+
def on_sandbox_feature_teardown(
|
|
440
|
+
self,
|
|
441
|
+
sandbox: 'Sandbox',
|
|
442
|
+
feature: 'Feature',
|
|
443
|
+
error: Exception | None = None
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Called when a sandbox feature is teardown."""
|
|
446
|
+
if self.event_handler:
|
|
447
|
+
self.event_handler.on_sandbox_feature_teardown(
|
|
448
|
+
self, sandbox, feature, error
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def on_sandbox_feature_housekeep(
|
|
452
|
+
self,
|
|
453
|
+
sandbox: 'Sandbox',
|
|
454
|
+
feature: 'Feature',
|
|
455
|
+
error: Exception | None = None
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Called when a sandbox feature is housekeeping."""
|
|
458
|
+
if self.event_handler:
|
|
459
|
+
self.event_handler.on_sandbox_feature_housekeep(
|
|
460
|
+
self, sandbox, feature, error
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def on_sandbox_session_start(
|
|
464
|
+
self,
|
|
465
|
+
sandbox: 'Sandbox',
|
|
466
|
+
session_id: str,
|
|
467
|
+
error: Exception | None = None
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Called when a sandbox session starts."""
|
|
470
|
+
if self.event_handler:
|
|
471
|
+
self.event_handler.on_sandbox_session_start(
|
|
472
|
+
self, sandbox, session_id, error
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def on_sandbox_session_activity(
|
|
476
|
+
self,
|
|
477
|
+
sandbox: 'Sandbox',
|
|
478
|
+
session_id: str,
|
|
479
|
+
feature: Optional['Feature'] = None,
|
|
480
|
+
error: Exception | None = None,
|
|
481
|
+
**kwargs
|
|
482
|
+
) -> None:
|
|
483
|
+
"""Called when a sandbox activity is performed."""
|
|
484
|
+
if self.event_handler:
|
|
485
|
+
self.event_handler.on_sandbox_session_activity(
|
|
486
|
+
self, sandbox, feature, session_id, error, **kwargs
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def on_sandbox_session_end(
|
|
490
|
+
self,
|
|
491
|
+
sandbox: 'Sandbox',
|
|
492
|
+
session_id: str,
|
|
493
|
+
error: Exception | None = None
|
|
494
|
+
) -> None:
|
|
495
|
+
"""Called when a sandbox session ends."""
|
|
496
|
+
if self.event_handler:
|
|
497
|
+
self.event_handler.on_sandbox_session_end(
|
|
498
|
+
self, sandbox, session_id, error
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def on_sandbox_ping(
|
|
502
|
+
self,
|
|
503
|
+
sandbox: 'Sandbox',
|
|
504
|
+
error: Exception | None = None
|
|
505
|
+
) -> None:
|
|
506
|
+
"""Called when a sandbox is pinged."""
|
|
507
|
+
if self.event_handler:
|
|
508
|
+
self.event_handler.on_sandbox_ping(self, sandbox, error)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class Sandbox(pg.Object):
|
|
512
|
+
"""Interface for sandboxes."""
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
@abc.abstractmethod
|
|
516
|
+
def id(self) -> SandboxId:
|
|
517
|
+
"""Returns the identifier for the sandbox."""
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
@abc.abstractmethod
|
|
521
|
+
def environment(self) -> Environment:
|
|
522
|
+
"""Returns the environment for the sandbox."""
|
|
523
|
+
|
|
524
|
+
@property
|
|
525
|
+
@abc.abstractmethod
|
|
526
|
+
def features(self) -> dict[str, 'Feature']:
|
|
527
|
+
"""Returns the features in the sandbox."""
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
@abc.abstractmethod
|
|
531
|
+
def is_alive(self) -> bool:
|
|
532
|
+
"""Returns True if the sandbox is alive."""
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
@abc.abstractmethod
|
|
536
|
+
def is_busy(self) -> bool:
|
|
537
|
+
"""Returns whether the sandbox is busy."""
|
|
538
|
+
|
|
539
|
+
@abc.abstractmethod
|
|
540
|
+
def set_pending(self) -> None:
|
|
541
|
+
"""Marks the sandbox pending after acquisition but before ready for use."""
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
@abc.abstractmethod
|
|
545
|
+
def is_pending(self) -> bool:
|
|
546
|
+
"""Returns whether the sandbox is pending."""
|
|
547
|
+
|
|
548
|
+
@abc.abstractmethod
|
|
549
|
+
def start(self) -> None:
|
|
550
|
+
"""Starts the sandbox.
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
SandboxStateError: If the sandbox is in a bad state.
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
@abc.abstractmethod
|
|
557
|
+
def shutdown(self) -> None:
|
|
558
|
+
"""Shuts down the sandbox.
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
SandboxStateError: If the sandbox is in a bad state.
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
@abc.abstractmethod
|
|
565
|
+
def ping(self) -> None:
|
|
566
|
+
"""Ping the sandbox to check if it is alive.
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
SandboxStateError: If the sandbox is in a bad state.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
@abc.abstractmethod
|
|
573
|
+
def start_session(self, session_id: str) -> None:
|
|
574
|
+
"""Begins a user session with the sandbox.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
session_id: The identifier for the user session.
|
|
578
|
+
|
|
579
|
+
Raises:
|
|
580
|
+
SandboxError: If the sandbox already has a user session
|
|
581
|
+
or the session cannot be started.
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
@abc.abstractmethod
|
|
585
|
+
def end_session(self) -> None:
|
|
586
|
+
"""Ends the user session with the sandbox."""
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
@abc.abstractmethod
|
|
590
|
+
def session_id(self) -> str | None:
|
|
591
|
+
"""Returns the current user session identifier."""
|
|
592
|
+
|
|
593
|
+
#
|
|
594
|
+
# API related to a user session.
|
|
595
|
+
# A sandbox could be reused across different user sessions.
|
|
596
|
+
# A user session is a sequence of stateful interactions with the sandbox,
|
|
597
|
+
# Across different sessions the sandbox are considered stateless.
|
|
598
|
+
#
|
|
599
|
+
|
|
600
|
+
@contextlib.contextmanager
|
|
601
|
+
def new_session(self, session_id: str | None = None) -> Iterator['Sandbox']:
|
|
602
|
+
"""Context manager for obtaining a sandbox for a user session."""
|
|
603
|
+
if session_id is None:
|
|
604
|
+
session_id = f'session-{uuid.uuid4().hex[:7]}'
|
|
605
|
+
self.start_session(session_id)
|
|
606
|
+
try:
|
|
607
|
+
yield self
|
|
608
|
+
finally:
|
|
609
|
+
self.end_session()
|
|
610
|
+
|
|
611
|
+
def __getattr__(self, name: str) -> Any:
|
|
612
|
+
"""Gets a feature from current sandbox.
|
|
613
|
+
|
|
614
|
+
Example:
|
|
615
|
+
```
|
|
616
|
+
with MyEnvironment(
|
|
617
|
+
features={'feature1': Feature1()}
|
|
618
|
+
) as env:
|
|
619
|
+
with env.sandbox('session1') as sb:
|
|
620
|
+
driver = sb.feature1.feature_method()
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
name: The name of the feature.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
A feature from current sandbox.
|
|
628
|
+
"""
|
|
629
|
+
if name in self.features:
|
|
630
|
+
return self.features[name]
|
|
631
|
+
raise AttributeError(name)
|
|
632
|
+
|
|
633
|
+
#
|
|
634
|
+
# Event handlers subclasses can override.
|
|
635
|
+
#
|
|
636
|
+
|
|
637
|
+
def on_start(self, error: Exception | None = None) -> None:
|
|
638
|
+
"""Called when the sandbox is started."""
|
|
639
|
+
self.environment.on_sandbox_start(self, error)
|
|
640
|
+
|
|
641
|
+
def on_shutdown(self, error: Exception | None = None) -> None:
|
|
642
|
+
"""Called when the sandbox is shutdown."""
|
|
643
|
+
self.environment.on_sandbox_shutdown(self, error)
|
|
644
|
+
|
|
645
|
+
def on_ping(self, error: Exception | None = None) -> None:
|
|
646
|
+
"""Called when the sandbox is pinged."""
|
|
647
|
+
self.environment.on_sandbox_ping(self, error)
|
|
648
|
+
|
|
649
|
+
def on_feature_setup(
|
|
650
|
+
self,
|
|
651
|
+
feature: 'Feature',
|
|
652
|
+
error: Exception | None = None
|
|
653
|
+
) -> None:
|
|
654
|
+
"""Called when a feature is setup."""
|
|
655
|
+
self.environment.on_sandbox_feature_setup(
|
|
656
|
+
self, feature, error
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def on_feature_teardown(
|
|
660
|
+
self,
|
|
661
|
+
feature: 'Feature',
|
|
662
|
+
error: Exception | None = None
|
|
663
|
+
) -> None:
|
|
664
|
+
"""Called when a feature is teardown."""
|
|
665
|
+
self.environment.on_sandbox_feature_teardown(
|
|
666
|
+
self, feature, error
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def on_feature_housekeep(
|
|
670
|
+
self,
|
|
671
|
+
feature: 'Feature',
|
|
672
|
+
error: Exception | None = None
|
|
673
|
+
) -> None:
|
|
674
|
+
"""Called when a feature is housekeeping."""
|
|
675
|
+
self.environment.on_sandbox_feature_housekeep(
|
|
676
|
+
self, feature, error
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def on_session_start(
|
|
680
|
+
self,
|
|
681
|
+
session_id: str,
|
|
682
|
+
error: Exception | None = None
|
|
683
|
+
) -> None:
|
|
684
|
+
"""Called when the user session starts."""
|
|
685
|
+
self.environment.on_sandbox_session_start(self, session_id, error)
|
|
686
|
+
|
|
687
|
+
def on_session_activity(
|
|
688
|
+
self,
|
|
689
|
+
session_id: str,
|
|
690
|
+
feature: Optional['Feature'] = None,
|
|
691
|
+
error: Exception | None = None,
|
|
692
|
+
**kwargs
|
|
693
|
+
) -> None:
|
|
694
|
+
"""Called when a sandbox activity is performed."""
|
|
695
|
+
self.environment.on_sandbox_session_activity(
|
|
696
|
+
sandbox=self,
|
|
697
|
+
feature=feature,
|
|
698
|
+
session_id=session_id,
|
|
699
|
+
error=error,
|
|
700
|
+
**kwargs
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
def on_session_end(
|
|
704
|
+
self,
|
|
705
|
+
session_id: str,
|
|
706
|
+
error: Exception | None = None
|
|
707
|
+
) -> None:
|
|
708
|
+
"""Called when the user session ends."""
|
|
709
|
+
self.environment.on_sandbox_session_end(self, session_id, error)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class Feature(pg.Object):
|
|
713
|
+
"""Interface for sandbox features."""
|
|
714
|
+
|
|
715
|
+
@property
|
|
716
|
+
@abc.abstractmethod
|
|
717
|
+
def name(self) -> str:
|
|
718
|
+
"""Name of the feature, which will be used as key to access the feature."""
|
|
719
|
+
|
|
720
|
+
@property
|
|
721
|
+
@abc.abstractmethod
|
|
722
|
+
def sandbox(self) -> Sandbox | None:
|
|
723
|
+
"""Returns the sandbox that the feature is running in."""
|
|
724
|
+
|
|
725
|
+
@abc.abstractmethod
|
|
726
|
+
def setup(self, sandbox: Sandbox) -> None:
|
|
727
|
+
"""Sets up the feature, which is called once when the sandbox is up.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
sandbox: The sandbox that the feature is running in.
|
|
731
|
+
|
|
732
|
+
Raises:
|
|
733
|
+
SandboxStateError: If the sandbox is in a bad state.
|
|
734
|
+
"""
|
|
735
|
+
|
|
736
|
+
@abc.abstractmethod
|
|
737
|
+
def teardown(self) -> None:
|
|
738
|
+
"""Teardowns the feature, which is called once when the sandbox is down.
|
|
739
|
+
|
|
740
|
+
Raises:
|
|
741
|
+
SandboxStateError: If the sandbox is in a bad state.
|
|
742
|
+
"""
|
|
743
|
+
|
|
744
|
+
@abc.abstractmethod
|
|
745
|
+
def setup_session(self, session_id: str) -> None:
|
|
746
|
+
"""Sets up the feature for a user session."""
|
|
747
|
+
|
|
748
|
+
@abc.abstractmethod
|
|
749
|
+
def teardown_session(self, session_id: str) -> None:
|
|
750
|
+
"""Teardowns the feature for a user session."""
|
|
751
|
+
|
|
752
|
+
@abc.abstractmethod
|
|
753
|
+
def housekeep(self) -> None:
|
|
754
|
+
"""Performs housekeeping for the feature.
|
|
755
|
+
|
|
756
|
+
Raises:
|
|
757
|
+
SandboxStateError: If the sandbox is in a bad state.
|
|
758
|
+
"""
|
|
759
|
+
|
|
760
|
+
@property
|
|
761
|
+
@abc.abstractmethod
|
|
762
|
+
def housekeep_interval(self) -> int | None:
|
|
763
|
+
"""Returns the interval in seconds for feature housekeeping.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
The interval in seconds for feature housekeeping. If None, the feature
|
|
767
|
+
will not be housekeeping.
|
|
768
|
+
"""
|
|
769
|
+
|
|
770
|
+
@property
|
|
771
|
+
def session_id(self) -> str | None:
|
|
772
|
+
"""Returns the current user session identifier."""
|
|
773
|
+
assert self.sandbox is not None
|
|
774
|
+
return self.sandbox.session_id
|
|
775
|
+
|
|
776
|
+
#
|
|
777
|
+
# Event handlers subclasses can override.
|
|
778
|
+
#
|
|
779
|
+
|
|
780
|
+
def on_setup(
|
|
781
|
+
self,
|
|
782
|
+
error: BaseException | None = None
|
|
783
|
+
) -> None:
|
|
784
|
+
"""Called when the feature is setup."""
|
|
785
|
+
self.sandbox.on_feature_setup(self, error)
|
|
786
|
+
|
|
787
|
+
def on_teardown(
|
|
788
|
+
self,
|
|
789
|
+
error: BaseException | None = None
|
|
790
|
+
) -> None:
|
|
791
|
+
"""Called when the feature is teardown."""
|
|
792
|
+
self.sandbox.on_feature_teardown(self, error)
|
|
793
|
+
|
|
794
|
+
def on_housekeep(
|
|
795
|
+
self,
|
|
796
|
+
error: BaseException | None = None
|
|
797
|
+
) -> None:
|
|
798
|
+
"""Called when the feature has done housekeeping."""
|
|
799
|
+
self.sandbox.on_feature_housekeep(self, error)
|
|
800
|
+
|
|
801
|
+
def on_session_setup(
|
|
802
|
+
self,
|
|
803
|
+
session_id: str,
|
|
804
|
+
error: BaseException | None = None,
|
|
805
|
+
) -> None:
|
|
806
|
+
"""Called when the feature is setup for a user session."""
|
|
807
|
+
|
|
808
|
+
def on_session_activity(
|
|
809
|
+
self,
|
|
810
|
+
session_id: str,
|
|
811
|
+
error: Exception | None,
|
|
812
|
+
**kwargs
|
|
813
|
+
) -> None:
|
|
814
|
+
"""Called when a sandbox activity is performed."""
|
|
815
|
+
self.sandbox.on_session_activity(
|
|
816
|
+
feature=self, session_id=session_id, error=error, **kwargs
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
def on_session_teardown(
|
|
820
|
+
self,
|
|
821
|
+
session_id: str,
|
|
822
|
+
error: BaseException | None = None,
|
|
823
|
+
) -> None:
|
|
824
|
+
"""Called when the feature is teardown for a user session."""
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def call_with_event(
|
|
828
|
+
action: Callable[[], None],
|
|
829
|
+
event_handler: Callable[..., None],
|
|
830
|
+
action_kwargs: dict[str, Any] | None = None,
|
|
831
|
+
event_handler_kwargs: dict[str, Any] | None = None,
|
|
832
|
+
) -> None:
|
|
833
|
+
"""Triggers an event handler."""
|
|
834
|
+
action_kwargs = action_kwargs or {}
|
|
835
|
+
event_handler_kwargs = event_handler_kwargs or {}
|
|
836
|
+
error = None
|
|
837
|
+
try:
|
|
838
|
+
action(**action_kwargs)
|
|
839
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
840
|
+
error = e
|
|
841
|
+
raise
|
|
842
|
+
finally:
|
|
843
|
+
event_handler(error=error, **event_handler_kwargs)
|