langfun 0.1.2.dev202510300805__py3-none-any.whl → 0.1.2.dev202511010804__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 CHANGED
@@ -24,6 +24,8 @@ from langfun.env.interface import Environment
24
24
  from langfun.env.interface import Sandbox
25
25
  from langfun.env.interface import Feature
26
26
 
27
+ from langfun.env.interface import EventHandler
28
+
27
29
  # Decorators for sandbox/feature methods.
28
30
  from langfun.env.interface import treat_as_sandbox_state_error
29
31
  from langfun.env.interface import log_sandbox_activity
@@ -35,7 +37,7 @@ from langfun.env.base_feature import BaseFeature
35
37
  from langfun.env import load_balancers
36
38
  from langfun.env.load_balancers import LoadBalancer
37
39
 
38
- from langfun.env import event_handlers
39
- EventHandler = event_handlers.EventHandler
40
+ # Import all event handlers.
41
+ from langfun.env.event_handlers import *
40
42
 
41
43
  # Google-internal imports.
@@ -36,7 +36,6 @@ import langfun.core as lf
36
36
  from langfun.env import base_sandbox
37
37
  from langfun.env import interface
38
38
  from langfun.env import load_balancers
39
- from langfun.env.event_handlers import base as event_handler_base
40
39
  import pyglove as pg
41
40
 
42
41
 
@@ -113,12 +112,13 @@ class BaseEnvironment(interface.Environment):
113
112
  )
114
113
  ] = True
115
114
 
116
- event_handlers: Annotated[
117
- list[event_handler_base.EventHandler],
115
+ event_handler: Annotated[
116
+ interface.EventHandler,
118
117
  (
119
118
  'User handler for the environment events.'
119
+ 'By default, the no-op event handler is used.'
120
120
  )
121
- ] = []
121
+ ] = interface.EventHandler()
122
122
 
123
123
  outage_grace_period: Annotated[
124
124
  float,
@@ -169,6 +169,7 @@ class BaseEnvironment(interface.Environment):
169
169
 
170
170
  self._status = self.Status.CREATED
171
171
  self._start_time = None
172
+
172
173
  self._sandbox_pool: dict[str, list[base_sandbox.BaseSandbox]] = (
173
174
  collections.defaultdict(list)
174
175
  )
@@ -555,8 +556,6 @@ class BaseEnvironment(interface.Environment):
555
556
  proactive_session_setup=self.proactive_session_setup,
556
557
  keepalive_interval=self.sandbox_keepalive_interval,
557
558
  )
558
- for handler in self.event_handlers:
559
- sandbox.add_event_handler(handler)
560
559
  sandbox.start()
561
560
  if set_acquired:
562
561
  sandbox.set_acquired()
@@ -738,16 +737,14 @@ class BaseEnvironment(interface.Environment):
738
737
 
739
738
  def on_starting(self) -> None:
740
739
  """Called when the environment is getting started."""
741
- for handler in self.event_handlers:
742
- handler.on_environment_starting(self)
740
+ self.event_handler.on_environment_starting(self)
743
741
 
744
742
  def on_start(
745
743
  self,
746
744
  duration: float, error: BaseException | None = None
747
745
  ) -> None:
748
746
  """Called when the environment is started."""
749
- for handler in self.event_handlers:
750
- handler.on_environment_start(self, duration, error)
747
+ self.event_handler.on_environment_start(self, duration, error)
751
748
 
752
749
  def on_housekeep(
753
750
  self,
@@ -756,16 +753,13 @@ class BaseEnvironment(interface.Environment):
756
753
  **kwargs
757
754
  ) -> None:
758
755
  """Called when the environment finishes a round of housekeeping."""
759
- housekeep_counter = self.housekeep_counter
760
- for handler in self.event_handlers:
761
- handler.on_environment_housekeep(
762
- self, housekeep_counter, duration, error, **kwargs
763
- )
756
+ self.event_handler.on_environment_housekeep(
757
+ self, self.housekeep_counter, duration, error, **kwargs
758
+ )
764
759
 
