pyglove 0.5.0.dev202509260810__py3-none-any.whl → 0.5.0.dev202509270808__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 pyglove might be problematic. Click here for more details.

pyglove/core/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2019 The PyGlove Authors
1
+ # Copyright 2025 The PyGlove Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -353,6 +353,12 @@ from pyglove.core import coding
353
353
 
354
354
  from pyglove.core import logging
355
355
 
356
+ #
357
+ # Symbols from `monitoring.py`.
358
+ #
359
+
360
+ from pyglove.core import monitoring
361
+
356
362
  # pylint: enable=g-import-not-at-top
357
363
  # pylint: enable=reimported
358
364
  # pylint: enable=unused-import
@@ -0,0 +1,522 @@
1
+ # Copyright 2025 The PyGlove 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
+ """Pluggable metric systems for monitoring.
15
+
16
+ This module allows PyGlove to plugin metrics to monitor the execution of
17
+ programs.
18
+ """
19
+
20
+ import abc
21
+ import collections
22
+ import math
23
+ import threading
24
+ import typing
25
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
26
+
27
+
28
+ try:
29
+ import numpy # pylint: disable=g-import-not-at-top
30
+ except ImportError:
31
+ numpy = None
32
+
33
+
34
+ class Metric(metaclass=abc.ABCMeta):
35
+ """Base class for metrics."""
36
+
37
+ def __init__(
38
+ self,
39
+ namespace: str,
40
+ name: str,
41
+ description: str,
42
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]]
43
+ ) -> None:
44
+ self._namespace = namespace
45
+ self._name = name
46
+ self._description = description
47
+ self._parameter_definitions = parameter_definitions
48
+
49
+ @property
50
+ def namespace(self) -> str:
51
+ """Returns the namespace of the metric."""
52
+ return self._namespace
53
+
54
+ @property
55
+ def name(self) -> str:
56
+ """Returns the name of the metric."""
57
+ return self._name
58
+
59
+ @property
60
+ def full_name(self) -> str:
61
+ """Returns the full name of the metric."""
62
+ return f'{self.namespace}/{self.name}'
63
+
64
+ @property
65
+ def description(self) -> str:
66
+ """Returns the description of the metric."""
67
+ return self._description
68
+
69
+ @property
70
+ def parameter_definitions(self) -> Dict[str, Type[Union[int, str, bool]]]:
71
+ """Returns the parameter definitions of the metric."""
72
+ return self._parameter_definitions
73
+
74
+ def _parameters_key(self, **parameters) -> Tuple[Any, ...]:
75
+ """Returns the parameters tuple for the metric."""
76
+ for k, t in self._parameter_definitions.items():
77
+ v = parameters.get(k)
78
+ if v is None:
79
+ raise KeyError(
80
+ f'Metric {self.full_name!r}: Parameter {k!r} is required but not '
81
+ f'given.'
82
+ )
83
+ if not isinstance(v, t):
84
+ raise TypeError(
85
+ f'Metric {self.full_name!r}: Parameter {k!r} has type '
86
+ f'{type(v)} but expected type {t}.'
87
+ )
88
+ for k in parameters:
89
+ if k not in self._parameter_definitions:
90
+ raise KeyError(
91
+ f'Metric {self.full_name!r}: Parameter {k!r} is not defined but '
92
+ f'provided. Available parameters: '
93
+ f'{list(self._parameter_definitions.keys())}'
94
+ )
95
+ return tuple(parameters[k] for k in self._parameter_definitions)
96
+
97
+
98
+ class Counter(Metric):
99
+ """Base class for counters."""
100
+
101
+ @abc.abstractmethod
102
+ def increment(self, delta: int = 1, **parameters) -> int:
103
+ """Increments the counter by delta and returns the new value.
104
+
105
+ Args:
106
+ delta: The amount to increment the counter by.
107
+ **parameters: Parameters for parameterized counters.
108
+
109
+ Returns:
110
+ The new value of the counter.
111
+ """
112
+
113
+ @abc.abstractmethod
114
+ def value(self, **parameters) -> int:
115
+ """Returns the value of the counter for the given parameters.
116
+
117
+ Args:
118
+ **parameters: Parameters for parameterized counters.
119
+
120
+ Returns:
121
+ The value of the counter.
122
+ """
123
+
124
+
125
+ class Distribution(metaclass=abc.ABCMeta):
126
+ """Distribution of scalar values."""
127
+
128
+ @property
129
+ @abc.abstractmethod
130
+ def count(self) -> int:
131
+ """Returns the number of samples in the distribution."""
132
+
133
+ @property
134
+ @abc.abstractmethod
135
+ def sum(self) -> float:
136
+ """Returns the sum of the distribution."""
137
+
138
+ @property
139
+ @abc.abstractmethod
140
+ def mean(self) -> float:
141
+ """Returns the mean of the distribution."""
142
+
143
+ @property
144
+ @abc.abstractmethod
145
+ def stddev(self) -> float:
146
+ """Returns the standard deviation of the distribution."""
147
+
148
+ @property
149
+ def median(self) -> float:
150
+ """Returns the standard deviation of the distribution."""
151
+ return self.percentile(50)
152
+
153
+ @property
154
+ @abc.abstractmethod
155
+ def variance(self) -> float:
156
+ """Returns the variance of the distribution."""
157
+
158
+ @abc.abstractmethod
159
+ def percentile(self, n: float) -> float:
160
+ """Returns the median of the distribution.
161
+
162
+ Args:
163
+ n: The percentile to return. Should be in the range [0, 100].
164
+
165
+ Returns:
166
+ The n-th percentile of the distribution.
167
+ """
168
+
169
+ @abc.abstractmethod
170
+ def fraction_less_than(self, value: float) -> float:
171
+ """Returns the fraction of values in the distribution less than value."""
172
+
173
+
174
+ class Scalar(Metric):
175
+ """Base class for scalar values."""
176
+
177
+ @abc.abstractmethod
178
+ def record(self, value: int, **parameters) -> None:
179
+ """Records a value to the scalar.
180
+
181
+ Args:
182
+ value: The value to record.
183
+ **parameters: Parameters for parameterized scalars.
184
+ """
185
+
186
+ @abc.abstractmethod
187
+ def distribution(self, **parameters) -> Distribution:
188
+ """Returns the distribution of the scalar.
189
+
190
+ Args:
191
+ **parameters: Parameters for parameterized scalars.
192
+
193
+ Returns:
194
+ The distribution of the scalar.
195
+ """
196
+
197
+
198
+ class MetricCollection(metaclass=abc.ABCMeta):
199
+ """Base class for counter collections."""
200
+
201
+ def __init__(
202
+ self,
203
+ namespace: str,
204
+ default_parameters: Optional[
205
+ Dict[str, Type[Union[int, str, bool]]]
206
+ ] = None
207
+ ):
208
+ """Initializes the metric collection.
209
+
210
+ Args:
211
+ namespace: The namespace of the metric collection.
212
+ default_parameters: The default parameters used to create metrics
213
+ if not specified.
214
+ """
215
+ self._namespace = namespace
216
+ self._default_parameter_definitions = default_parameters or {}
217
+ self._metrics = self._metric_container()
218
+
219
+ @property
220
+ def namespace(self) -> str:
221
+ """Returns the namespace of the metric collection."""
222
+ return self._namespace
223
+
224
+ def metrics(self) -> List[Metric]:
225
+ """Returns the names of the metrics."""
226
+ return [m for m in self._metrics.values() if m.namespace == self._namespace]
227
+
228
+ def _metric_container(self) -> dict[str, Metric]:
229
+ """Returns the container for metrics."""
230
+ return {}
231
+
232
+ def _get_or_create_metric(
233
+ self,
234
+ metric_cls: Type[Metric],
235
+ create_metric_fn: Callable[
236
+ [str, str, Dict[str, Type[Union[int, str, bool]]]],
237
+ Metric
238
+ ],
239
+ name: str,
240
+ description: str,
241
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]]
242
+ ) -> Metric:
243
+ """Gets or creates a metric with the given name."""
244
+ full_name = f'{self._namespace}/{name}'
245
+ metric = self._metrics.get(full_name)
246
+ if metric is not None:
247
+ if not isinstance(metric, metric_cls):
248
+ raise ValueError(
249
+ f'Metric {full_name!r} already exists with a different type '
250
+ f'({type(metric)}).'
251
+ )
252
+ if description != metric.description:
253
+ raise ValueError(
254
+ f'Metric {full_name!r} already exists with a different description '
255
+ f'({metric.description!r}).'
256
+ )
257
+ if parameter_definitions != metric.parameter_definitions:
258
+ raise ValueError(
259
+ f'Metric {full_name!r} already exists with different parameter '
260
+ f'definitions ({metric.parameter_definitions!r}).'
261
+ )
262
+ else:
263
+ metric = create_metric_fn(name, description, parameter_definitions)
264
+ self._metrics[full_name] = metric
265
+ return metric
266
+
267
+ def get_counter(
268
+ self,
269
+ name: str,
270
+ description: str,
271
+ parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
272
+ **kwargs
273
+ ) -> Counter:
274
+ """Gets or creates a counter with the given name.
275
+
276
+ Args:
277
+ name: The name of the counter.
278
+ description: The description of the counter.
279
+ parameters: The definitions of the parameters for the counter.
280
+ `default_parameters` from the collection will be used if not specified.
281
+ **kwargs: Additional arguments for creating the counter.
282
+
283
+ Returns:
284
+ The counter with the given name.
285
+ """
286
+ if parameters is None:
287
+ parameters = self._default_parameter_definitions
288
+ return typing.cast(
289
+ Counter, self._get_or_create_metric(
290
+ Counter, self._create_counter, name, description, parameters,
291
+ **kwargs
292
+ )
293
+ )
294
+
295
+ @abc.abstractmethod
296
+ def _create_counter(
297
+ self,
298
+ name: str,
299
+ description: str,
300
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
301
+ **kwargs
302
+ ) -> Counter:
303
+ """Creates a counter with the given name."""
304
+
305
+ def get_scalar(
306
+ self,
307
+ name: str,
308
+ description: str,
309
+ parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
310
+ **kwargs
311
+ ) -> Scalar:
312
+ """Gets or creates a scalar with the given name.
313
+
314
+ Args:
315
+ name: The name of the counter.
316
+ description: The description of the counter.
317
+ parameters: The definitions of the parameters for the counter.
318
+ `default_parameters` from the collection will be used if not specified.
319
+ **kwargs: Additional arguments for creating the scalar.
320
+
321
+ Returns:
322
+ The counter with the given name.
323
+ """
324
+ if parameters is None:
325
+ parameters = self._default_parameter_definitions
326
+ return typing.cast(
327
+ Scalar,
328
+ self._get_or_create_metric(
329
+ Scalar, self._create_scalar, name, description, parameters, **kwargs
330
+ )
331
+ )
332
+
333
+ @abc.abstractmethod
334
+ def _create_scalar(
335
+ self,
336
+ name: str,
337
+ description: str,
338
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
339
+ **kwargs
340
+ ) -> Scalar:
341
+ """Creates a counter with the given name."""
342
+
343
+ #
344
+ # InMemoryMetricCollection.
345
+ #
346
+
347
+
348
+ class _InMemoryCounter(Counter):
349
+ """In-memory counter."""
350
+
351
+ def __init__(self, *args, **kwargs):
352
+ super().__init__(*args, **kwargs)
353
+ self._counter = collections.defaultdict(int)
354
+ self._lock = threading.Lock()
355
+
356
+ def increment(self, delta: int = 1, **parameters) -> int:
357
+ """Increments the counter by delta and returns the new value."""
358
+ parameters_key = self._parameters_key(**parameters)
359
+ with self._lock:
360
+ value = self._counter[parameters_key]
361
+ value += delta
362
+ self._counter[parameters_key] = value
363
+ return value
364
+
365
+ def value(self, **parameters) -> int:
366
+ """Returns the value of the counter based on the given parameters."""
367
+ return self._counter[self._parameters_key(**parameters)]
368
+
369
+
370
+ class _InMemoryScalar(Scalar):
371
+ """In-memory scalar."""
372
+
373
+ def __init__(self, *args, window_size: int = 1024 * 1024, **kwargs):
374
+ super().__init__(*args, **kwargs)
375
+ self._window_size = window_size
376
+ self._distributions = collections.defaultdict(
377
+ lambda: _InMemoryDistribution(self._window_size)
378
+ )
379
+ self._lock = threading.Lock()
380
+
381
+ def record(self, value: Any, **parameters) -> None:
382
+ """Records a value to the scalar."""
383
+ parameters_key = self._parameters_key(**parameters)
384
+ self._distributions[parameters_key].add(value)
385
+
386
+ def distribution(self, **parameters) -> Distribution:
387
+ """Returns the distribution of the scalar."""
388
+ parameters_key = self._parameters_key(**parameters)
389
+ return self._distributions[parameters_key]
390
+
391
+
392
+ class _InMemoryDistribution(Distribution):
393
+ """In memory distribution of scalar values."""
394
+
395
+ def __init__(self, window_size: int = 1024 * 1024):
396
+ self._window_size = window_size
397
+ self._data = []
398
+ self._sum = 0.0
399
+ self._square_sum = 0.0
400
+ self._count = 0
401
+ self._lock = threading.Lock()
402
+
403
+ def add(self, value: int):
404
+ """Adds a value to the distribution."""
405
+ with self._lock:
406
+ if len(self._data) == self._window_size:
407
+ x = self._data.pop(0)
408
+ self._sum -= x
409
+ self._count -= 1
410
+ self._square_sum -= x ** 2
411
+
412
+ self._data.append(value)
413
+ self._count += 1
414
+ self._sum += value
415
+ self._square_sum += value ** 2
416
+
417
+ @property
418
+ def count(self) -> int:
419
+ """Returns the number of samples in the distribution."""
420
+ return self._count
421
+
422
+ @property
423
+ def sum(self) -> float:
424
+ """Returns the sum of the distribution."""
425
+ return self._sum
426
+
427
+ @property
428
+ def mean(self) -> float:
429
+ """Returns the mean of the distribution."""
430
+ return self._sum / self._count if self._count else 0.0
431
+
432
+ @property
433
+ def stddev(self) -> float:
434
+ """Returns the standard deviation of the distribution."""
435
+ return math.sqrt(self.variance)
436
+
437
+ @property
438
+ def variance(self) -> float:
439
+ """Returns the variance of the distribution."""
440
+ if self._count < 2:
441
+ return 0.0
442
+ return self._square_sum / self._count - self.mean ** 2
443
+
444
+ def percentile(self, n: float) -> float:
445
+ """Returns the median of the distribution."""
446
+ if n < 0 or n > 100:
447
+ raise ValueError(f'Percentile {n} is not in the range [0, 100].')
448
+
449
+ if self._count == 0:
450
+ return 0.0
451
+
452
+ if numpy is not None:
453
+ return numpy.percentile(self._data, n) # pytype: disable=attribute-error
454
+
455
+ sorted_data = sorted(self._data)
456
+ index = (n / 100) * (len(sorted_data) - 1)
457
+ if index % 1 == 0:
458
+ return sorted_data[int(index)]
459
+ else:
460
+ # Interpolate the value at the given percentile.
461
+ lower_index = int(index)
462
+ fraction = index - lower_index
463
+
464
+ # Get the values at the two surrounding integer indices
465
+ lower_value = sorted_data[lower_index]
466
+ upper_value = sorted_data[lower_index + 1]
467
+ return lower_value + fraction * (upper_value - lower_value)
468
+
469
+ def fraction_less_than(self, value: float) -> float:
470
+ """Returns the fraction of values in the distribution less than value."""
471
+ if self._count == 0:
472
+ return 0.0
473
+ with self._lock:
474
+ return len([x for x in self._data if x < value]) / self._count
475
+
476
+
477
+ class InMemoryMetricCollection(MetricCollection):
478
+ """In-memory counter."""
479
+
480
+ def _create_counter(
481
+ self,
482
+ name: str,
483
+ description: str,
484
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
485
+ **kwargs
486
+ ) -> Counter:
487
+ return _InMemoryCounter(
488
+ self._namespace, name, description, parameter_definitions
489
+ )
490
+
491
+ def _create_scalar(
492
+ self,
493
+ name: str,
494
+ description: str,
495
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
496
+ *,
497
+ window_size: int = 1024 * 1024,
498
+ **kwargs
499
+ ) -> Scalar:
500
+ return _InMemoryScalar(
501
+ self._namespace, name, description, parameter_definitions,
502
+ window_size=window_size, **kwargs
503
+ )
504
+
505
+
506
+ _METRIC_COLLECTION_CLS = InMemoryMetricCollection # pylint: disable=invalid-name
507
+
508
+
509
+ def metric_collection(namespace: str, **kwargs) -> MetricCollection:
510
+ """Creates a metric collection."""
511
+ return _METRIC_COLLECTION_CLS(namespace, **kwargs)
512
+
513
+
514
+ def set_default_metric_collection_cls(cls: Type[MetricCollection]) -> None:
515
+ """Sets the default metric collection class."""
516
+ global _METRIC_COLLECTION_CLS
517
+ _METRIC_COLLECTION_CLS = cls
518
+
519
+
520
+ def default_metric_collection_cls() -> Type[MetricCollection]:
521
+ """Returns the default metric collection class."""
522
+ return _METRIC_COLLECTION_CLS
@@ -0,0 +1,230 @@
1
+ # Copyright 2025 The PyGlove 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
+
15
+ import unittest
16
+ from pyglove.core import monitoring
17
+
18
+
19
+ class MetricCollectionTest(unittest.TestCase):
20
+ """Tests for metric collection."""
21
+
22
+ def test_default_metric_collection_cls(self):
23
+ self.assertIs(
24
+ monitoring.default_metric_collection_cls(),
25
+ monitoring.InMemoryMetricCollection
26
+ )
27
+
28
+ class TestMetricCollection(monitoring.MetricCollection):
29
+ pass
30
+
31
+ monitoring.set_default_metric_collection_cls(TestMetricCollection)
32
+ self.assertIs(
33
+ monitoring.default_metric_collection_cls(),
34
+ TestMetricCollection
35
+ )
36
+ monitoring.set_default_metric_collection_cls(
37
+ monitoring.InMemoryMetricCollection
38
+ )
39
+ self.assertIs(
40
+ monitoring.default_metric_collection_cls(),
41
+ monitoring.InMemoryMetricCollection
42
+ )
43
+
44
+ def test_metric_collection(self):
45
+ collection = monitoring.metric_collection('/test')
46
+ self.assertEqual(collection.namespace, '/test')
47
+ self.assertIsInstance(collection, monitoring.InMemoryMetricCollection)
48
+
49
+ def test_creation_failures(self):
50
+ collection = monitoring.InMemoryMetricCollection('/test')
51
+ counter = collection.get_counter('counter', 'counter description')
52
+ self.assertIsInstance(counter, monitoring.Counter)
53
+ with self.assertRaisesRegex(
54
+ ValueError, 'Metric .* already exists with a different type'
55
+ ):
56
+ collection.get_scalar('counter', 'counter description')
57
+
58
+ with self.assertRaisesRegex(
59
+ ValueError, 'Metric .* already exists with a different description'
60
+ ):
61
+ collection.get_counter('counter', 'different description')
62
+
63
+ with self.assertRaisesRegex(
64
+ ValueError,
65
+ 'Metric .* already exists with different parameter definitions'
66
+ ):
67
+ collection.get_counter(
68
+ 'counter', 'counter description', parameters={'field1': str}
69
+ )
70
+
71
+
72
+ class InMemoryDistributionTest(unittest.TestCase):
73
+ """Tests for in memory distribution."""
74
+
75
+ def test_empty_distribution(self):
76
+ dist = monitoring._InMemoryDistribution()
77
+ self.assertEqual(dist.count, 0)
78
+ self.assertEqual(dist.sum, 0.0)
79
+ self.assertEqual(dist.mean, 0.0)
80
+ self.assertEqual(dist.stddev, 0.0)
81
+ self.assertEqual(dist.variance, 0.0)
82
+ self.assertEqual(dist.median, 0.0)
83
+ self.assertEqual(dist.percentile(50), 0.0)
84
+ self.assertEqual(dist.fraction_less_than(100), 0.0)
85
+
86
+ def test_add_value(self):
87
+ dist = monitoring._InMemoryDistribution()
88
+ dist.add(1)
89
+ dist.add(3)
90
+ dist.add(10)
91
+ dist.add(2)
92
+ self.assertEqual(dist.count, 4)
93
+ self.assertEqual(dist.sum, 16.0)
94
+ self.assertEqual(dist.mean, 4.0)
95
+ self.assertEqual(dist.stddev, 3.5355339059327378)
96
+ self.assertEqual(dist.variance, 12.5)
97
+ self.assertEqual(dist.median, 2.5)
98
+ self.assertEqual(dist.percentile(50), 2.5)
99
+ self.assertEqual(dist.percentile(10), 1.3)
100
+ self.assertEqual(dist.fraction_less_than(100), 1.0)
101
+ self.assertEqual(dist.fraction_less_than(1), 0.0)
102
+ self.assertEqual(dist.fraction_less_than(10), 0.75)
103
+
104
+ def test_add_value_no_numpy(self):
105
+ numpy = monitoring.numpy
106
+ monitoring.numpy = None
107
+ dist = monitoring._InMemoryDistribution()
108
+ dist.add(1)
109
+ dist.add(3)
110
+ dist.add(10)
111
+ dist.add(2)
112
+ self.assertEqual(dist.count, 4)
113
+ self.assertEqual(dist.sum, 16.0)
114
+ self.assertEqual(dist.mean, 4.0)
115
+ self.assertEqual(dist.stddev, 3.5355339059327378)
116
+ self.assertEqual(dist.variance, 12.5)
117
+ self.assertEqual(dist.median, 2.5)
118
+ self.assertEqual(dist.percentile(50), 2.5)
119
+ self.assertEqual(dist.percentile(10), 1.3)
120
+ self.assertEqual(dist.fraction_less_than(100), 1.0)
121
+ self.assertEqual(dist.fraction_less_than(1), 0.0)
122
+ self.assertEqual(dist.fraction_less_than(10), 0.75)
123
+ monitoring.numpy = numpy
124
+
125
+ def test_window_size(self):
126
+ dist = monitoring._InMemoryDistribution(window_size=3)
127
+ dist.add(1)
128
+ dist.add(3)
129
+ dist.add(10)
130
+ dist.add(2)
131
+ self.assertEqual(dist.count, 3)
132
+ self.assertEqual(dist.sum, 15.0)
133
+ self.assertEqual(dist.mean, 5.0)
134
+ self.assertEqual(dist.stddev, 3.5590260840104366)
135
+ self.assertEqual(dist.variance, 12.666666666666664)
136
+ self.assertEqual(dist.median, 3.0)
137
+ self.assertEqual(dist.percentile(50), 3.0)
138
+ self.assertEqual(dist.percentile(10), 2.2)
139
+ self.assertEqual(dist.fraction_less_than(100), 1.0)
140
+ self.assertEqual(dist.fraction_less_than(1), 0.0)
141
+ self.assertEqual(dist.fraction_less_than(10), 0.6666666666666666)
142
+
143
+
144
+ class InMemoryCounterTest(unittest.TestCase):
145
+ """Tests for in memory counter."""
146
+
147
+ def test_counter_without_parameters(self):
148
+ collection = monitoring.InMemoryMetricCollection('/test')
149
+ counter = collection.get_counter('counter', 'counter description')
150
+ self.assertEqual(counter.namespace, '/test')
151
+ self.assertEqual(counter.name, 'counter')
152
+ self.assertEqual(counter.description, 'counter description')
153
+ self.assertEqual(counter.parameter_definitions, {})
154
+ self.assertEqual(counter.full_name, '/test/counter')
155
+ self.assertEqual(counter.value(), 0)
156
+ self.assertEqual(counter.increment(), 1)
157
+ self.assertEqual(counter.value(), 1)
158
+ self.assertEqual(counter.increment(2), 3)
159
+ self.assertEqual(counter.value(), 3)
160
+ self.assertIs(collection.metrics()[0], counter)
161
+
162
+ def test_counter_with_parameters(self):
163
+ collection = monitoring.InMemoryMetricCollection('/test')
164
+ counter = collection.get_counter(
165
+ 'counter', 'counter description', {'field1': str}
166
+ )
167
+ self.assertEqual(counter.namespace, '/test')
168
+ self.assertEqual(counter.name, 'counter')
169
+ self.assertEqual(counter.description, 'counter description')
170
+ self.assertEqual(counter.parameter_definitions, {'field1': str})
171
+ self.assertEqual(counter.full_name, '/test/counter')
172
+ self.assertEqual(counter.value(field1='foo'), 0)
173
+ self.assertEqual(counter.increment(field1='foo'), 1)
174
+ self.assertEqual(counter.value(field1='bar'), 0)
175
+ self.assertEqual(counter.increment(field1='bar'), 1)
176
+ self.assertEqual(counter.increment(field1='foo', delta=2), 3)
177
+ self.assertEqual(counter.value(field1='foo'), 3)
178
+
179
+ with self.assertRaisesRegex(TypeError, '.* has type .* but expected type'):
180
+ counter.increment(field1=1)
181
+
182
+ with self.assertRaisesRegex(KeyError, '.* is required but not given'):
183
+ counter.increment()
184
+
185
+ with self.assertRaisesRegex(KeyError, '.* is not defined but provided'):
186
+ counter.increment(field1='foo', field2='a')
187
+
188
+
189
+ class InMemoryScalarTest(unittest.TestCase):
190
+ """Tests for in memory scalar."""
191
+
192
+ def test_scalar_without_parameters(self):
193
+ collection = monitoring.InMemoryMetricCollection('/test')
194
+ scalar = collection.get_scalar('scalar', 'scalar description')
195
+ self.assertEqual(scalar.namespace, '/test')
196
+ self.assertEqual(scalar.name, 'scalar')
197
+ self.assertEqual(scalar.description, 'scalar description')
198
+ self.assertEqual(scalar.parameter_definitions, {})
199
+ self.assertEqual(scalar.full_name, '/test/scalar')
200
+ dist = scalar.distribution()
201
+ self.assertEqual(dist.count, 0)
202
+ scalar.record(1)
203
+ scalar.record(2)
204
+ scalar.record(3)
205
+ scalar.distribution()
206
+ self.assertEqual(dist.count, 3)
207
+
208
+ def test_scalar_with_parameters(self):
209
+ collection = monitoring.InMemoryMetricCollection('/test')
210
+ scalar = collection.get_scalar(
211
+ 'scalar', 'scalar description', {'field1': str}
212
+ )
213
+ self.assertEqual(scalar.namespace, '/test')
214
+ self.assertEqual(scalar.name, 'scalar')
215
+ self.assertEqual(scalar.description, 'scalar description')
216
+ self.assertEqual(scalar.parameter_definitions, {'field1': str})
217
+ self.assertEqual(scalar.full_name, '/test/scalar')
218
+ dist = scalar.distribution(field1='foo')
219
+ self.assertEqual(dist.count, 0)
220
+ scalar.record(1, field1='foo')
221
+ scalar.record(2, field1='foo')
222
+ scalar.record(3, field1='bar')
223
+ dist = scalar.distribution(field1='foo')
224
+ self.assertEqual(dist.count, 2)
225
+ dist = scalar.distribution(field1='bar')
226
+ self.assertEqual(dist.count, 1)
227
+
228
+
229
+ if __name__ == '__main__':
230
+ unittest.main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglove
3
- Version: 0.5.0.dev202509260810
3
+ Version: 0.5.0.dev202509270808
4
4
  Summary: PyGlove: A library for manipulating Python objects.
