lsst-utils 29.2025.3900__tar.gz → 29.2025.4100__tar.gz

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 (64) hide show
  1. {lsst_utils-29.2025.3900/python/lsst_utils.egg-info → lsst_utils-29.2025.4100}/PKG-INFO +2 -1
  2. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/pyproject.toml +1 -0
  3. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/logging.py +29 -1
  4. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/timer.py +33 -5
  5. lsst_utils-29.2025.4100/python/lsst/utils/version.py +2 -0
  6. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100/python/lsst_utils.egg-info}/PKG-INFO +2 -1
  7. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst_utils.egg-info/requires.txt +1 -0
  8. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_timer.py +37 -0
  9. lsst_utils-29.2025.3900/python/lsst/utils/version.py +0 -2
  10. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/COPYRIGHT +0 -0
  11. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/LICENSE +0 -0
  12. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/MANIFEST.in +0 -0
  13. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/README.rst +0 -0
  14. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/doc/lsst.utils/CHANGES.rst +0 -0
  15. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/doc/lsst.utils/index.rst +0 -0
  16. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/__init__.py +0 -0
  17. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/__init__.py +0 -0
  18. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/_packaging.py +0 -0
  19. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/argparsing.py +0 -0
  20. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/classes.py +0 -0
  21. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/db_auth.py +0 -0
  22. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/deprecated.py +0 -0
  23. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/doImport.py +0 -0
  24. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/inheritDoc.py +0 -0
  25. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/introspection.py +0 -0
  26. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/iteration.py +0 -0
  27. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/packages.py +0 -0
  28. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/plotting/__init__.py +0 -0
  29. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/plotting/figures.py +0 -0
  30. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/plotting/limits.py +0 -0
  31. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/plotting/publication_plots.py +0 -0
  32. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/plotting/rubin.mplstyle +0 -0
  33. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/py.typed +0 -0
  34. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/tests.py +0 -0
  35. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/threads.py +0 -0
  36. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/usage.py +0 -0
  37. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst/utils/wrappers.py +0 -0
  38. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst_utils.egg-info/SOURCES.txt +0 -0
  39. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst_utils.egg-info/dependency_links.txt +0 -0
  40. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst_utils.egg-info/top_level.txt +0 -0
  41. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/python/lsst_utils.egg-info/zip-safe +0 -0
  42. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/setup.cfg +0 -0
  43. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_argparsing.py +0 -0
  44. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_classes.py +0 -0
  45. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_db_auth.py +0 -0
  46. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_decorators.py +0 -0
  47. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_deprecated.py +0 -0
  48. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_doImport.py +0 -0
  49. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_executables.py +0 -0
  50. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_getPackageDir.py +0 -0
  51. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_getTempFilePath.py +0 -0
  52. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_import.py +0 -0
  53. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_inheritDoc.py +0 -0
  54. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_introspection.py +0 -0
  55. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_iteration.py +0 -0
  56. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_logging.py +0 -0
  57. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_matplotlib_figures.py +0 -0
  58. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_ordering.py +0 -0
  59. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_packages.py +0 -0
  60. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_plotting_limits.py +0 -0
  61. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_threads.py +0 -0
  62. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_usage.py +0 -0
  63. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_utils.py +0 -0
  64. {lsst_utils-29.2025.3900 → lsst_utils-29.2025.4100}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-utils
3
- Version: 29.2025.3900
3
+ Version: 29.2025.4100
4
4
  Summary: Utility functions from Rubin Observatory Data Management for the Legacy Survey of Space and Time (LSST).
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License: BSD 3-Clause License
@@ -22,6 +22,7 @@ Requires-Dist: psutil>=5.7
22
22
  Requires-Dist: deprecated>=1.2
23
23
  Requires-Dist: pyyaml>=5.1
24
24
  Requires-Dist: astropy>=5.0
25
+ Requires-Dist: structlog
25
26
  Requires-Dist: threadpoolctl
26
27
  Provides-Extra: test
27
28
  Requires-Dist: pytest>=3.2; extra == "test"
@@ -27,6 +27,7 @@ dependencies = [
27
27
  "deprecated >= 1.2",
28
28
  "pyyaml >= 5.1",
29
29
  "astropy >= 5.0",
30
+ "structlog",
30
31
  "threadpoolctl",
31
32
  ]
32
33
  dynamic = ["version"]
@@ -28,13 +28,24 @@ import time
28
28
  from collections.abc import Generator
29
29
  from contextlib import contextmanager
30
30
  from logging import LoggerAdapter
31
- from typing import Any, TypeAlias
31
+ from typing import TYPE_CHECKING, Any, TypeAlias, TypeGuard
32
32
 
33
33
  try:
34
34
  import lsst.log.utils as logUtils
35
35
  except ImportError:
