pylemetry 1.1.4__tar.gz → 1.2.1__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 (43) hide show
  1. pylemetry-1.2.1/.github/actions/bump-version/action.yaml +26 -0
  2. {pylemetry-1.1.4 → pylemetry-1.2.1}/.github/actions/configure-uv/action.yaml +2 -1
  3. {pylemetry-1.1.4 → pylemetry-1.2.1}/.github/workflows/ci.yaml +4 -8
  4. pylemetry-1.2.1/.github/workflows/release.yaml +102 -0
  5. {pylemetry-1.1.4 → pylemetry-1.2.1}/PKG-INFO +67 -23
  6. {pylemetry-1.1.4 → pylemetry-1.2.1}/README.md +65 -21
  7. {pylemetry-1.1.4 → pylemetry-1.2.1}/pyproject.toml +2 -2
  8. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/decorators/__init__.py +0 -1
  9. pylemetry-1.2.1/src/pylemetry/decorators/count.py +78 -0
  10. pylemetry-1.2.1/src/pylemetry/decorators/time.py +81 -0
  11. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/counter.py +2 -2
  12. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/gauge.py +2 -2
  13. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/meter.py +10 -1
  14. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/timer.py +15 -9
  15. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/registry.py +37 -25
  16. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/__init__.py +2 -3
  17. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/logging.py +12 -6
  18. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/reporter.py +22 -12
  19. pylemetry-1.2.1/src/pylemetry/utils/__init__.py +3 -0
  20. pylemetry-1.2.1/src/pylemetry/utils/timer_units.py +22 -0
  21. pylemetry-1.2.1/test/test_pylemetry/decorators/test_count.py +134 -0
  22. pylemetry-1.2.1/test/test_pylemetry/decorators/test_time.py +143 -0
  23. {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/meters/test_counter.py +5 -5
  24. {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/meters/test_gauge.py +7 -7
  25. {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/meters/test_timer.py +21 -8
  26. {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/reporting/test_logging.py +25 -25
  27. {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/reporting/test_reporter.py +46 -23
  28. {pylemetry-1.1.4 → pylemetry-1.2.1}/test/test_pylemetry/test_registry.py +40 -40
  29. pylemetry-1.2.1/test/test_pylemetry/utils/test_timer_units.py +20 -0
  30. {pylemetry-1.1.4 → pylemetry-1.2.1}/uv.lock +2 -2
  31. pylemetry-1.1.4/.github/workflows/release.yaml +0 -89
  32. pylemetry-1.1.4/src/pylemetry/decorators/count.py +0 -39
  33. pylemetry-1.1.4/src/pylemetry/decorators/time.py +0 -38
  34. pylemetry-1.1.4/test/test_pylemetry/decorators/test_count.py +0 -48
  35. pylemetry-1.1.4/test/test_pylemetry/decorators/test_time.py +0 -51
  36. {pylemetry-1.1.4 → pylemetry-1.2.1}/.gitignore +0 -0
  37. {pylemetry-1.1.4 → pylemetry-1.2.1}/LICENSE +0 -0
  38. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/__init__.py +0 -0
  39. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/__init__.py +0 -0
  40. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/meters/__init__.py +0 -0
  41. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/py.typed +0 -0
  42. {pylemetry-1.1.4 → pylemetry-1.2.1}/src/pylemetry/reporting/reporting_type.py +0 -0
  43. {pylemetry-1.1.4 → pylemetry-1.2.1}/test/conftest.py +0 -0
@@ -0,0 +1,26 @@
1
+ name: Bump version
2
+ description: Bump the project version and create a pull request
3
+ runs:
4
+ using: composite
5
+ steps:
6
+ - name: Bump Version
7
+ run: uv version --bump patch --bump alpha
8
+ shell: bash
9
+
10
+ - name: Get New Version Branch Name
11
+ id: bump_version
12
+ run: echo "branch_name=feature/bump_version_$(uv version --short)" >> $GITHUB_OUTPUT
13
+ shell: bash
14
+
15
+ - name: Commit Version Bump
16
+ uses: EndBug/add-and-commit@v11.1.1
17
+ with:
18
+ message: "Bump project version following release"
19
+ default_author: github_actions
20
+ new_branch: ${{ steps.bump_version.outputs.branch_name }}
21
+
22
+ - name: Raise Pull Request
23
+ run: gh pr create -B main -H ${{ steps.bump_version.outputs.branch_name }} --title "Update version to v$(uv version --short)" --body 'Created by GitHub Actions in the Release pipeline'
24
+ shell: bash
25
+ env:
26
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,9 +1,10 @@
1
1
  name: Configure uv
2
+ description: Configure uv for running within pipelines
2
3
  runs:
3
4
  using: composite
4
5
  steps:
5
6
  - name: Install uv
6
- uses: astral-sh/setup-uv@v6
7
+ uses: astral-sh/setup-uv@v10.2.0
7
8
 
8
9
  - name: Install dependencies
9
10
  run: uv sync --locked --dev
@@ -1,13 +1,9 @@
1
1
  name: Test, Build, and Publish
2
2
 
3
3
  on:
4
- push:
5
- branches:
6
- - develop
7
4
  pull_request:
8
5
  branches:
9
6
  - main
10
- - develop
11
7
 
12
8
  jobs:
13
9
  lint:
@@ -15,11 +11,11 @@ jobs:
15
11
  runs-on: ubuntu-latest
16
12
 
17
13
  steps:
18
- - uses: actions/checkout@v5
14
+ - uses: actions/checkout@v7.0.1
19
15
 
20
16
  - uses: ./.github/actions/configure-uv
21
17
 
22
- - uses: astral-sh/ruff-action@v3
18
+ - uses: astral-sh/ruff-action@v4.1.0
23
19
 
24
20
  - name: Type Checks
25
21
  run: uv run mypy .
@@ -29,7 +25,7 @@ jobs:
29
25
  runs-on: ubuntu-latest
30
26
 
31
27
  steps:
32
- - uses: actions/checkout@v5
28
+ - uses: actions/checkout@v7.0.1
33
29
 
34
30
  - uses: ./.github/actions/configure-uv
35
31
 
@@ -43,7 +39,7 @@ jobs:
43
39
  needs: [lint, test]
44
40
 
45
41
  steps:
46
- - uses: actions/checkout@v5
42
+ - uses: actions/checkout@v7.0.1
47
43
 
48
44
  - uses: ./.github/actions/configure-uv
49
45
 
@@ -0,0 +1,102 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ dry-run:
7
+ description: Run as a dry-run
8
+ type: boolean
9
+ bump-to-stable:
10
+ description: Bump the version to the next stable version
11
+ type: boolean
12
+ default: true
13
+ override-version:
14
+ description: Override the current version and set as the release version
15
+ type: string
16
+
17
+ jobs:
18
+ lint:
19
+ name: Lint & Types
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - uses: actions/checkout@v7.0.1
24
+
25
+ - uses: ./.github/actions/configure-uv
26
+
27
+ - uses: astral-sh/ruff-action@v4.1.0
28
+
29
+ - name: Type Checks
30
+ run: uv run mypy .
31
+
32
+ test:
33
+ name: Unit Tests
34
+ runs-on: ubuntu-latest
35
+
36
+ steps:
37
+ - uses: actions/checkout@v7.0.1
38
+
39
+ - uses: ./.github/actions/configure-uv
40
+
41
+ - name: Run tests with Tox
42
+ run: tox
43
+
44
+ publish:
45
+ name: Build & Publish
46
+ runs-on: ubuntu-latest
47
+ environment: release
48
+ needs: [lint, test]
49
+ permissions:
50
+ contents: write
51
+ pull-requests: write
52
+
53
+ steps:
54
+ - uses: actions/checkout@v7.0.1
55
+ with:
56
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
57
+
58
+ - uses: ./.github/actions/configure-uv
59
+
60
+ - name: Set release version
61
+ if: ${{ inputs.bump-to-stable || inputs.override-version != '' }}
62
+ run: |
63
+ if [[ $OVERRIDE_VERSION != '' ]]; then
64
+ uv version $OVERRIDE_VERSION;
65
+ elif $BUMP_TO_STABLE; then
66
+ uv version --bump stable
67
+ fi;
68
+ env:
69
+ BUMP_TO_STABLE: ${{ inputs.bump-to-stable }}
70
+ OVERRIDE_VERSION: ${{ inputs.override-version }}
71
+
72
+ - name: Get version from uv
73
+ id: version
74
+ run: echo "version=$(uv version --short)" >> $GITHUB_OUTPUT
75
+
76
+ - name: Build
77
+ run: uv build
78
+
79
+ - name: Publish to PyPI
80
+ env:
81
+ PYPI_TOKEN: ${{ secrets.pypi_token }}
82
+ DRY_RUN: ${{ inputs.dry-run }}
83
+ run: |
84
+ if $DRY_RUN; then
85
+ uv publish --index pypi --dry-run --token $PYPI_TOKEN
86
+ else
87
+ uv publish --index pypi --token $PYPI_TOKEN
88
+ fi;
89
+
90
+ - name: GitHub Release
91
+ if: ${{ !inputs.dry-run }}
92
+ uses: ncipollo/release-action@v1
93
+ env:
94
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
95
+ with:
96
+ tag: ${{ steps.version.outputs.version }}
97
+ name: ${{ steps.version.outputs.version }}
98
+ artifacts: "dist/pylemetry-*.tar.gz,dist/pylemetry-*.whl"
99
+
100
+ - name: Bump Version
101
+ if: ${{ !inputs.dry-run }}
102
+ uses: ./.github/actions/bump-version
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pylemetry
3
- Version: 1.1.4
3
+ Version: 1.2.1
4
4
  Summary: A Python metrics library
5
5
  Project-URL: Homepage, https://pypi.org/project/pylemetry/
6
6
  Project-URL: Repository, https://github.com/amurphy4/pylemetry
@@ -44,7 +44,7 @@ from pylemetry.meters import Counter
44
44
 
45
45
 
46
46
  def some_method() -> None:
47
- counter = Counter()
47
+ counter = Counter("example")
48
48
 
49
49
  for _ in range(100):
50
50
  counter.add() # counter += 1 is also supported
@@ -60,13 +60,11 @@ from pylemetry.decorators import count
60
60
 
61
61
 
62
62
  @count()
63
- def some_method() -> None:
64
- ...
63
+ def some_method() -> None: ...
65
64
 
66
65
 
67
66
  @count("named_counter")
68
- def another_method() -> None:
69
- ...
67
+ def another_method() -> None: ...
70
68
 
71
69
 
72
70
  def main() -> None:
@@ -93,9 +91,9 @@ from pylemetry.meters import Gauge
93
91
 
94
92
 
95
93
  def some_method() -> None:
96
- gauge = Gauge()
94
+ gauge = Gauge("sample_gauge")
97
95
 
98
- registry.add_gauge("sample_gauge", gauge)
96
+ registry.add_gauge(gauge)
99
97
  ```
100
98
 
101
99
  The `Gauge` supports incrementing, decrementing, and setting a value directly
@@ -127,7 +125,7 @@ from pylemetry.meters import Timer
127
125
 
128
126
 
129
127
  def some_method() -> None:
130
- timer = Timer()
128
+ timer = Timer("example")
131
129
 
132
130
  for _ in range(100):
133
131
  with timer.time():
@@ -145,20 +143,18 @@ from pylemetry.decorators import time
145
143
 
146
144
 
147
145
  @time()
148
- def some_method() -> None:
149
- ...
146
+ def some_method() -> None: ...
150
147
 
151
148
 
152
149
  @time("named_timer")
153
- def another_method() -> None:
154
- ...
150
+ def another_method() -> None: ...
155
151
 
156
152
 
157
153
  def main() -> None:
158
154
  for _ in range(100):
159
155
  some_method()
160
156
  another_method()
161
-
157
+
162
158
  timer = registry.get_timer("some_method")
163
159
  timer.get_count() # 100
164
160
  timer.get_value() # Sum total execution time of the some_method function
@@ -173,6 +169,53 @@ def main() -> None:
173
169
 
174
170
  When using this meter via a decorator, the meter gets added to the global `registry`, with the method name it's decorating as the meter name. Alternatively, you can provide a name for the meter as a parameter to the decorator
175
171
 
172
+ By default, timer meters will measure time in nanoseconds, this can be changed via the `unit` parameter using the `TimerUnits` enum in the utils module
173
+
174
+ ```python
175
+ import time
176
+
177
+ from pylemetry.meters import Timer
178
+ from pylemetry.utils import TimerUnits
179
+
180
+
181
+ timer_s = Timer("example_s", TimerUnits.SECONDS)
182
+ timer_ms = Timer("example_ms", TimerUnits.MILLISECONDS)
183
+
184
+ with timer_s.time():
185
+ time.sleep(1)
186
+
187
+ with timer_ms.time():
188
+ time.sleep(1)
189
+
190
+ timer_s.get_mean_tick_time() # 1
191
+ timer_ms.get_mean_tick_time() # 1000
192
+ ```
193
+
194
+ ## Tags
195
+
196
+ When creating a meter you can assign a set of tags to it as key-value pairs. The value must be one of either `str`, `int`, or `float`.
197
+ When using a decorator to create a meter, you can use a custom format for the value to extract values out of the method's args and kwargs
198
+
199
+ In order to allow for multiple meters with the same name and different tags, the name in the registry gets mangled with the tags to produce a unique name,
200
+ as a result when trying to get the meter from the registry you will need to provide both its name and its tags.
201
+
202
+ ```python
203
+ from pylemetry import registry
204
+ from pylemetry.decorators import time
205
+
206
+
207
+ @time("example_timer", tags={"tag_1": "args[0]", "tag_2": "kwargs[param_2]", "tag_3": "some value"})
208
+ def some_method(param_1: int, param_2: int) -> None: ...
209
+
210
+
211
+ def main() -> None:
212
+ for _ in range(100):
213
+ some_method(1, param_2=2)
214
+
215
+ timer = registry.get_timer("example_timer", {"tag_1": 1, "tag_2": 2, "tag_3": "some value"})
216
+ timer.get_tags() # {"tag_1": 1, "tag_2": 2, "tag_3": "some value"}
217
+ ```
218
+
176
219
  ## The Registry
177
220
 
178
221
  Pylemetry maintains a global registry of meters, allowing you to share a meter across multiple files, or reference metrics from a central location.
@@ -183,13 +226,13 @@ from pylemetry import registry
183
226
  from pylemetry.meters import Counter, Gauge, Timer
184
227
 
185
228
 
186
- counter = Counter()
187
- gauge = Gauge()
188
- timer = Timer()
229
+ counter = Counter("example")
230
+ gauge = Gauge("example")
231
+ timer = Timer("example")
189
232
 
190
- registry.add_counter("example", counter)
191
- registry.add_gauge("example", gauge)
192
- registry.add_timer("example", timer)
233
+ registry.add_counter(counter)
234
+ registry.add_gauge(gauge)
235
+ registry.add_timer(timer)
193
236
  ```
194
237
 
195
238
  Each meter type has an `add_meter`, `get_meter` and `remove_meter` method to manage meters in the `registry`, each requiring a unique meter name.
@@ -200,9 +243,9 @@ from pylemetry import registry
200
243
  from pylemetry.meters import Counter, MeterType
201
244
 
202
245
 
203
- counter = Counter()
246
+ counter = Counter("example")
204
247
 
205
- registry.add_meter("example", counter, MeterType.COUNTER)
248
+ registry.add_meter(counter, MeterType.COUNTER)
206
249
  registry.get_meter("example", MeterType.COUNTER)
207
250
  registry.remove_meter("example", MeterType.COUNTER)
208
251
  registry.get_meter("example", MeterType.COUNTER) # None
@@ -226,6 +269,7 @@ The message format allows for substitutions for metric values with the following
226
269
  | max | Maximum value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `max_tick_time` for `Timer` meters |
227
270
  | avg | Mean average value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `mean_tick_time` for `Timer` meters |
228
271
  | type | Type of the meter (`counter`, `gauge`, or `timer`) |
272
+ | tags | The tags associated with the meter |
229
273
 
230
274
  As an example, a `Counter` meter named `sample_counter` with a value of 10
231
275
  ```python
@@ -21,7 +21,7 @@ from pylemetry.meters import Counter
21
21
 
22
22
 
23
23
  def some_method() -> None:
24
- counter = Counter()
24
+ counter = Counter("example")
25
25
 
26
26
  for _ in range(100):
27
27
  counter.add() # counter += 1 is also supported
@@ -37,13 +37,11 @@ from pylemetry.decorators import count
37
37
 
38
38
 
39
39
  @count()
40
- def some_method() -> None:
41
- ...
40
+ def some_method() -> None: ...
42
41
 
43
42
 
44
43
  @count("named_counter")
45
- def another_method() -> None:
46
- ...
44
+ def another_method() -> None: ...
47
45
 
48
46
 
49
47
  def main() -> None:
@@ -70,9 +68,9 @@ from pylemetry.meters import Gauge
70
68
 
71
69
 
72
70
  def some_method() -> None:
73
- gauge = Gauge()
71
+ gauge = Gauge("sample_gauge")
74
72
 
75
- registry.add_gauge("sample_gauge", gauge)
73
+ registry.add_gauge(gauge)
76
74
  ```
77
75
 
78
76
  The `Gauge` supports incrementing, decrementing, and setting a value directly
@@ -104,7 +102,7 @@ from pylemetry.meters import Timer
104
102
 
105
103
 
106
104
  def some_method() -> None:
107
- timer = Timer()
105
+ timer = Timer("example")
108
106
 
109
107
  for _ in range(100):
110
108
  with timer.time():
@@ -122,20 +120,18 @@ from pylemetry.decorators import time
122
120
 
123
121
 
124
122
  @time()
125
- def some_method() -> None:
126
- ...
123
+ def some_method() -> None: ...
127
124
 
128
125
 
129
126
  @time("named_timer")
130
- def another_method() -> None:
131
- ...
127
+ def another_method() -> None: ...
132
128
 
133
129
 
134
130
  def main() -> None:
135
131
  for _ in range(100):
136
132
  some_method()
137
133
  another_method()
138
-
134
+
139
135
  timer = registry.get_timer("some_method")
140
136
  timer.get_count() # 100
141
137
  timer.get_value() # Sum total execution time of the some_method function
@@ -150,6 +146,53 @@ def main() -> None:
150
146
 
151
147
  When using this meter via a decorator, the meter gets added to the global `registry`, with the method name it's decorating as the meter name. Alternatively, you can provide a name for the meter as a parameter to the decorator
152
148
 
149
+ By default, timer meters will measure time in nanoseconds, this can be changed via the `unit` parameter using the `TimerUnits` enum in the utils module
150
+
151
+ ```python
152
+ import time
153
+
154
+ from pylemetry.meters import Timer
155
+ from pylemetry.utils import TimerUnits
156
+
157
+
158
+ timer_s = Timer("example_s", TimerUnits.SECONDS)
159
+ timer_ms = Timer("example_ms", TimerUnits.MILLISECONDS)
160
+
161
+ with timer_s.time():
162
+ time.sleep(1)
163
+
164
+ with timer_ms.time():
165
+ time.sleep(1)
166
+
167
+ timer_s.get_mean_tick_time() # 1
168
+ timer_ms.get_mean_tick_time() # 1000
169
+ ```
170
+
171
+ ## Tags
172
+
173
+ When creating a meter you can assign a set of tags to it as key-value pairs. The value must be one of either `str`, `int`, or `float`.
174
+ When using a decorator to create a meter, you can use a custom format for the value to extract values out of the method's args and kwargs
175
+
176
+ In order to allow for multiple meters with the same name and different tags, the name in the registry gets mangled with the tags to produce a unique name,
177
+ as a result when trying to get the meter from the registry you will need to provide both its name and its tags.
178
+
179
+ ```python
180
+ from pylemetry import registry
181
+ from pylemetry.decorators import time
182
+
183
+
184
+ @time("example_timer", tags={"tag_1": "args[0]", "tag_2": "kwargs[param_2]", "tag_3": "some value"})
185
+ def some_method(param_1: int, param_2: int) -> None: ...
186
+
187
+
188
+ def main() -> None:
189
+ for _ in range(100):
190
+ some_method(1, param_2=2)
191
+
192
+ timer = registry.get_timer("example_timer", {"tag_1": 1, "tag_2": 2, "tag_3": "some value"})
193
+ timer.get_tags() # {"tag_1": 1, "tag_2": 2, "tag_3": "some value"}
194
+ ```
195
+
153
196
  ## The Registry
154
197
 
155
198
  Pylemetry maintains a global registry of meters, allowing you to share a meter across multiple files, or reference metrics from a central location.
@@ -160,13 +203,13 @@ from pylemetry import registry
160
203
  from pylemetry.meters import Counter, Gauge, Timer
161
204
 
162
205
 
163
- counter = Counter()
164
- gauge = Gauge()
165
- timer = Timer()
206
+ counter = Counter("example")
207
+ gauge = Gauge("example")
208
+ timer = Timer("example")
166
209
 
167
- registry.add_counter("example", counter)
168
- registry.add_gauge("example", gauge)
169
- registry.add_timer("example", timer)
210
+ registry.add_counter(counter)
211
+ registry.add_gauge(gauge)
212
+ registry.add_timer(timer)
170
213
  ```
171
214
 
172
215
  Each meter type has an `add_meter`, `get_meter` and `remove_meter` method to manage meters in the `registry`, each requiring a unique meter name.
@@ -177,9 +220,9 @@ from pylemetry import registry
177
220
  from pylemetry.meters import Counter, MeterType
178
221
 
179
222
 
180
- counter = Counter()
223
+ counter = Counter("example")
181
224
 
182
- registry.add_meter("example", counter, MeterType.COUNTER)
225
+ registry.add_meter(counter, MeterType.COUNTER)
183
226
  registry.get_meter("example", MeterType.COUNTER)
184
227
  registry.remove_meter("example", MeterType.COUNTER)
185
228
  registry.get_meter("example", MeterType.COUNTER) # None
@@ -203,6 +246,7 @@ The message format allows for substitutions for metric values with the following
203
246
  | max | Maximum value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `max_tick_time` for `Timer` meters |
204
247
  | avg | Mean average value of the meter, equivalent to the `value` substitution for `Counter` and `Gauge` meters, `mean_tick_time` for `Timer` meters |
205
248
  | type | Type of the meter (`counter`, `gauge`, or `timer`) |
249
+ | tags | The tags associated with the meter |
206
250
 
207
251
  As an example, a `Counter` meter named `sample_counter` with a value of 10
208
252
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pylemetry"
3
- version = "1.1.4"
3
+ version = "1.2.1"
4
4
  description = "A Python metrics library"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -60,7 +60,7 @@ explicit_package_bases = true
60
60
  line-length = 120
61
61
 
62
62
  [tool.ruff.lint]
63
- select = ["E", "F", "B"]
63
+ select = ["ARG", "B", "E", "F", "I", "N", "RUF", "TID", "UP", "W"]
64
64
 
65
65
  [tool.pytest.ini_options]
66
66
  addopts = "--cov=pylemetry --cov-report=html"
@@ -1,5 +1,4 @@
1
1
  from .count import count
2
2
  from .time import time
3
3
 
4
-
5
4
  __all__ = ["count", "time"]
@@ -0,0 +1,78 @@
1
+ import re
2
+ from collections.abc import Callable
3
+ from functools import wraps
4
+ from typing import ParamSpec, TypeVar
5
+
6
+ from pylemetry import registry
7
+ from pylemetry.meters import Counter
8
+
9
+ P = ParamSpec("P")
10
+ R = TypeVar("R", covariant=True)
11
+
12
+
13
+ def count(
14
+ name: str | None = None, tags: dict[str, str | int | float] | None = None
15
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
16
+ """
17
+ Decorator to count the number of invocations of a given callable. Creates a Counter meter in the Registry
18
+ with either the provided name or the fully qualified name of the callable object as the metric name.
19
+
20
+ :param name: Name of the meter to create, if None the function name is used
21
+ :param tags: Optional key-value pairs of tags for this meter
22
+ :return: Result of the wrapped function
23
+ """
24
+
25
+ def decorator(f: Callable[P, R]) -> Callable[P, R]:
26
+ @wraps(f)
27
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
28
+ counter_name = f.__qualname__ if name is None else name
29
+
30
+ _tags: dict[str, str | int | float] = {}
31
+
32
+ if tags:
33
+ for key, value in tags.items():
34
+ args_match = re.search(r"^args\[[0-9]+]", str(value))
35
+ kwargs_match = re.search(r"^kwargs\[[a-zA-Z0-9_]+]", str(value))
36
+
37
+ if args_match:
38
+ args_index = int(args_match.group().replace("args[", "").replace("]", ""))
39
+
40
+ if args_index >= len(args):
41
+ raise IndexError(f"args index {args_index} out of range, args has length {len(args)}")
42
+
43
+ if not isinstance(args[args_index], (str, int, float)):
44
+ raise ValueError(
45
+ f"Only args of types (str | int | float) may be added as tags. "
46
+ f"Got {args[args_index].__class__}"
47
+ )
48
+
49
+ _tags[key] = args[args_index] # type: ignore
50
+ elif kwargs_match:
51
+ kwargs_key = kwargs_match.group().replace("kwargs[", "").replace("]", "")
52
+
53
+ if kwargs_key not in kwargs:
54
+ raise KeyError(f"Key '{kwargs_key}' not present in kwargs")
55
+
56
+ if not isinstance(kwargs[kwargs_key], (str, int, float)):
57
+ raise ValueError(
58
+ f"Only kwargs of types (str | int | float) may be added as tags. "
59
+ f"Got {kwargs[kwargs_key].__class__}"
60
+ )
61
+
62
+ _tags[key] = kwargs[kwargs_key] # type: ignore
63
+ else:
64
+ _tags[key] = value
65
+
66
+ counter = registry.get_counter(counter_name, tags=_tags)
67
+
68
+ if not counter:
69
+ counter = Counter(name=counter_name, tags=_tags)
70
+ registry.add_counter(counter)
71
+
72
+ counter += 1
73
+
74
+ return f(*args, **kwargs)
75
+
76
+ return wrapper
77
+
78
+ return decorator