765
760
  def on_shutting_down(self) -> None:
766
761
  """Called when the environment is shutting down."""
767
- for handler in self.event_handlers:
768
- handler.on_environment_shutting_down(self, self.offline_duration)
762
+ self.event_handler.on_environment_shutting_down(self, self.offline_duration)
769
763
 
770
764
  def on_shutdown(
771
765
  self,
@@ -773,5 +767,4 @@ class BaseEnvironment(interface.Environment):
773
767
  error: BaseException | None = None) -> None:
774
768
  """Called when the environment is shutdown."""
775
769
  lifetime = (time.time() - self.start_time) if self.start_time else 0.0
776
- for handler in self.event_handlers:
777
- handler.on_environment_shutdown(self, duration, lifetime, error)
770
+ self.event_handler.on_environment_shutdown(self, duration, lifetime, error)
@@ -0,0 +1,473 @@
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
+ import time
15
+ import unittest
16
+
17
+ from langfun.env import interface
18
+ from langfun.env import test_utils
19
+
20
+ TestingEnvironment = test_utils.TestingEnvironment
21
+ TestingSandbox = test_utils.TestingSandbox
22
+ TestingFeature = test_utils.TestingFeature
23
+ TestingEventHandler = test_utils.TestingEventHandler
24
+
25
+
26
+ class BaseEnvironmentTests(unittest.TestCase):
27
+
28
+ def test_basics(self):
29
+ env = TestingEnvironment(
30
+ image_ids=['test_image'],
31
+ root_dir='/tmp',
32
+ pool_size=0,
33
+ features={'test_feature': TestingFeature()},
34
+ outage_grace_period=1,
35
+ outage_retry_interval=0,
36
+ )
37
+ self.assertIsNone(interface.Environment.current())
38
+ self.assertEqual(env.image_ids, ['test_image'])
39
+ self.assertFalse(env.supports_dynamic_image_loading)
40
+ self.assertEqual(env.status, interface.Environment.Status.CREATED)
41
+ self.assertFalse(env.is_online)
42
+ self.assertEqual(env.min_pool_size('test_image'), 0)
43
+ self.assertEqual(env.max_pool_size('test_image'), 0)
44
+ self.assertEqual(env.sandbox_pool, {})
45
+ self.assertEqual(env.id, interface.Environment.Id('testing-env'))
46
+ self.assertEqual(env.outage_grace_period, 1)
47
+ self.assertEqual(env.features['test_feature'].name, 'test_feature')
48
+
49
+ self.assertIsNone(env.start_time)
50
+
51
+ with env:
52
+ self.assertEqual(env.status, interface.Environment.Status.ONLINE)
53
+ self.assertIs(interface.Environment.current(), env)
54
+ self.assertTrue(env.is_online)
55
+ self.assertIsNotNone(env.start_time)
56
+ self.assertEqual(env.offline_duration, 0.0)
57
+ self.assertEqual(env.sandbox_pool, {})
58
+ self.assertEqual(env.working_dir, '/tmp/testing-env')
59
+
60
+ with env.sandbox(session_id='session1') as sb:
61
+ self.assertEqual(
62
+ sb.id, interface.Sandbox.Id(
63
+ environment_id=env.id,
64
+ image_id=sb.image_id,
65
+ sandbox_id='0')
66
+ )
67
+ self.assertEqual(sb.session_id, 'session1')
68
+ self.assertEqual(sb.working_dir, '/tmp/testing-env/test_image/0')
69
+ self.assertTrue(sb.is_online)
70
+ self.assertIs(sb.test_feature, sb.features['test_feature'])
71
+ self.assertEqual(
72
+ sb.test_feature.working_dir,
73
+ '/tmp/testing-env/test_image/0/test_feature'
74
+ )
75
+ with self.assertRaises(AttributeError):
76
+ _ = sb.test_feature2
77
+ self.assertFalse(sb.is_online)
78
+
79
+ with self.assertRaisesRegex(
80
+ ValueError, 'Environment .* does not serve image ID .*'
81
+ ):
82
+ env.sandbox('test_image2')
83
+
84
+ with env.test_feature() as feature:
85
+ self.assertIsInstance(feature, TestingFeature)
86
+ self.assertEqual(
87
+ feature.sandbox.status, interface.Sandbox.Status.IN_SESSION
88
+ )
89
+ self.assertTrue(
90
+ feature.sandbox.session_id.startswith('test_feature-session')
91
+ )
92
+
93
+ with self.assertRaises(AttributeError):
94
+ _ = env.test_feature2
95
+
96
+ def test_dynamic_image_loading(self):
97
+ env = TestingEnvironment(
98
+ image_ids=[],
99
+ supports_dynamic_image_loading=True,
100
+ pool_size=0,
101
+ features={'test_feature': TestingFeature()},
102
+ outage_grace_period=1,
103
+ outage_retry_interval=0,
104
+ )
105
+ with env:
106
+ with env.sandbox(image_id='test_image2') as sb:
107
+ self.assertEqual(sb.image_id, 'test_image2')
108
+
109
+ with self.assertRaisesRegex(
110
+ ValueError, 'Environment .* does not have a default image ID.'
111
+ ):
112
+ env.sandbox()
113
+
114
+ def test_dynamic_image_loading_with_pooling(self):
115
+ env = TestingEnvironment(
116
+ image_ids=[],
117
+ supports_dynamic_image_loading=True,
118
+ pool_size=2,
119
+ features={'test_feature': TestingFeature()},
120
+ outage_grace_period=1,
121
+ outage_retry_interval=0,
122
+ )
123
+ with env:
124
+ with env.sandbox(image_id='test_image'):
125
+ self.assertEqual(len(env.sandbox_pool['test_image']), 1)
126
+
127
+ with env.sandbox(image_id='test_image'):
128
+ self.assertEqual(len(env.sandbox_pool['test_image']), 2)
129
+
130
+ with self.assertRaises(interface.EnvironmentOverloadError):
131
+ with env.sandbox(image_id='test_image'):
132
+ pass
133
+ self.assertEqual(len(env.sandbox_pool['test_image']), 2)
134
+
135
+ with env.sandbox(image_id='test_image'):
136
+ self.assertEqual(len(env.sandbox_pool['test_image']), 2)
137
+
138
+ def test_image_feature_mappings(self):
139
+ env = TestingEnvironment(
140
+ image_ids=[
141
+ 'test_image1',
142
+ 'test_image2',
143
+ ],
144
+ features={
145
+ 'test_feature': TestingFeature(
146
+ applicable_images=['test_image1.*']
147
+ ),
148
+ 'test_feature2': TestingFeature(
149
+ applicable_images=['test_image2.*']
150
+ ),
151
+ 'test_feature3': TestingFeature(
152
+ applicable_images=['test_image.*']
153
+ ),
154
+ },
155
+ pool_size=0,
156
+ outage_grace_period=1,
157
+ outage_retry_interval=0,
158
+ sandbox_keepalive_interval=0,
159
+ )
160
+ with env:
161
+ with env.sandbox(image_id='test_image1') as sb:
162
+ self.assertIn('test_feature', sb.features)
163
+ self.assertNotIn('test_feature2', sb.features)
164
+ self.assertIn('test_feature3', sb.features)
165
+
166
+ with env.sandbox(image_id='test_image2') as sb:
167
+ self.assertNotIn('test_feature', sb.features)
168
+ self.assertIn('test_feature2', sb.features)
169
+ self.assertIn('test_feature3', sb.features)
170
+
171
+ with env.test_feature() as feature:
172
+ self.assertEqual(feature.sandbox.image_id, 'test_image1')
173
+
174
+ with self.assertRaisesRegex(
175
+ ValueError, 'Feature .* is not applicable to .*'
176
+ ):
177
+ with env.test_feature('test_image2'):
178
+ pass
179
+
180
+ with env.test_feature2() as feature:
181
+ self.assertEqual(feature.sandbox.image_id, 'test_image2')
182
+
183
+ with env.test_feature3() as feature:
184
+ self.assertEqual(feature.sandbox.image_id, 'test_image1')
185
+
186
+ with env.test_feature3('test_image2') as feature:
187
+ self.assertEqual(feature.sandbox.image_id, 'test_image2')
188
+
189
+ def test_feature_applicability_check(self):
190
+ with self.assertRaisesRegex(
191
+ ValueError, 'Feature .* is not applicable to .*'
192
+ ):
193
+ TestingEnvironment(
194
+ image_ids=[
195
+ 'test_image1',
196
+ ],
197
+ features={
198
+ 'test_feature2': TestingFeature(
199
+ applicable_images=['test_image2.*']
200
+ ),
201
+ },
202
+ )
203
+ env = TestingEnvironment(
204
+ image_ids=[],
205
+ supports_dynamic_image_loading=True,
206
+ features={
207
+ 'test_feature2': TestingFeature(
208
+ applicable_images=['test_image2.*']
209
+ ),
210
+ },
211
+ pool_size=0
212
+ )
213
+ with env:
214
+ with self.assertRaisesRegex(
215
+ ValueError, 'No image ID found for feature .*'
216
+ ):
217
+ with env.test_feature2():
218
+ pass
219
+
220
+ # Dynamically loaded IDs.
221
+ with env.test_feature2('test_image2') as feature:
222
+ self.assertEqual(feature.sandbox.image_id, 'test_image2')
223
+
224
+ def test_pool_size(self):
225
+ env = TestingEnvironment(
226
+ image_ids=['test_image'],
227
+ pool_size=1,
228
+ outage_grace_period=1,
229
+ outage_retry_interval=0,
230
+ )
231
+ self.assertEqual(env.min_pool_size('test_image'), 1)
232
+ self.assertEqual(env.max_pool_size('test_image'), 1)
233
+
234
+ env = TestingEnvironment(
235
+ image_ids=['test_image'],
236
+ pool_size=(0, 256),
237
+ outage_grace_period=1,
238
+ outage_retry_interval=0,
239
+ )
240
+ self.assertEqual(env.min_pool_size('test_image'), 0)
241
+ self.assertEqual(env.max_pool_size('test_image'), 256)
242
+
243
+ env = TestingEnvironment(
244
+ image_ids=['test_image'],
245
+ pool_size={
246
+ 'test_.*': (0, 128),
247
+ 'my.*': (5, 64),
248
+ 'exact_image_name': 10,
249
+ },
250
+ outage_grace_period=1,
251
+ outage_retry_interval=0,
252
+ )
253
+ self.assertEqual(env.min_pool_size('test_image'), 0)
254
+ self.assertEqual(env.max_pool_size('test_image'), 128)
255
+ self.assertEqual(env.min_pool_size('my_image'), 5)
256
+ self.assertEqual(env.max_pool_size('my_image'), 64)
257
+ self.assertEqual(env.min_pool_size('exact_image_name'), 10)
258
+ self.assertEqual(env.max_pool_size('exact_image_name'), 10)
259
+ self.assertEqual(env.min_pool_size('some_image'), 0) # default
260
+ self.assertEqual(env.max_pool_size('some_image'), 256) # default
261
+
262
+ def test_del(self):
263
+ env = TestingEnvironment(
264
+ features={'test_feature': TestingFeature()},
265
+ pool_size=0,
266
+ outage_grace_period=1,
267
+ outage_retry_interval=0,
268
+ sandbox_keepalive_interval=0,
269
+ )
270
+ env.start()
271
+ sb = env.acquire()
272
+ del sb
273
+ del env
274
+
275
+ def test_acquire_env_offline(self):
276
+ env = TestingEnvironment(
277
+ features={'test_feature': TestingFeature()},
278
+ pool_size=0,
279
+ outage_grace_period=1,
280
+ outage_retry_interval=0,
281
+ sandbox_keepalive_interval=0,
282
+ )
283
+ with self.assertRaises(interface.EnvironmentOutageError):
284
+ env.acquire()
285
+
286
+ def test_acquire_no_pooling(self):
287
+ env = TestingEnvironment(
288
+ features={'test_feature': TestingFeature()},
289
+ pool_size=0,
290
+ outage_grace_period=1,
291
+ outage_retry_interval=0,
292
+ sandbox_keepalive_interval=0,
293
+ )
294
+ with env:
295
+ sb = env.acquire()
296
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
297
+ self.assertIsNone(env.working_dir)
298
+ self.assertIsNone(sb.working_dir)
299
+ self.assertIsNone(sb.test_feature.working_dir)
300
+
301
+ def test_acquire_no_pooling_with_error(self):
302
+ env = TestingEnvironment(
303
+ features={
304
+ 'test_feature': TestingFeature(
305
+ simulate_setup_error=interface.SandboxStateError
306
+ )
307
+ },
308
+ pool_size=0,
309
+ outage_grace_period=1,
310
+ outage_retry_interval=0,
311
+ sandbox_keepalive_interval=0,
312
+ )
313
+ with env:
314
+ with self.assertRaises(interface.EnvironmentOutageError):
315
+ env.acquire()
316
+
317
+ def test_acquire_with_pooling(self):
318
+ env = TestingEnvironment(
319
+ features={'test_feature': TestingFeature()},
320
+ pool_size=1,
321
+ outage_grace_period=1,
322
+ outage_retry_interval=0,
323
+ sandbox_keepalive_interval=0,
324
+ )
325
+ with env:
326
+ sb = env.acquire()
327
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
328
+
329
+ def test_acquire_with_pooling_overload(self):
330
+ env = TestingEnvironment(
331
+ features={'test_feature': TestingFeature()},
332
+ pool_size=1,
333
+ outage_grace_period=1,
334
+ outage_retry_interval=0,
335
+ sandbox_keepalive_interval=0,
336
+ )
337
+ with env:
338
+ sb = env.acquire()
339
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
340
+ with self.assertRaises(interface.EnvironmentOverloadError):
341
+ env.acquire()
342
+
343
+ def test_acquire_with_growing_pool(self):
344
+ env = TestingEnvironment(
345
+ features={'test_feature': TestingFeature()},
346
+ pool_size=(1, 3),
347
+ outage_grace_period=1,
348
+ outage_retry_interval=0,
349
+ sandbox_keepalive_interval=0,
350
+ )
351
+ with env:
352
+ self.assertEqual(len(env.sandbox_pool['test_image']), 1)
353
+ self.assertEqual(
354
+ env.stats(),
355
+ {
356
+ 'sandbox': {
357
+ 'test_image': {
358
+ 'created': 0,
359
+ 'setting_up': 0,
360
+ 'ready': 1,
361
+ 'acquired': 0,
362
+ 'in_session': 0,
363
+ 'exiting_session': 0,
364
+ 'shutting_down': 0,
365
+ 'offline': 0,
366
+ }
367
+ }
368
+ }
369
+ )
370
+ sb = env.acquire()
371
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
372
+ self.assertEqual(
373
+ env.stats(),
374
+ {
375
+ 'sandbox': {
376
+ 'test_image': {
377
+ 'created': 0,
378
+ 'setting_up': 0,
379
+ 'ready': 0,
380
+ 'acquired': 1,
381
+ 'in_session': 0,
382
+ 'exiting_session': 0,
383
+ 'shutting_down': 0,
384
+ 'offline': 0,
385
+ }
386
+ }
387
+ }
388
+ )
389
+ self.assertEqual(len(env.sandbox_pool['test_image']), 1)
390
+ sb2 = env.acquire()
391
+ self.assertEqual(sb2.status, interface.Sandbox.Status.ACQUIRED)
392
+ self.assertEqual(len(env.sandbox_pool['test_image']), 2)
393
+ self.assertEqual(
394
+ env.stats(),
395
+ {
396
+ 'sandbox': {
397
+ 'test_image': {
398
+ 'created': 0,
399
+ 'setting_up': 0,
400
+ 'ready': 0,
401
+ 'acquired': 2,
402
+ 'in_session': 0,
403
+ 'exiting_session': 0,
404
+ 'shutting_down': 0,
405
+ 'offline': 0,
406
+ }
407
+ }
408
+ }
409
+ )
410
+ self.assertEqual(
411
+ env.stats(),
412
+ {
413
+ 'sandbox': {}
414
+ }
415
+ )
416
+
417
+ def test_acquire_with_growing_pool_failure(self):
418
+ env = TestingEnvironment(
419
+ features={'test_feature': TestingFeature()},
420
+ pool_size=(1, 3),
421
+ outage_grace_period=1,
422
+ outage_retry_interval=0,
423
+ sandbox_keepalive_interval=0,
424
+ )
425
+ with env:
426
+ self.assertEqual(len(env.sandbox_pool), 1)
427
+ sb = env.acquire()
428
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
429
+
430
+ # Make future sandbox setup to fail.
431
+ env.features.test_feature.rebind(
432
+ simulate_setup_error=interface.SandboxStateError,
433
+ skip_notification=True
434
+ )
435
+ with self.assertRaises(interface.EnvironmentOutageError):
436
+ env.acquire()
437
+
438
+ def test_housekeep_error(self):
439
+ env = TestingEnvironment(
440
+ features={'test_feature': TestingFeature()},
441
+ pool_size=1,
442
+ proactive_session_setup=True,
443
+ outage_grace_period=1,
444
+ outage_retry_interval=0,
445
+ sandbox_keepalive_interval=0,
446
+ )
447
+ with env:
448
+ self.assertEqual(len(env.sandbox_pool), 1)
449
+ self.assertIn('test_image', env.sandbox_pool)
450
+ self.assertEqual(
451
+ env.sandbox_pool['test_image'][0].status,
452
+ interface.Sandbox.Status.READY
453
+ )
454
+ # Make future sandbox setup to fail.
455
+ env.features.test_feature.rebind(
456
+ simulate_setup_error=interface.SandboxStateError,
457
+ skip_notification=True
458
+ )
459
+ with self.assertRaises(interface.SandboxStateError):
460
+ with env.sandbox() as sb:
461
+ sb.shell('bad command', raise_error=interface.SandboxStateError)
462
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
463
+ self.assertEqual(len(sb.state_errors), 1)
464
+ sb_offline_time = time.time()
465
+ while time.time() - sb_offline_time < 10:
466
+ if not env.is_online:
467
+ break
468
+ time.sleep(0.5)
469
+ self.assertFalse(env.is_online)
470
+
471
+
472
+ if __name__ == '__main__':
473
+ unittest.main()
@@ -110,6 +110,15 @@ class BaseFeature(interface.Feature):
110
110
  super()._on_parent_change(old_parent, new_parent)
