BDPotentiometer 0.2.0__tar.gz → 0.4.0__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 (36) hide show
  1. {bdpotentiometer-0.2.0/src/BDPotentiometer.egg-info → bdpotentiometer-0.4.0}/PKG-INFO +60 -21
  2. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/README.md +53 -13
  3. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/pyproject.toml +8 -9
  4. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/__helpers.py +6 -7
  5. bdpotentiometer-0.4.0/src/BDPotentiometer/__init__.py +15 -0
  6. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/__logger.py +2 -3
  7. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/digital_potentiometer.py +38 -39
  8. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/digital_wiper.py +10 -9
  9. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/mcp4xxx/mcp4xx1.py +9 -8
  10. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/mcp4xxx/mcp4xx2.py +9 -8
  11. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/mcp4xxx/mcp4xxx.py +16 -11
  12. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0/src/BDPotentiometer.egg-info}/PKG-INFO +60 -21
  13. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer.egg-info/SOURCES.txt +0 -8
  14. bdpotentiometer-0.4.0/src/BDPotentiometer.egg-info/requires.txt +25 -0
  15. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/tests/test_backward_compatibility.py +27 -65
  16. bdpotentiometer-0.4.0/tests/test_optional_backends.py +62 -0
  17. bdpotentiometer-0.2.0/src/BDPotentiometer/__gpiozero_helpers.py +0 -160
  18. bdpotentiometer-0.2.0/src/BDPotentiometer/__init__.py +0 -62
  19. bdpotentiometer-0.2.0/src/BDPotentiometer/transport/__init__.py +0 -58
  20. bdpotentiometer-0.2.0/src/BDPotentiometer/transport/base.py +0 -72
  21. bdpotentiometer-0.2.0/src/BDPotentiometer/transport/fake.py +0 -97
  22. bdpotentiometer-0.2.0/src/BDPotentiometer/transport/gpiozero_spi.py +0 -122
  23. bdpotentiometer-0.2.0/src/BDPotentiometer/transport/selection.py +0 -82
  24. bdpotentiometer-0.2.0/src/BDPotentiometer/transport/spidev_spi.py +0 -114
  25. bdpotentiometer-0.2.0/src/BDPotentiometer.egg-info/requires.txt +0 -25
  26. bdpotentiometer-0.2.0/tests/test_gpiozero_helpers.py +0 -174
  27. bdpotentiometer-0.2.0/tests/test_optional_backends.py +0 -145
  28. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/LICENSE +0 -0
  29. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/setup.cfg +0 -0
  30. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/mcp4xxx/__init__.py +0 -0
  31. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/potentiometer.py +0 -0
  32. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer/py.typed +0 -0
  33. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer.egg-info/dependency_links.txt +0 -0
  34. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/src/BDPotentiometer.egg-info/top_level.txt +0 -0
  35. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/tests/test_logger.py +0 -0
  36. {bdpotentiometer-0.2.0 → bdpotentiometer-0.4.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: BDPotentiometer
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: BD Digital Potentiometer
5
5
  Author-email: Anton Bondarenko <bond.anton@gmail.com>
6
6
  License: MIT License
@@ -28,27 +28,26 @@ Project-URL: Homepage, https://github.com/bond-anton/BDPotentiometer
28
28
  Project-URL: Bug Tracker, https://github.com/bond-anton/BDPotentiometer/issues
29
29
  Project-URL: Documentation, https://bdpotentiometer.readthedocs.io
30
30
  Classifier: Programming Language :: Python :: 3
31
- Classifier: Programming Language :: Python :: 3.9
32
31
  Classifier: Programming Language :: Python :: 3.10
33
32
  Classifier: Programming Language :: Python :: 3.11
34
33
  Classifier: Programming Language :: Python :: 3.12
35
34
  Classifier: License :: OSI Approved :: MIT License
36
35
  Classifier: Operating System :: OS Independent
37
- Requires-Python: >=3.9
36
+ Requires-Python: >=3.10
38
37
  Description-Content-Type: text/markdown
39
38
  License-File: LICENSE
39
+ Requires-Dist: scietex.hal.spi<1.0.0,>=0.1.0
40
40
  Provides-Extra: gpiozero
41
- Requires-Dist: gpiozero>=1.6.2; extra == "gpiozero"
41
+ Requires-Dist: scietex.hal.spi[gpiozero]; extra == "gpiozero"
42
42
  Provides-Extra: spi
43
- Requires-Dist: spidev>=3.6; extra == "spi"
43
+ Requires-Dist: scietex.hal.spi[spi]; extra == "spi"
44
44
  Provides-Extra: dev
45
- Requires-Dist: gpiozero>=1.6.2; extra == "dev"
46
- Requires-Dist: spidev>=3.6; extra == "dev"
45
+ Requires-Dist: scietex.hal.spi[gpiozero,spi]; extra == "dev"
47
46
  Requires-Dist: numpy; extra == "dev"
48
47
  Provides-Extra: docs
49
48
  Requires-Dist: sphinx; extra == "docs"
50
49
  Requires-Dist: sphinx_rtd_theme; extra == "docs"
51
- Requires-Dist: gpiozero>=1.6.2; extra == "docs"
50
+ Requires-Dist: scietex.hal.spi[gpiozero]; extra == "docs"
52
51
  Requires-Dist: numpy; extra == "docs"
53
52
  Provides-Extra: test
54
53
  Requires-Dist: pytest; extra == "test"
@@ -76,14 +75,15 @@ The bare package installs the core library only: the model classes and
76
75
  $ pip install BDPotentiometer
77
76
  ```
78
77
 
79
- The default hardware backend is spidev; the `spi` extra installs it:
78
+ The default hardware backend is spidev; the transport package's `spi` extra
79
+ installs it:
80
80
  ```shell
81
- $ pip install 'BDPotentiometer[spi]'
81
+ $ pip install 'scietex.hal.spi[spi]'
82
82
  ```
83
83
 
84
84
  The optional gpiozero backend needs its own extra:
85
85
  ```shell
86
- $ pip install 'BDPotentiometer[gpiozero]'
86
+ $ pip install 'scietex.hal.spi[gpiozero]'
87
87
  ```
88
88
 
89
89
  ## Usage
@@ -112,20 +112,22 @@ or `spi=`; doing so raises `TypeError`.
112
112
 
113
113
  ## Transports
114
114
 
115
- SPI access goes through a transport object passed with `transport=`:
115
+ The transport layer lives in the `scietex.hal.spi` distribution
116
+ (https://github.com/bond-anton/scietex.hal.spi). SPI access goes through a
117
+ transport object passed with `transport=`:
116
118
 
117
119
  * `SpiDevTransport` (default) opens `/dev/spidev<bus>.<device>` through
118
- `spidev`; needs the `spi` extra. `port=` maps to the bus, `device=` to the
119
- chip-select line.
120
+ `spidev`; needs `scietex.hal.spi[spi]`. `port=` maps to the bus, `device=` to
121
+ the chip-select line.
120
122
  * `GpiozeroSpiTransport` (optional) wraps gpiozero hardware SPI; passing
121
123
  `clock_pin=`, `mosi_pin=`, `miso_pin=`, or `select_pin=` builds one, and it
122
- needs the `gpiozero` extra.
124
+ needs `scietex.hal.spi[gpiozero]`.
123
125
  * `FakeSpiTransport` replays scripted responses, so code can run without
124
126
  hardware or extras.
125
127
 
126
128
  ```python
127
129
  from BDPotentiometer.mcp4xxx import MCP4231
128
- from BDPotentiometer.transport import SpiDevTransport
130
+ from scietex.hal.spi import SpiDevTransport
129
131
 
130
132
 
131
133
  my_pot = MCP4231(r_ab=10e3, transport=SpiDevTransport(bus=0, device=0))
@@ -134,7 +136,8 @@ my_pot = MCP4231(r_ab=10e3, transport=SpiDevTransport(bus=0, device=0))
134
136
  For tests, the fake backend needs no extra:
135
137
 
136
138
  ```python
137
- from BDPotentiometer.transport import FakeSpiTransport
139
+ from BDPotentiometer.mcp4xxx import MCP4231
140
+ from scietex.hal.spi import FakeSpiTransport
138
141
 
139
142
 
140
143
  my_pot = MCP4231(r_ab=10e3, transport=FakeSpiTransport(default=[0, 0]))
@@ -145,10 +148,46 @@ my_pot = MCP4231(r_ab=10e3, transport=FakeSpiTransport(default=[0, 0]))
145
148
  `spidev.SpiDev` or a gpiozero SPI object), and `.close()` / `.closed` release
146
149
  and report the underlying transport.
147
150
 
148
- `from BDPotentiometer import *` follows `__all__`, which includes
149
- `GpiozeroSpiTransport`. That name is resolved lazily, so a star import without
150
- the `gpiozero` extra raises `AttributeError`; import the names you need
151
- explicitly when the optional backend may be absent.
151
+ Transports are owned by whoever constructs them: a device closes only a
152
+ transport it built itself from backend keywords, and leaves a transport passed
153
+ with `transport=` or `spi=` open.
154
+
155
+ `from BDPotentiometer import *` follows `__all__`, which lists only the model
156
+ classes: `DigitalPotentiometerDevice`, `DigitalWiper`, `Potentiometer`, and
157
+ `SpiDigitalWiper`. Import transports and transport exceptions from
158
+ `scietex.hal.spi`.
159
+
160
+ ## Upgrading to 0.4.0
161
+
162
+ 0.4.0 moves the transport layer out of this package into the published
163
+ `scietex.hal.spi` 0.1.0 distribution.
164
+
165
+ Breaking changes:
166
+
167
+ * `BDPotentiometer.transport` is gone; `scietex.hal.spi` replaces it.
168
+ * These names are no longer importable from `BDPotentiometer`: `SpiTransport`,
169
+ `SpiDevTransport`, `FakeSpiTransport`, `I2cTransport`, `TransportError`,
170
+ `TransportClosed`, `is_spi_transport`, `fix_gpiozero_dual_cs_spi`, and
171
+ `GpiozeroSpiTransport`.
172
+ * `__all__` now lists only `DigitalPotentiometerDevice`, `DigitalWiper`,
173
+ `Potentiometer`, and `SpiDigitalWiper`.
174
+ * The `ImportError` for a missing backend now names `scietex.hal.spi[spi]` or
175
+ `scietex.hal.spi[gpiozero]` instead of the `BDPotentiometer` extras.
176
+
177
+ Migration:
178
+
179
+ ```python
180
+ # before
181
+ from BDPotentiometer.transport import SpiDevTransport
182
+
183
+ # after
184
+ from scietex.hal.spi import SpiDevTransport
185
+ ```
186
+
187
+ Install the matching transport extra, for example
188
+ `pip install 'scietex.hal.spi[spi]'`. Device constructors, the
189
+ `transport=`/`spi=`/pin keywords, `.close()`, `.closed`, `.transport`, and
190
+ `.spi` are unchanged.
152
191
 
153
192
  Please see the [examples](examples) directory for more usage examples.
154
193
 
@@ -16,14 +16,15 @@ The bare package installs the core library only: the model classes and
16
16
  $ pip install BDPotentiometer
17
17
  ```
18
18
 
19
- The default hardware backend is spidev; the `spi` extra installs it:
19
+ The default hardware backend is spidev; the transport package's `spi` extra
20
+ installs it:
20
21
  ```shell
21
- $ pip install 'BDPotentiometer[spi]'
22
+ $ pip install 'scietex.hal.spi[spi]'
22
23
  ```
23
24
 
24
25
  The optional gpiozero backend needs its own extra:
25
26
  ```shell
26
- $ pip install 'BDPotentiometer[gpiozero]'
27
+ $ pip install 'scietex.hal.spi[gpiozero]'
27
28
  ```
28
29
 
29
30
  ## Usage
@@ -52,20 +53,22 @@ or `spi=`; doing so raises `TypeError`.
52
53
 
53
54
  ## Transports
54
55
 
55
- SPI access goes through a transport object passed with `transport=`:
56
+ The transport layer lives in the `scietex.hal.spi` distribution
57
+ (https://github.com/bond-anton/scietex.hal.spi). SPI access goes through a
58
+ transport object passed with `transport=`:
56
59
 
57
60
  * `SpiDevTransport` (default) opens `/dev/spidev<bus>.<device>` through
58
- `spidev`; needs the `spi` extra. `port=` maps to the bus, `device=` to the
59
- chip-select line.
61
+ `spidev`; needs `scietex.hal.spi[spi]`. `port=` maps to the bus, `device=` to
62
+ the chip-select line.
60
63
  * `GpiozeroSpiTransport` (optional) wraps gpiozero hardware SPI; passing
61
64
  `clock_pin=`, `mosi_pin=`, `miso_pin=`, or `select_pin=` builds one, and it
62
- needs the `gpiozero` extra.
65
+ needs `scietex.hal.spi[gpiozero]`.
63
66
  * `FakeSpiTransport` replays scripted responses, so code can run without
64
67
  hardware or extras.
65
68
 
66
69
  ```python
67
70
  from BDPotentiometer.mcp4xxx import MCP4231
68
- from BDPotentiometer.transport import SpiDevTransport
71
+ from scietex.hal.spi import SpiDevTransport
69
72
 
70
73
 
71
74
  my_pot = MCP4231(r_ab=10e3, transport=SpiDevTransport(bus=0, device=0))
@@ -74,7 +77,8 @@ my_pot = MCP4231(r_ab=10e3, transport=SpiDevTransport(bus=0, device=0))
74
77
  For tests, the fake backend needs no extra:
75
78
 
76
79
  ```python
77
- from BDPotentiometer.transport import FakeSpiTransport
80
+ from BDPotentiometer.mcp4xxx import MCP4231
81
+ from scietex.hal.spi import FakeSpiTransport
78
82
 
79
83
 
80
84
  my_pot = MCP4231(r_ab=10e3, transport=FakeSpiTransport(default=[0, 0]))
@@ -85,10 +89,46 @@ my_pot = MCP4231(r_ab=10e3, transport=FakeSpiTransport(default=[0, 0]))
85
89
  `spidev.SpiDev` or a gpiozero SPI object), and `.close()` / `.closed` release
86
90
  and report the underlying transport.
87
91
 
88
- `from BDPotentiometer import *` follows `__all__`, which includes
89
- `GpiozeroSpiTransport`. That name is resolved lazily, so a star import without
90
- the `gpiozero` extra raises `AttributeError`; import the names you need
91
- explicitly when the optional backend may be absent.
92
+ Transports are owned by whoever constructs them: a device closes only a
93
+ transport it built itself from backend keywords, and leaves a transport passed
94
+ with `transport=` or `spi=` open.
95
+
96
+ `from BDPotentiometer import *` follows `__all__`, which lists only the model
97
+ classes: `DigitalPotentiometerDevice`, `DigitalWiper`, `Potentiometer`, and
98
+ `SpiDigitalWiper`. Import transports and transport exceptions from
99
+ `scietex.hal.spi`.
100
+
101
+ ## Upgrading to 0.4.0
102
+
103
+ 0.4.0 moves the transport layer out of this package into the published
104
+ `scietex.hal.spi` 0.1.0 distribution.
105
+
106
+ Breaking changes:
107
+
108
+ * `BDPotentiometer.transport` is gone; `scietex.hal.spi` replaces it.
109
+ * These names are no longer importable from `BDPotentiometer`: `SpiTransport`,
110
+ `SpiDevTransport`, `FakeSpiTransport`, `I2cTransport`, `TransportError`,
111
+ `TransportClosed`, `is_spi_transport`, `fix_gpiozero_dual_cs_spi`, and
112
+ `GpiozeroSpiTransport`.
113
+ * `__all__` now lists only `DigitalPotentiometerDevice`, `DigitalWiper`,
114
+ `Potentiometer`, and `SpiDigitalWiper`.
115
+ * The `ImportError` for a missing backend now names `scietex.hal.spi[spi]` or
116
+ `scietex.hal.spi[gpiozero]` instead of the `BDPotentiometer` extras.
117
+
118
+ Migration:
119
+
120
+ ```python
121
+ # before
122
+ from BDPotentiometer.transport import SpiDevTransport
123
+
124
+ # after
125
+ from scietex.hal.spi import SpiDevTransport
126
+ ```
127
+
128
+ Install the matching transport extra, for example
129
+ `pip install 'scietex.hal.spi[spi]'`. Device constructors, the
130
+ `transport=`/`spi=`/pin keywords, `.close()`, `.closed`, `.transport`, and
131
+ `.spi` are unchanged.
92
132
 
93
133
  Please see the [examples](examples) directory for more usage examples.
94
134
 
@@ -13,15 +13,14 @@ license = { file = "LICENSE" }
13
13
  readme = "README.md"
14
14
  classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
- "Programming Language :: Python :: 3.9",
17
16
  "Programming Language :: Python :: 3.10",
18
17
  "Programming Language :: Python :: 3.11",
19
18
  "Programming Language :: Python :: 3.12",
20
19
  "License :: OSI Approved :: MIT License",
21
20
  "Operating System :: OS Independent",
22
21
  ]