5
5
  Home-page: https://github.com/google/pyglove
6
6
  Author: PyGlove Authors
@@ -1,7 +1,9 @@
1
1
  pyglove/__init__.py,sha256=LP1HNk_VVWMHaakX3HZ0NeZ2c4lq2uJaRbalvT8haOg,1352
2
- pyglove/core/__init__.py,sha256=H4Yah3bYPrEqvuwRvhbA1MbQn7W6l2j5mmXkeAUNI30,9700
2
+ pyglove/core/__init__.py,sha256=WPTvhQXv63f4-AkYH__RwVKQyr8vHr5e3tGqT2OxRGM,9774
3
3
  pyglove/core/logging.py,sha256=zTNLGnWrl6kkjjEjTphQMeNf9FfqFtl1G9VCh2LhnHg,4614
4
4
  pyglove/core/logging_test.py,sha256=ioDbmf4S6xQXeyGWihvk6292wfYdldBw_TK1WXvIq5k,1648
5
+ pyglove/core/monitoring.py,sha256=6QWbnCBC9HOc6_lnPmoDi-JTsusNwlXz4_LPnwWjjsw,15143
6
+ pyglove/core/monitoring_test.py,sha256=47zjsNiYEMKOOdYpUue7_BPJYi1z9eJN_t6cw65TB-4,8558
5
7
  pyglove/core/coding/__init__.py,sha256=tuHIg19ZchtkOQbdFVTVLkUpBa5f1eo66XtnKw3lcIU,1645