111
111
  self.__dict__.pop('name', None)
112
112
 
113
+ @functools.cached_property
114
+ def environment(self) -> interface.Environment:
115
+ """Returns the environment that the feature is running in."""
116
+ if self._sandbox is not None:
117
+ return self._sandbox.environment
118
+ env = self.sym_ancestor(lambda v: isinstance(v, interface.Environment))
119
+ assert env is not None, 'Feature is not put into an environment.'
120
+ return env
121
+
113
122
  @property
114
123
  def sandbox(self) -> interface.Sandbox:
115
124
  """Returns the sandbox that the feature is running in."""
@@ -188,7 +197,13 @@ class BaseFeature(interface.Feature):
188
197
  error: BaseException | None = None
189
198
  ) -> None:
190
199
  """Called when the feature is setup."""
191
- self.sandbox.on_feature_setup(self, duration, error)
200
+ self.environment.event_handler.on_feature_setup(
201
+ environment=self.environment,
202
+ sandbox=self.sandbox,
203
+ feature=self,
204
+ duration=duration,
205
+ error=error
206
+ )
192
207
 
193
208
  def on_teardown(
194
209
  self,
@@ -196,7 +211,13 @@ class BaseFeature(interface.Feature):
196
211
  error: BaseException | None = None
197
212
  ) -> None:
198
213
  """Called when the feature is teardown."""
199
- self.sandbox.on_feature_teardown(self, duration, error)
214
+ self.environment.event_handler.on_feature_teardown(
215
+ environment=self.environment,
216
+ sandbox=self.sandbox,
217
+ feature=self,
218
+ duration=duration,
219
+ error=error
220
+ )
200
221
 
201
222
  def on_housekeep(
202
223
  self,
@@ -205,8 +226,14 @@ class BaseFeature(interface.Feature):
205
226
  **kwargs
206
227
  ) -> None:
207
228
  """Called when the feature has done housekeeping."""
208
- self.sandbox.on_feature_housekeep(
209
- self, self._housekeep_counter, duration, error, **kwargs
229
+ self.environment.event_handler.on_feature_housekeep(
230
+ environment=self.environment,
231
+ sandbox=self.sandbox,
232
+ feature=self,
233
+ counter=self._housekeep_counter,
234
+ duration=duration,
235
+ error=error,
236
+ **kwargs
210
237
  )
211
238
 
212
239
  def on_setup_session(
@@ -215,7 +242,14 @@ class BaseFeature(interface.Feature):
215
242
  error: BaseException | None = None,
216
243
  ) -> None:
217
244
  """Called when the feature is setup for a user session."""
218
- self.sandbox.on_feature_setup_session(self, duration, error)
245
+ self.environment.event_handler.on_feature_setup_session(
246
+ environment=self.environment,
247
+ sandbox=self.sandbox,
248
+ feature=self,
249
+ session_id=self.session_id,
250
+ duration=duration,
251
+ error=error
252
+ )
219
253
 
220
254
  def on_teardown_session(
221
255
  self,
@@ -223,7 +257,14 @@ class BaseFeature(interface.Feature):
223
257
  error: BaseException | None = None,
224
258
  ) -> None:
225
259
  """Called when the feature is teardown for a user session."""
226
- self.sandbox.on_feature_teardown_session(self, duration, error)
260
+ self.environment.event_handler.on_feature_teardown_session(
261
+ environment=self.environment,
262
+ sandbox=self.sandbox,
263
+ feature=self,
264
+ session_id=self.session_id,
265
+ duration=duration,
266
+ error=error
267
+ )
227
268
 
228
269
  def on_activity(
229
270
  self,
@@ -233,10 +274,13 @@ class BaseFeature(interface.Feature):
233
274
  **kwargs
234
275
  ) -> None:
235
276
  """Called when a sandbox activity is performed."""
236
- self.sandbox.on_activity(
277
+ self.environment.event_handler.on_sandbox_activity(
237
278
  name=f'{self.name}.{name}',
279
+ environment=self.environment,
280
+ sandbox=self.sandbox,
238
281
  feature=self,
239
- error=error,
282
+ session_id=self.session_id,
240
283
  duration=duration,
284
+ error=error,
241
285
  **kwargs
242
286
  )