pyglove 0.5.0.dev202508250811__py3-none-any.whl → 0.5.0.dev202511300809__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.
Files changed (44) hide show
  1. pyglove/core/__init__.py +8 -1
  2. pyglove/core/geno/base.py +7 -3
  3. pyglove/core/io/file_system.py +295 -2
  4. pyglove/core/io/file_system_test.py +291 -0
  5. pyglove/core/logging.py +45 -1
  6. pyglove/core/logging_test.py +12 -21
  7. pyglove/core/monitoring.py +657 -0
  8. pyglove/core/monitoring_test.py +289 -0
  9. pyglove/core/symbolic/__init__.py +7 -0
  10. pyglove/core/symbolic/base.py +89 -35
  11. pyglove/core/symbolic/base_test.py +3 -3
  12. pyglove/core/symbolic/dict.py +31 -12
  13. pyglove/core/symbolic/dict_test.py +49 -0
  14. pyglove/core/symbolic/list.py +17 -3
  15. pyglove/core/symbolic/list_test.py +24 -2
  16. pyglove/core/symbolic/object.py +3 -1
  17. pyglove/core/symbolic/object_test.py +13 -10
  18. pyglove/core/symbolic/ref.py +19 -7
  19. pyglove/core/symbolic/ref_test.py +94 -7
  20. pyglove/core/symbolic/unknown_symbols.py +147 -0
  21. pyglove/core/symbolic/unknown_symbols_test.py +100 -0
  22. pyglove/core/typing/annotation_conversion.py +8 -1
  23. pyglove/core/typing/annotation_conversion_test.py +14 -19
  24. pyglove/core/typing/class_schema.py +24 -1
  25. pyglove/core/typing/json_schema.py +221 -8
  26. pyglove/core/typing/json_schema_test.py +508 -12
  27. pyglove/core/typing/type_conversion.py +17 -3
  28. pyglove/core/typing/type_conversion_test.py +7 -2
  29. pyglove/core/typing/value_specs.py +5 -1
  30. pyglove/core/typing/value_specs_test.py +5 -0
  31. pyglove/core/utils/__init__.py +2 -0
  32. pyglove/core/utils/contextual.py +9 -4
  33. pyglove/core/utils/contextual_test.py +10 -0
  34. pyglove/core/utils/error_utils.py +59 -25
  35. pyglove/core/utils/json_conversion.py +360 -63
  36. pyglove/core/utils/json_conversion_test.py +146 -13
  37. pyglove/core/views/html/controls/tab.py +33 -0
  38. pyglove/core/views/html/controls/tab_test.py +37 -0
  39. pyglove/ext/evolution/base_test.py +1 -1
  40. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/METADATA +8 -1
  41. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/RECORD +44 -40
  42. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/WHEEL +0 -0
  43. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/licenses/LICENSE +0 -0
  44. {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,657 @@
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. There are three common metrics for monitoring: counters, scalars, and
18
+ distributions.
19
+
20
+ * Counters are metrics that track the number of times an event occurs. It's
21
+ monotonically increasing over time.
22
+
23
+ * Scalars are metrics that track a single value at a given time, for example,
24
+ available memory size. It does not accumulate over time like counters.
25
+
26
+ * Distributions are metrics that track the distribution of a numerical value.
27
+ For example, the latency of an operation.
28
+ """
29
+
30
+ import abc
31
+ import collections
32
+ import contextlib
33
+ import math
34
+ import threading
35
+ import time
36
+ import typing
37
+ from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, Union
38
+ from pyglove.core.utils import error_utils
39
+
40
+ try:
41
+ import numpy # pylint: disable=g-import-not-at-top
42
+ except ImportError:
43
+ numpy = None
44
+
45
+
46
+ MetricValueType = TypeVar('MetricValueType')
47
+
48
+
49
+ class Metric(Generic[MetricValueType], metaclass=abc.ABCMeta):
50
+ """Base class for metrics."""
51
+
52
+ def __init__(
53
+ self,
54
+ namespace: str,
55
+ name: str,
56
+ description: str,
57
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
58
+ **additional_flags,
59
+ ) -> None:
60
+ """Initializes the metric.
61
+
62
+ Args:
63
+ namespace: The namespace of the metric.
64
+ name: The name of the metric.
65
+ description: The description of the metric.
66
+ parameter_definitions: The definitions of the parameters for the metric.
67
+ **additional_flags: Additional flags for the metric.
68
+ """
69
+ self._namespace = namespace
70
+ self._name = name
71
+ self._description = description
72
+ self._parameter_definitions = parameter_definitions
73
+ self._flags = additional_flags
74
+
75
+ @property
76
+ def namespace(self) -> str:
77
+ """Returns the namespace of the metric."""
78
+ return self._namespace
79
+
80
+ @property
81
+ def name(self) -> str:
82
+ """Returns the name of the metric."""
83
+ return self._name
84
+
85
+ @property
86
+ def full_name(self) -> str:
87
+ """Returns the full name of the metric."""
88
+ return f'{self.namespace}/{self.name}'
89
+
90
+ @property
91
+ def description(self) -> str:
92
+ """Returns the description of the metric."""
93
+ return self._description
94
+
95
+ @property
96
+ def parameter_definitions(self) -> Dict[str, Type[Union[int, str, bool]]]:
97
+ """Returns the parameter definitions of the metric."""
98
+ return self._parameter_definitions
99
+
100
+ @property
101
+ def flags(self) -> Dict[str, Any]:
102
+ """Returns the flags of the metric."""
103
+ return self._flags
104
+
105
+ def _parameters_key(self, **parameters) -> Tuple[Any, ...]:
106
+ """Returns the parameters tuple for the metric."""
107
+ for k, t in self._parameter_definitions.items():
108
+ v = parameters.get(k)
109
+ if v is None:
110
+ raise KeyError(
111
+ f'Metric {self.full_name!r}: Parameter {k!r} is required but not '
112
+ f'given.'
113
+ )
114
+ if not isinstance(v, t):
115
+ raise TypeError(
116
+ f'Metric {self.full_name!r}: Parameter {k!r} has type '
117
+ f'{type(v)} but expected type {t}.'
118
+ )
119
+ for k in parameters:
120
+ if k not in self._parameter_definitions:
121
+ raise KeyError(
122
+ f'Metric {self.full_name!r}: Parameter {k!r} is not defined but '
123
+ f'provided. Available parameters: '
124
+ f'{list(self._parameter_definitions.keys())}'
125
+ )
126
+ return tuple(parameters[k] for k in self._parameter_definitions)
127
+
128
+ @abc.abstractmethod
129
+ def value(self, **parameters) -> MetricValueType:
130
+ """Returns the value of the metric for the given parameters.
131
+
132
+ Args:
133
+ **parameters: Parameters for parameterized counters.
134
+
135
+ Returns:
136
+ The value of the metric.
137
+ """
138
+
139
+
140
+ class Counter(Metric[int]):
141
+ """Base class for counters.
142
+
143
+ Counters are metrics that track the number of times an event occurs. It's
144
+ monotonically increasing over time.
145
+ """
146
+
147
+ @abc.abstractmethod
148
+ def increment(self, delta: int = 1, **parameters) -> int:
149
+ """Increments the counter by delta and returns the new value.
150
+
151
+ Args:
152
+ delta: The amount to increment the counter by.
153
+ **parameters: Parameters for parameterized counters.
154
+
155
+ Returns:
156
+ The new value of the counter.
157
+ """
158
+
159
+
160
+ class Scalar(Metric[MetricValueType]):
161
+ """Base class for scalar values.
162
+
163
+ Scalar values are metrics that track a single value at a given time, for
164
+ example, available memory size. It does not accumulate over time like
165
+ counters.
166
+ """
167
+
168
+ @abc.abstractmethod
169
+ def set(self, value: MetricValueType, **parameters) -> None:
170
+ """Sets the value of the scalar.
171
+
172
+ Args:
173
+ value: The value to record.
174
+ **parameters: Parameters for parameterized scalars.
175
+ """
176
+
177
+ @abc.abstractmethod
178
+ def increment(
179
+ self,
180
+ delta: MetricValueType = 1, **parameters
181
+ ) -> MetricValueType:
182
+ """Increments the scalar by delta and returns the new value.
183
+
184
+ Args:
185
+ delta: The amount to increment the counter by.
186
+ **parameters: Parameters for parameterized counters.
187
+
188
+ Returns:
189
+ The new value of the metric.
190
+ """
191
+
192
+
193
+ class DistributionValue(metaclass=abc.ABCMeta):
194
+ """Base for distribution value."""
195
+
196
+ @property
197
+ @abc.abstractmethod
198
+ def count(self) -> int:
199
+ """Returns the number of samples in the distribution."""
200
+
201
+ @property
202
+ @abc.abstractmethod
203
+ def sum(self) -> float:
204
+ """Returns the sum of the distribution."""
205
+
206
+ @property
207
+ @abc.abstractmethod
208
+ def mean(self) -> float:
209
+ """Returns the mean of the distribution."""
210
+
211
+ @property
212
+ @abc.abstractmethod
213
+ def stddev(self) -> float:
214
+ """Returns the standard deviation of the distribution."""
215
+
216
+ @property
217
+ def median(self) -> float:
218
+ """Returns the standard deviation of the distribution."""
219
+ return self.percentile(50)
220
+
221
+ @property
222
+ @abc.abstractmethod
223
+ def variance(self) -> float:
224
+ """Returns the variance of the distribution."""
225
+
226
+ @abc.abstractmethod
227
+ def percentile(self, n: float) -> float:
228
+ """Returns the median of the distribution.
229
+
230
+ Args:
231
+ n: The percentile to return. Should be in the range [0, 100].
232
+
233
+ Returns:
234
+ The n-th percentile of the distribution.
235
+ """
236
+
237
+ @abc.abstractmethod
238
+ def fraction_less_than(self, value: float) -> float:
239
+ """Returns the fraction of values in the distribution less than value."""
240
+
241
+
242
+ class Distribution(Metric[DistributionValue]):
243
+ """Base class for distributional metrics.
244
+
245
+ Distributions are metrics that track the distribution of a numerical value.
246
+ For example, the latency of an operation.
247
+ """
248
+
249
+ @abc.abstractmethod
250
+ def record(self, value: float, **parameters) -> None:
251
+ """Records a value to the distribution.
252
+
253
+ Args:
254
+ value: The value to record.
255
+ **parameters: Parameters for parameterized distributions.
256
+ """
257
+
258
+ @contextlib.contextmanager
259
+ def record_duration(
260
+ self,
261
+ *,
262
+ scale: int = 1000,
263
+ error_parameter: str = 'error',
264
+ **parameters) -> Iterator[None]:
265
+ """Context manager that records the duration of code block.
266
+
267
+ Args:
268
+ scale: The scale of the duration.
269
+ error_parameter: The parameter name for recording the error. If the name
270
+ is not defined as a parameter for the distribution, the error tag will
271
+ not be recorded.
272
+ **parameters: Parameters for parameterized distributions.
273
+ """
274
+ start_time = time.time()
275
+ error = None
276
+ try:
277
+ yield
278
+ except BaseException as e:
279
+ error = e
280
+ raise e
281
+ finally:
282
+ duration = (time.time() - start_time) * scale
283
+ if error_parameter in self._parameter_definitions:
284
+ parameters[error_parameter] = (
285
+ error_utils.ErrorInfo.from_exception(error).tag
286
+ if error is not None else ''
287
+ )
288
+ self.record(duration, **parameters)
289
+
290
+
291
+ class MetricCollection(metaclass=abc.ABCMeta):
292
+ """Base class for counter collections."""
293
+
294
+ _COUNTER_CLASS = Counter
295
+ _SCALAR_CLASS = Scalar
296
+ _DISTRIBUTION_CLASS = Distribution
297
+
298
+ def __init__(
299
+ self,
300
+ namespace: str,
301
+ default_parameters: Optional[
302
+ Dict[str, Type[Union[int, str, bool]]]
303
+ ] = None
304
+ ):
305
+ """Initializes the metric collection.
306
+
307
+ Args:
308
+ namespace: The namespace of the metric collection.
309
+ default_parameters: The default parameters used to create metrics
310
+ if not specified.
311
+ """
312
+ self._namespace = namespace
313
+ self._default_parameter_definitions = default_parameters or {}
314
+ self._metrics = self._metric_container()
315
+
316
+ @property
317
+ def namespace(self) -> str:
318
+ """Returns the namespace of the metric collection."""
319
+ return self._namespace
320
+
321
+ def metrics(self) -> List[Metric]:
322
+ """Returns the names of the metrics."""
323
+ return [m for m in self._metrics.values() if m.namespace == self._namespace]
324
+
325
+ def _metric_container(self) -> dict[str, Metric]:
326
+ """Returns the container for metrics."""
327
+ return {}
328
+
329
+ def _get_or_create_metric(
330
+ self,
331
+ metric_cls: Type[Metric],
332
+ name: str,
333
+ description: str,
334
+ parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
335
+ **additional_flags,
336
+ ) -> Metric:
337
+ """Gets or creates a metric with the given name."""
338
+ full_name = f'{self._namespace}/{name}'
339
+ metric = self._metrics.get(full_name)
340
+ if metric is not None:
341
+ if not isinstance(metric, metric_cls):
342
+ raise ValueError(
343
+ f'Metric {full_name!r} already exists with a different type '
344
+ f'({type(metric)}).'
345
+ )
346
+ if description != metric.description:
347
+ raise ValueError(
348
+ f'Metric {full_name!r} already exists with a different description '
349
+ f'({metric.description!r}).'
350
+ )
351
+ if parameter_definitions != metric.parameter_definitions:
352
+ raise ValueError(
353
+ f'Metric {full_name!r} already exists with different parameter '
354
+ f'definitions ({metric.parameter_definitions!r}).'
355
+ )
356
+ else:
357
+ metric = metric_cls(
358
+ self.namespace,
359
+ name,
360
+ description,
361
+ parameter_definitions,
362
+ **additional_flags
363
+ )
364
+ self._metrics[full_name] = metric
365
+ return metric
366
+
367
+ def get_counter(
368
+ self,
369
+ name: str,
370
+ description: str,
371
+ parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
372
+ **additional_flags
373
+ ) -> Counter:
374
+ """Gets or creates a counter with the given name.
375
+
376
+ Args:
377
+ name: The name of the counter.
378
+ description: The description of the counter.
379
+ parameters: The definitions of the parameters for the counter.
380
+ `default_parameters` from the collection will be used if not specified.
381
+ **additional_flags: Additional arguments for creating the counter.
382
+ Subclasses can use these arguments to provide additional information for
383
+ creating the counter.
384
+
385
+ Returns:
386
+ The counter with the given name.
387
+ """
388
+ if parameters is None:
389
+ parameters = self._default_parameter_definitions
390
+ return typing.cast(
391
+ Counter,
392
+ self._get_or_create_metric(
393
+ self._COUNTER_CLASS,
394
+ name,
395
+ description,
396
+ parameters,
397
+ **additional_flags
398
+ )
399
+ )
400
+
401
+ def get_scalar(
402
+ self,
403
+ name: str,
404
+ description: str,
405
+ parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
406
+ value_type: Type[Union[int, float]] = int,
407
+ **additional_flags
408
+ ) -> Scalar:
409
+ """Gets or creates a scalar with the given name.
410
+
411
+ Args:
412
+ name: The name of the counter.
413
+ description: The description of the counter.
414
+ parameters: The definitions of the parameters for the counter.
415
+ `default_parameters` from the collection will be used if not specified.
416
+ value_type: The type of the value for the scalar.
417
+ **additional_flags: Additional arguments for creating the scalar.
418
+
419
+ Returns:
420
+ The counter with the given name.
421
+ """
422
+ if parameters is None:
423
+ parameters = self._default_parameter_definitions
424
+ return typing.cast(
425
+ Scalar,
426
+ self._get_or_create_metric(
427
+ self._SCALAR_CLASS,
428
+ name,
429
+ description,
430
+ parameters,
431
+ value_type=value_type,
432
+ **additional_flags
433
+ )
434
+ )
435
+
436
+ def get_distribution(
437
+ self,
438
+ name: str,
439
+ description: str,
440
+ parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
441
+ **additional_flags
442
+ ) -> Distribution:
443
+ """Gets or creates a distribution with the given name.
444
+
445
+ Args:
446
+ name: The name of the distribution.
447
+ description: The description of the distribution.
448
+ parameters: The definitions of the parameters for the distribution.
449
+ `default_parameters` from the collection will be used if not specified.
450
+ **additional_flags: Additional arguments for creating the distribution.
451
+
452
+ Returns:
453
+ The distribution with the given name.
454
+ """
455
+ if parameters is None:
456
+ parameters = self._default_parameter_definitions
457
+ return typing.cast(
458
+ Distribution,
459
+ self._get_or_create_metric(
460
+ self._DISTRIBUTION_CLASS,
461
+ name,
462
+ description,
463
+ parameters,
464
+ **additional_flags
465
+ )
466
+ )
467
+
468
+ #
469
+ # InMemoryMetricCollection.
470
+ #
471
+
472
+
473
+ class _InMemoryCounter(Counter):
474
+ """In-memory counter."""
475
+
476
+ def __init__(self, *args, **kwargs):
477
+ super().__init__(*args, **kwargs)
478
+ self._counter = collections.defaultdict(int)
479
+ self._lock = threading.Lock()
480
+
481
+ def increment(self, delta: int = 1, **parameters) -> int:
482
+ """Increments the counter by delta and returns the new value."""
483
+ parameters_key = self._parameters_key(**parameters)
484
+ with self._lock:
485
+ value = self._counter[parameters_key]
486
+ value += delta
487
+ self._counter[parameters_key] = value
488
+ return value
489
+
490
+ def value(self, **parameters) -> int:
491
+ """Returns the value of the counter based on the given parameters."""
492
+ return self._counter[self._parameters_key(**parameters)]
493
+
494
+
495
+ class _InMemoryScalar(Scalar[MetricValueType]):
496
+ """In-memory scalar."""
497
+
498
+ def __init__(self, *args, **kwargs):
499
+ super().__init__(*args, **kwargs)
500
+ self._values = collections.defaultdict(self.flags['value_type'])
501
+ self._lock = threading.Lock()
502
+
503
+ def increment(
504
+ self,
505
+ delta: MetricValueType = 1,
506
+ **parameters
507
+ ) -> MetricValueType:
508
+ """Increments the scalar by delta and returns the new value."""
509
+ parameters_key = self._parameters_key(**parameters)
510
+ with self._lock:
511
+ value = self._values[parameters_key]
512
+ value += delta
513
+ self._values[parameters_key] = value
514
+ return value
515
+
516
+ def set(self, value: MetricValueType, **parameters) -> None:
517
+ """Sets the value of the scalar."""
518
+ parameters_key = self._parameters_key(**parameters)
519
+ with self._lock:
520
+ self._values[parameters_key] = value
521
+
522
+ def value(self, **parameters) -> MetricValueType:
523
+ """Returns the distribution of the scalar."""
524
+ return self._values[self._parameters_key(**parameters)]
525
+
526
+
527
+ class _InMemoryDistribution(Distribution):
528
+ """In-memory distribution."""
529
+
530
+ def __init__(self, *args, window_size: int = 1024 * 1024, **kwargs):
531
+ super().__init__(*args, **kwargs)
532
+ self._window_size = window_size
533
+ self._distributions = collections.defaultdict(
534
+ lambda: _InMemoryDistributionValue(self._window_size)
535
+ )
536
+ self._lock = threading.Lock()
537
+
538
+ def record(self, value: Any, **parameters) -> None:
539
+ """Records a value to the scalar."""
540
+ parameters_key = self._parameters_key(**parameters)
541
+ self._distributions[parameters_key].add(value)
542
+
543
+ def value(self, **parameters) -> DistributionValue:
544
+ """Returns the distribution of the scalar."""
545
+ return self._distributions[self._parameters_key(**parameters)]
546
+
547
+
548
+ class _InMemoryDistributionValue(DistributionValue):
549
+ """In memory distribution value."""
550
+
551
+ def __init__(self, window_size: int = 1024 * 1024):
552
+ self._window_size = window_size
553
+ self._data = []
554
+ self._sum = 0.0
555
+ self._square_sum = 0.0
556
+ self._count = 0
557
+ self._lock = threading.Lock()
558
+
559
+ def add(self, value: int):
560
+ """Adds a value to the distribution."""
561
+ with self._lock:
562
+ if len(self._data) == self._window_size:
563
+ x = self._data.pop(0)
564
+ self._sum -= x
565
+ self._count -= 1
566
+ self._square_sum -= x ** 2
567
+
568
+ self._data.append(value)
569
+ self._count += 1
570
+ self._sum += value
571
+ self._square_sum += value ** 2
572
+
573
+ @property
574
+ def count(self) -> int:
575
+ """Returns the number of samples in the distribution."""
576
+ return self._count
577
+
578
+ @property
579
+ def sum(self) -> float:
580
+ """Returns the sum of the distribution."""
581
+ return self._sum
582
+
583
+ @property
584
+ def mean(self) -> float:
585
+ """Returns the mean of the distribution."""
586
+ return self._sum / self._count if self._count else 0.0
587
+
588
+ @property
589
+ def stddev(self) -> float:
590
+ """Returns the standard deviation of the distribution."""
591
+ return math.sqrt(self.variance)
592
+
593
+ @property
594
+ def variance(self) -> float:
595
+ """Returns the variance of the distribution."""
596
+ if self._count < 2:
597
+ return 0.0
598
+ return self._square_sum / self._count - self.mean ** 2
599
+
600
+ def percentile(self, n: float) -> float:
601
+ """Returns the median of the distribution."""
602
+ if n < 0 or n > 100:
603
+ raise ValueError(f'Percentile {n} is not in the range [0, 100].')
604
+
605
+ if self._count == 0:
606
+ return 0.0
607
+
608
+ if numpy is not None:
609
+ return numpy.percentile(self._data, n) # pytype: disable=attribute-error
610
+
611
+ sorted_data = sorted(self._data)
612
+ index = (n / 100) * (len(sorted_data) - 1)
613
+ if index % 1 == 0:
614
+ return sorted_data[int(index)]
615
+ else:
616
+ # Interpolate the value at the given percentile.
617
+ lower_index = int(index)
618
+ fraction = index - lower_index
619
+
620
+ # Get the values at the two surrounding integer indices
621
+ lower_value = sorted_data[lower_index]
622
+ upper_value = sorted_data[lower_index + 1]
623
+ return lower_value + fraction * (upper_value - lower_value)
624
+
625
+ def fraction_less_than(self, value: float) -> float:
626
+ """Returns the fraction of values in the distribution less than value."""
627
+ if self._count == 0:
628
+ return 0.0
629
+ with self._lock:
630
+ return len([x for x in self._data if x < value]) / self._count
631
+
632
+
633
+ class InMemoryMetricCollection(MetricCollection):
634
+ """In-memory counter."""
635
+
636
+ _COUNTER_CLASS = _InMemoryCounter
637
+ _SCALAR_CLASS = _InMemoryScalar
638
+ _DISTRIBUTION_CLASS = _InMemoryDistribution
639
+
640
+
641
+ _METRIC_COLLECTION_CLS = InMemoryMetricCollection # pylint: disable=invalid-name
642
+
643
+
644
+ def metric_collection(namespace: str, **kwargs) -> MetricCollection:
645
+ """Creates a metric collection."""
646
+ return _METRIC_COLLECTION_CLS(namespace, **kwargs)
647
+
648
+
649
+ def set_default_metric_collection_cls(cls: Type[MetricCollection]) -> None:
650
+ """Sets the default metric collection class."""
651
+ global _METRIC_COLLECTION_CLS
652
+ _METRIC_COLLECTION_CLS = cls
653
+
654
+
655
+ def default_metric_collection_cls() -> Type[MetricCollection]:
656
+ """Returns the default metric collection class."""
657
+ return _METRIC_COLLECTION_CLS