23
- requires-python = ">=3.9"
24
- dependencies = []
22
+ requires-python = ">=3.10"
23
+ dependencies = ["scietex.hal.spi >= 0.1.0, < 1.0.0"]
25
24
 
26
25
  [project.urls]
27
26
  "Homepage" = "https://github.com/bond-anton/BDPotentiometer"
@@ -29,10 +28,10 @@ dependencies = []
29
28
  "Documentation" = "https://bdpotentiometer.readthedocs.io"
30
29
 
31
30
  [project.optional-dependencies]
32
- gpiozero = ["gpiozero>=1.6.2"]
33
- spi = ["spidev>=3.6"]
34
- dev = ["gpiozero>=1.6.2", "spidev>=3.6", "numpy"]
35
- docs = ["sphinx", "sphinx_rtd_theme", "gpiozero>=1.6.2", "numpy"]
31
+ gpiozero = ["scietex.hal.spi[gpiozero]"]
32
+ spi = ["scietex.hal.spi[spi]"]
33
+ dev = ["scietex.hal.spi[spi,gpiozero]", "numpy"]
34
+ docs = ["sphinx", "sphinx_rtd_theme", "scietex.hal.spi[gpiozero]", "numpy"]
36
35
  test = ["pytest", "numpy"]
37
36
  lint = ["ruff==0.17.4", "ty==0.0.99"]