6
8
  pyglove/core/coding/errors.py,sha256=aP3Y4amBzOKdlb5JnESJ3kdoijQXbiBiPDMeA88LNrk,3310
7
9
  pyglove/core/coding/errors_test.py,sha256=fwOR8vLiRvLadubsccyE19hLHj-kThlCQt88qmUYk9M,2311
@@ -216,8 +218,8 @@ pyglove/ext/scalars/randoms.py,sha256=LkMIIx7lOq_lvJvVS3BrgWGuWl7Pi91-lA-O8x_gZs
216
218
  pyglove/ext/scalars/randoms_test.py,sha256=nEhiqarg8l_5EOucp59CYrpO2uKxS1pe0hmBdZUzRNM,2000
217
219
  pyglove/ext/scalars/step_wise.py,sha256=IDw3tuTpv0KVh7AN44W43zqm1-E0HWPUlytWOQC9w3Y,3789
218
220
  pyglove/ext/scalars/step_wise_test.py,sha256=TL1vJ19xVx2t5HKuyIzGoogF7N3Rm8YhLE6JF7i0iy8,2540
219
- pyglove-0.5.0.dev202509260810.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
220
- pyglove-0.5.0.dev202509260810.dist-info/METADATA,sha256=itSuEYa6B_MjlxjPTUoiFAEb_KcjWxrKbYNmgdG-_I4,7089
221
- pyglove-0.5.0.dev202509260810.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
222
- pyglove-0.5.0.dev202509260810.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
223
- pyglove-0.5.0.dev202509260810.dist-info/RECORD,,
221
+ pyglove-0.5.0.dev202509270808.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
222
+ pyglove-0.5.0.dev202509270808.dist-info/METADATA,sha256=V_xogke2HnisdqNjo0a6ZL4iQhbaJyeh1toYIFFDl5Y,7089
223
+ pyglove-0.5.0.dev202509270808.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
224
+ pyglove-0.5.0.dev202509270808.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
225
+ pyglove-0.5.0.dev202509270808.dist-info/RECORD,,