36
36
  logUtils = None
37
37
 
38
+ try:
39
+ from structlog import get_context as get_structlog_context
40
+ except ImportError:
41
+ get_structlog_context = None # type: ignore[assignment]
42
+
43
+
44
+ if TYPE_CHECKING:
45
+ try:
46
+ from structlog.typing import BindableLogger
47
+ except ImportError:
48
+ BindableLogger: TypeAlias = Any # type: ignore[no-redef]
38
49
 
39
50
  # log level for trace (verbose debug).
40
51
  TRACE = 5
@@ -45,6 +56,23 @@ VERBOSE = (logging.INFO + logging.DEBUG) // 2
45
56
  logging.addLevelName(VERBOSE, "VERBOSE")
46
57
 
47
58
 
59
+ def _is_structlog_logger(
60
+ logger: logging.Logger | LsstLogAdapter | BindableLogger,
61
+ ) -> TypeGuard[BindableLogger]:
62
+ """Check if the given logger is a structlog logger."""
63
+ if get_structlog_context is None:
64
+ return False # type: ignore[unreachable]
65
+
66
+ try:
67
+ # Returns a dict for structlog loggers; raises for stdlib logger
68
+ # objects.
69
+ get_structlog_context(logger) # type: ignore[arg-type]
70
+ return True
71
+ except Exception:
72
+ # In practice this is usually ValueError or AttributeError.
73
+ return False
74
+
75
+
48
76
  def _calculate_base_stacklevel(default: int, offset: int) -> int:
49
77
  """Calculate the default logging stacklevel to use.
50
78
 
@@ -32,7 +32,7 @@ from typing import TYPE_CHECKING, Any, TypeVar, overload
32
32
  from astropy import units as u
33
33
 
34
34
  from .introspection import find_outside_stacklevel
35
- from .logging import LsstLoggers
35
+ from .logging import LsstLoggers, _is_structlog_logger
36
36
  from .usage import _get_current_rusage, get_current_mem_usage, get_peak_mem_usage
37
37
 
38
38
  if TYPE_CHECKING:
@@ -374,9 +374,10 @@ def time_this(
374
374
  level: int = logging.DEBUG,
375
375
  prefix: str | None = "timer",
376
376
  args: Iterable[Any] = (),
377
+ kwargs: dict[str, Any] | None = None,
377
378
  mem_usage: bool = False,
378
379
  mem_child: bool = False,
379
- mem_unit: u.Quantity = u.byte,
380
+ mem_unit: u.Unit = u.byte,
380
381
  mem_fmt: str = ".0f",
381
382
  force_mem_usage: bool = False,
382
383
  ) -> Iterator[_TimerResult]:
@@ -386,7 +387,8 @@ def time_this(
386
387
  ----------
387
388
  log : `logging.Logger`, optional
388
389
  Logger to use to report the timer message. The root logger will
389
- be used if none is given.
390
+ be used if none is given. Is also allowed to be a `structlog` bound
391
+ logger.
390
392
  msg : `str`, optional
391
393
  Context to include in log message.
392
394
  level : `int`, optional
@@ -400,6 +402,11 @@ def time_this(
400
402
  args : iterable of any
401
403
  Additional parameters passed to the log command that should be
402
404
  written to ``msg``.
405
+ kwargs : `dict`, optional
406
+ Additional keyword parameters passed to the log command. If a Structlog
407
+ is used then these will be added to the structured data. Otherwise
408
+ they will be converted to a single string for inclusion in the log
409
+ message.
403
410
  mem_usage : `bool`, optional
404
411
  Flag indicating whether to include the memory usage in the report.
405
412
  Defaults, to False. Does nothing if the log message will not be
@@ -424,10 +431,17 @@ def time_this(
424
431
  """
425
432
  if log is None:
426
433
  log = logging.getLogger()
427
- if prefix:
434
+ is_structlog = _is_structlog_logger(log)
435
+ if prefix and not is_structlog:
436
+ # Struct log loggers do not have a name property and so the prefix
437
+ # is not applied to them.
428
438
  log_name = f"{prefix}.{log.name}" if not isinstance(log, logging.RootLogger) else prefix
429
439
  log = logging.getLogger(log_name)
430
440
 
441
+ # Some structured data that can be used if we have been given a
442
+ # structlog logger.
443
+ structured_args: dict[str, Any] = {}
444
+
431
445
  start = time.time()
432
446
 
433
447
  if mem_usage and not log.isEnabledFor(level):