38
37
 
@@ -49,7 +48,7 @@ pythonpath = [
49
48
 
50
49
  [tool.ruff]
51
50
  line-length = 88
52
- target-version = "py39"
51
+ target-version = "py310"
53
52
  extend-exclude = ["docs"]
54
53
 
55
54
  [tool.ruff.lint]
@@ -66,7 +65,7 @@ line-ending = "auto"
66
65
  docstring-code-format = false
67
66
 
68
67
  [tool.ty.environment]
69
- python-version = "3.9"
68
+ python-version = "3.10"
70
69
 
71
70
  [tool.ty.analysis]
72
71
  replace-imports-with-any = ["gpiozero.**", "spidev.**"]
@@ -8,10 +8,9 @@ the requested range.
8
8
 
9
9
  from math import isnan
10
10
  from numbers import Integral, Real
11
- from typing import Union
12
11
 
13
12
 
14
- def check_number(num: Union[float, int, Real]) -> Union[float, int]:
13
+ def check_number(num: float | int | Real) -> float | int:
15
14
  """
16
15
  Check that `num` is a finite real number.
17
16
 
@@ -43,7 +42,7 @@ def check_number(num: Union[float, int, Real]) -> Union[float, int]:
43
42
  return float(num)
44
43
 
45
44
 
46
- def check_integer(num: Union[float, int, Real]) -> int:
45
+ def check_integer(num: float | int | Real) -> int:
47
46
  """
48
47
  Check that `num` is an integer and convert it to int.
49
48
 
@@ -102,7 +101,7 @@ def check_label(value: str) -> str:
102
101
  return text
103
102
 
104
103
 
105
- def check_not_negative(num: Union[float, int]) -> Union[float, int]:
104
+ def check_not_negative(num: float | int) -> float | int:
106
105
  """
107
106
  Check that `num` is a not negative number.
108
107
 
@@ -120,7 +119,7 @@ def check_not_negative(num: Union[float, int]) -> Union[float, int]:
120
119
  return num
121
120
 
122
121
 
123
- def check_positive(num: Union[float, int]) -> Union[float, int]:
122
+ def check_positive(num: float | int) -> float | int:
124
123
  """
125
124
  Check that `num` is a positive number.
126
125
 
@@ -139,8 +138,8 @@ def check_positive(num: Union[float, int]) -> Union[float, int]:
139
138
 
140
139
 
141
140
  def clamp(
142
- value: Union[float, int], min_value: Union[float, int], max_value: Union[float, int]
143
- ) -> Union[float, int]:
141
+ value: float | int, min_value: float | int, max_value: float | int
142
+ ) -> float | int:
144
143
  """
145
144
  Clamp `value` to the range between `min_value` and `max_value`.
146
145
 
@@ -0,0 +1,15 @@
1
+ """Digital potentiometer and wiper models with pluggable bus transports."""
2
+
3
+ __version__ = "0.4.0"
4
+
5
+
6
+ from .digital_potentiometer import DigitalPotentiometerDevice
7
+ from .digital_wiper import DigitalWiper, SpiDigitalWiper
8
+ from .potentiometer import Potentiometer
9
+
10
+ __all__ = [
11
+ "DigitalPotentiometerDevice",
12
+ "DigitalWiper",
13
+ "Potentiometer",
14
+ "SpiDigitalWiper",
15
+ ]
@@ -11,7 +11,6 @@ propagation on the returned logger if that is not wanted.
11
11
 
12
12
  import logging
13
13
  import threading
14
- from typing import Union
15
14
 
16
15
  # Guards handler installation so that two threads calling `get_logger` for the
17
16
  # same label cannot attach two handlers.
@@ -25,7 +24,7 @@ _FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
25
24
  _PACKAGE_HANDLER_ATTR = "_bdpot_package_handler"
26
25
 
27
26
 
28
- def _package_handler(logger: logging.Logger) -> Union[logging.Handler, None]:
27
+ def _package_handler(logger: logging.Logger) -> logging.Handler | None:
29
28
  """Return the handler installed by this module, if it is still attached."""
30
29
  for handler in logger.handlers:
31
30
  if getattr(handler, _PACKAGE_HANDLER_ATTR, False):
@@ -33,7 +32,7 @@ def _package_handler(logger: logging.Logger) -> Union[logging.Handler, None]:
33
32
  return None
34
33
 
35
34
 
36
- def get_logger(label: str, log_level: Union[int, None] = None) -> logging.Logger:
35
+ def get_logger(label: str, log_level: int | None = None) -> logging.Logger:
37
36
  """
38
37
  Return the named logger `label`, configured once.
39
38