@@ -467,6 +481,7 @@ def time_this(
467
481
  # caller (1 is this file, 2 is contextlib, 3 is user)
468
482
  params += (": " if msg else "", duration)
469
483
  msg += "%sTook %.4f seconds"
484
+ structured_args["duration"] = duration
470
485
  if errmsg:
471
486
  params += (f" (timed code triggered exception of {errmsg!r})",)
472
487
  msg += "%s"
@@ -502,7 +517,20 @@ def time_this(
502
517
  f", delta: {current_delta:{mem_fmt}}"
503
518
  f", peak delta: {peak_delta:{mem_fmt}}"
504
519
  )
505
- log.log(level, msg, *params, stacklevel=3)
520
+ structured_args["mem_current_usage"] = float(current_usage.value)
521
+ structured_args["mem_current_delta"] = float(current_delta.value)
522
+ structured_args["mem_peak_delta"] = float(peak_delta.value)
523
+ if not is_structlog:
524
+ # Can only use the structured content if we have structlog logger
525
+ # but stacklevel is only supported by standard loggers.
526
+ structured_args = {"stacklevel": 3}
527
+ if kwargs is not None:
528
+ msg += " %s"
529
+ params += ("; ".join(f"{k}={v!r}" for k, v in kwargs.items()),)
530
+ elif kwargs:
531
+ structured_args.update(kwargs)
532
+
533
+ log.log(level, msg, *params, **structured_args)
506
534
 
507
535
 
508
536
  @contextmanager
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "29.2025.4100"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-utils
3
- Version: 29.2025.3900
3
+ Version: 29.2025.4100
4
4
  Summary: Utility functions from Rubin Observatory Data Management for the Legacy Survey of Space and Time (LSST).
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License: BSD 3-Clause License
@@ -22,6 +22,7 @@ Requires-Dist: psutil>=5.7
22
22
  Requires-Dist: deprecated>=1.2
23
23
  Requires-Dist: pyyaml>=5.1
24
24
  Requires-Dist: astropy>=5.0
25
+ Requires-Dist: structlog
25
26
  Requires-Dist: threadpoolctl
26
27
  Provides-Extra: test
27
28
  Requires-Dist: pytest>=3.2; extra == "test"
@@ -3,6 +3,7 @@ psutil>=5.7
3
3
  deprecated>=1.2
4
4
  pyyaml>=5.1
5
5
  astropy>=5.0
6
+ structlog
6
7
  threadpoolctl
7
8
 
8
9
  [plotting]
@@ -319,6 +319,22 @@ class TimerTestCase(unittest.TestCase):
319
319
  self.assertIn("Took", cm.output[0])
320
320
  self.assertIn(msg % test_num, cm.output[0])
321
321
 
322
+ # Now with kwargs.
323
+ msg = "Test message %d"
324
+ test_num = 42
325
+ logname = "test"
326
+ kwargs = {"extra_info": "extra", "value": 3}
327
+ with self.assertLogs(level="DEBUG") as cm:
328
+ with time_this(
329
+ log=logging.getLogger(logname), msg=msg, args=(test_num,), kwargs=kwargs, prefix=None
330
+ ):
331
+ pass
332
+ self.assertEqual(cm.records[0].name, logname)
333
+ self.assertIn("Took", cm.output[0])
334
+ self.assertIn(msg % test_num, cm.output[0])
335
+ self.assertIn("extra_info='extra'", cm.output[0])
336
+ self.assertIn("value=3", cm.output[0])
337
+
322
338
  # Prefix the logger.
323
339
  prefix = "prefix"
324
340
  with self.assertLogs(level="DEBUG") as cm:
@@ -374,6 +390,27 @@ class TimerTestCase(unittest.TestCase):
374
390
  self.assertGreater(timer.duration, 0.0)
375
391
  self.assertIsInstance(timer.mem_current_usage, float)
376
392
 
393
+ def test_structlog(self):
394
+ """Test that the timer works with structlog loggers."""
395
+ try:
396
+ import structlog
397
+ from structlog.testing import capture_logs
398
+ except ImportError:
399
+ self.skipTest("structlog is not installed")
400
+
401
+ msg = "Test message %d"
402
+ test_num = 42
403
+ kwargs = {"extra_info": "extra", "value": 3}
404
+
405
+ with capture_logs() as cap:
406
+ slog = structlog.get_logger("structlog_timer")
407
+ with time_this(log=slog, msg=msg, args=(test_num,), kwargs=kwargs):
408
+ pass
409
+ self.assertEqual(cap[0]["value"], 3)
410
+ self.assertEqual(cap[0]["extra_info"], "extra")
411
+ self.assertGreaterEqual(cap[0]["duration"], 0.0)
412
+ self.assertIn("Test message 42", cap[0]["event"])
413
+
377
414
 
378
415
  class ProfileTestCase(unittest.TestCase):
379
416
  """Test profiling decorator."""
@@ -1,2 +0,0 @@
1
- __all__ = ["__version__"]
2
- __version__ = "29.2025.3900"