logctx 0.0.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.
@@ -0,0 +1,12 @@
1
+ name: CICD
2
+
3
+ on:
4
+ pull_request: {}
5
+
6
+ jobs:
7
+ python-checks:
8
+ name: Python Quality Checks
9
+ uses: ./.github/workflows/python-checks.yml
10
+ with:
11
+ python-versions: '["3.9", "3.10", "3.11", "3.12", "3.13"]'
12
+ secrets: inherit
@@ -0,0 +1,75 @@
1
+ name: Python Quality Checks
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ python-versions:
7
+ required: true
8
+ type: string
9
+
10
+ env:
11
+ UV_FROZEN: true
12
+
13
+ jobs:
14
+ lint:
15
+ name: Ruff Lint (${{ matrix.python-version }})
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python-version: ${{ fromJSON(inputs.python-versions) }}
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: astral-sh/setup-uv@v6
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+
29
+ - name: Install lint dependencies
30
+ run: uv sync --group linting
31
+
32
+ - name: Run ruff
33
+ run: uv run ruff check .
34
+
35
+ typecheck:
36
+ name: Mypy Type Check (${{ matrix.python-version }})
37
+ runs-on: ubuntu-latest
38
+ strategy:
39
+ fail-fast: false
40
+ matrix:
41
+ python-version: ${{ fromJSON(inputs.python-versions) }}
42
+
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ - uses: astral-sh/setup-uv@v6
47
+ with:
48
+ python-version: ${{ matrix.python-version }}
49
+
50
+ - name: Install type-checking dependencies
51
+ run: uv sync --group typechecking
52
+
53
+ - name: Run mypy
54
+ run: uv run mypy .
55
+
56
+ test:
57
+ name: Pytest (${{ matrix.python-version }})
58
+ runs-on: ubuntu-latest
59
+ strategy:
60
+ fail-fast: false
61
+ matrix:
62
+ python-version: ${{ fromJSON(inputs.python-versions) }}
63
+
64
+ steps:
65
+ - uses: actions/checkout@v4
66
+
67
+ - uses: astral-sh/setup-uv@v6
68
+ with:
69
+ python-version: ${{ matrix.python-version }}
70
+
71
+ - name: Install test dependencies
72
+ run: uv sync --group testing
73
+
74
+ - name: Run tests
75
+ run: uv run pytest .
@@ -0,0 +1,152 @@
1
+ name: Semantic Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ python-checks:
10
+ name: Python Quality Checks
11
+ uses: ./.github/workflows/python-checks.yml
12
+ with:
13
+ python-versions: '["3.9", "3.10", "3.11", "3.12", "3.13"]'
14
+ secrets: inherit
15
+
16
+ release:
17
+ name: Semantic Version Release
18
+ runs-on: ubuntu-latest
19
+ needs: python-checks
20
+ environment: release
21
+ concurrency:
22
+ group: ${{ github.workflow }}-release-${{ github.ref_name }}
23
+ cancel-in-progress: false
24
+
25
+ permissions:
26
+ id-token: write
27
+ contents: write
28
+
29
+ steps:
30
+ - name: Setup | Checkout Repository on Release Branch
31
+ uses: actions/checkout@v4
32
+ # the checkout action persists the passed credentials by default
33
+ # subsequent git commands will pick them up automatically
34
+ with:
35
+ ref: ${{ github.ref_name }}
36
+ fetch-depth: 0
37
+ token: ${{ secrets.RELEASE_PAT }}
38
+
39
+ - name: Setup | Force release branch to be at workflow sha
40
+ run: |
41
+ git reset --hard ${{ github.sha }}
42
+
43
+ - name: Evaluate | Verify upstream has NOT changed
44
+ shell: bash
45
+ run: |
46
+ set +o pipefail
47
+
48
+ UPSTREAM_BRANCH_NAME="$(git status -sb | head -n 1 | cut -d' ' -f2 | grep -E '\.{3}' | cut -d'.' -f4)"
49
+ printf '%s\n' "Upstream branch name: $UPSTREAM_BRANCH_NAME"
50
+
51
+ set -o pipefail
52
+
53
+ if [ -z "$UPSTREAM_BRANCH_NAME" ]; then
54
+ printf >&2 '%s\n' "::error::Unable to determine upstream branch name!"
55
+ exit 1
56
+ fi
57
+
58
+ git fetch "${UPSTREAM_BRANCH_NAME%%/*}"
59
+
60
+ if ! UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_BRANCH_NAME")"; then
61
+ printf >&2 '%s\n' "::error::Unable to determine upstream branch sha!"
62
+ exit 1
63
+ fi
64
+
65
+ HEAD_SHA="$(git rev-parse HEAD)"
66
+
67
+ if [ "$HEAD_SHA" != "$UPSTREAM_SHA" ]; then
68
+ printf >&2 '%s\n' "[HEAD SHA] $HEAD_SHA != $UPSTREAM_SHA [UPSTREAM SHA]"
69
+ printf >&2 '%s\n' "::error::Upstream has changed, aborting release..."
70
+ exit 1
71
+ fi
72
+
73
+ printf '%s\n' "Verified upstream branch has not changed, continuing with release..."
74
+
75
+ - name: Action | Semantic Version Release
76
+ id: release
77
+ uses: python-semantic-release/python-semantic-release@v9.21.1
78
+ with:
79
+ github_token: ${{ secrets.RELEASE_PAT }}
80
+ git_committer_name: "github-actions"
81
+ git_committer_email: "actions@users.noreply.github.com"
82
+
83
+ - name: Publish | Upload to GitHub Release Assets
84
+ uses: python-semantic-release/publish-action@v9.21.1
85
+ with:
86
+ github_token: ${{ secrets.RELEASE_PAT }}
87
+ tag: ${{ needs.release.outputs.tag }}
88
+
89
+ build:
90
+ name: Build Distribution Packages
91
+ runs-on: ubuntu-latest
92
+ needs: release
93
+ environment: release
94
+
95
+ steps:
96
+ - name: Setup | Checkout Repository
97
+ uses: actions/checkout@v4
98
+
99
+ - name: Setup | Install Python
100
+ uses: actions/setup-python@v4
101
+ with:
102
+ python-version: '3.x'
103
+
104
+ - name: Build | Create Distribution Packages
105
+ run: |
106
+ python -m pip install --upgrade pip
107
+ pip install build
108
+ python -m build
109
+
110
+ - name: Upload | Upload Distribution Packages
111
+ uses: actions/upload-artifact@v4
112
+ with:
113
+ name: logctx-distributions
114
+ path: dist/
115
+
116
+ testpypi:
117
+ name: Publish to TestPyPI
118
+ runs-on: ubuntu-latest
119
+ needs: build
120
+ environment: testpypi
121
+ permissions:
122
+ id-token: write
123
+
124
+ steps:
125
+ - name: Download | Download Distribution Packages
126
+ uses: actions/download-artifact@v4
127
+ with:
128
+ name: logctx-distributions
129
+ path: dist/
130
+
131
+ - name: Publish | Upload package to PyPI
132
+ uses: pypa/gh-action-pypi-publish@release/v1
133
+ with:
134
+ repository-url: https://test.pypi.org/legacy/
135
+
136
+ pypi:
137
+ name: Publish to PyPI
138
+ runs-on: ubuntu-latest
139
+ needs: testpypi
140
+ environment: pypi
141
+ permissions:
142
+ id-token: write
143
+
144
+ steps:
145
+ - name: Download | Download Distribution Packages
146
+ uses: actions/download-artifact@v4
147
+ with:
148
+ name: logctx-distributions
149
+ path: dist/
150
+
151
+ - name: Publish | Upload package to PyPI
152
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ # Virtual environments
2
+ .venv/
3
+ __pypackages__/
4
+
5
+ # IDEs and editors
6
+ .vscode/
7
+
8
+ # Package distribution and build files
9
+ *.egg-info/
10
+ dist/
11
+ /build/
12
+ _build/
13
+
14
+ # Python bytecode and cache files
15
+ *.py[cod]
16
+ .cache/
17
+ .mypy_cache/
18
+ .pytest_cache/
19
+ /.ruff_cache/
20
+
21
+ # Benchmark and test files
22
+ .coverage
23
+
24
+ # Other files and folders
25
+ .python-version
26
+ /sandbox/
@@ -0,0 +1,4 @@
1
+ # CHANGELOG
2
+
3
+
4
+ <!-- version list -->
logctx-0.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Alexander Schulte.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
logctx-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: logctx
3
+ Version: 0.0.0
4
+ Summary: Management and injection of contextual variables into log messages.
5
+ Author: Alexander Schulte
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/aschulte201/logctx
8
+ Project-URL: Source, https://github.com/aschulte201/logctx
9
+ Project-URL: Documentation, https://github.com/aschulte201/logctx/blob/README.md
10
+ Project-URL: Changelog, https://github.com/aschulte201/logctx/blob/CHANGELOG.md
11
+ Keywords: logging,context,log,logger,logctx,log-context
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Natural Language :: English
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: Implementation :: CPython
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: typing-extensions>=4.12
28
+ Dynamic: license-file
29
+
30
+ [![CICD](https://github.com/aschulte201/logctx/actions/workflows/cicd.yml/badge.svg?branch=main)](https://github.com/aschulte201/logctx/actions/workflows/cicd.yml)
31
+
32
+ ## Enabling
33
+
34
+ The core module provides a `logging.Filter` subclass designed to inject the current active context into any log messages.
35
+
36
+ Below is a demo usage on how to enable context injection:
37
+
38
+ ```python
39
+ import logging
40
+ import logctx
41
+
42
+ root_logger = logging.getLogger()
43
+ console_handler = logging.StreamHandler()
44
+
45
+ formatter = jsonlogger.JsonFormatter("%(logctx)s")
46
+ context_filter = ContextInjectingLoggingFilter(output_field="logctx")
47
+
48
+ console_handler.setFormatter(formatter)
49
+ console_handler.addFilter(context_filter)
50
+
51
+ root_logger.addHandler(console_handler)
52
+ logger.setLevel(logging.DEBUG)
53
+ ```
54
+
55
+ # Generators
56
+ * During execution
57
+ * Between yields
58
+
59
+ ## Log Arguments
60
+ * Raises during initializtaion
61
+ * Value Error
62
+ * Able to rename args
63
+ * Unable to extract from kwargs
64
+ * Unable to work on generators
65
+ * Unable to work on async functions
66
+
67
+ # update
68
+ * can change root context
logctx-0.0.0/README.md ADDED
@@ -0,0 +1,39 @@
1
+ [![CICD](https://github.com/aschulte201/logctx/actions/workflows/cicd.yml/badge.svg?branch=main)](https://github.com/aschulte201/logctx/actions/workflows/cicd.yml)
2
+
3
+ ## Enabling
4
+
5
+ The core module provides a `logging.Filter` subclass designed to inject the current active context into any log messages.
6
+
7
+ Below is a demo usage on how to enable context injection:
8
+
9
+ ```python
10
+ import logging
11
+ import logctx
12
+
13
+ root_logger = logging.getLogger()
14
+ console_handler = logging.StreamHandler()
15
+
16
+ formatter = jsonlogger.JsonFormatter("%(logctx)s")
17
+ context_filter = ContextInjectingLoggingFilter(output_field="logctx")
18
+
19
+ console_handler.setFormatter(formatter)
20
+ console_handler.addFilter(context_filter)
21
+
22
+ root_logger.addHandler(console_handler)
23
+ logger.setLevel(logging.DEBUG)
24
+ ```
25
+
26
+ # Generators
27
+ * During execution
28
+ * Between yields
29
+
30
+ ## Log Arguments
31
+ * Raises during initializtaion
32
+ * Value Error
33
+ * Able to rename args
34
+ * Unable to extract from kwargs
35
+ * Unable to work on generators
36
+ * Unable to work on async functions
37
+
38
+ # update
39
+ * can change root context
File without changes
@@ -0,0 +1,32 @@
1
+ """logctx package
2
+
3
+ This package provides a convenient way to manage logging contexts in Python.
4
+
5
+ It allows you to manage key-value pairs for log-contexts which can be automatically
6
+ added to log messages within their respective context.
7
+ """
8
+
9
+ __author__ = "Alexander Schulte"
10
+ __maintainer__ = "Alexander Schulte"
11
+
12
+ __version__ = "0.0.0"
13
+
14
+ from logctx import decorators
15
+ from logctx._core import (
16
+ ContextInjectingLoggingFilter,
17
+ LogContext,
18
+ clear,
19
+ get_current,
20
+ new_context,
21
+ update,
22
+ )
23
+
24
+ __all__ = [
25
+ "ContextInjectingLoggingFilter",
26
+ "LogContext",
27
+ "clear",
28
+ "get_current",
29
+ "new_context",
30
+ "update",
31
+ "decorators",
32
+ ]
@@ -0,0 +1,165 @@
1
+ import contextvars
2
+ import dataclasses
3
+ import logging
4
+ from contextlib import contextmanager
5
+ from typing import Any, Generator, Mapping, Optional
6
+
7
+ __all__: list[str] = [
8
+ "LogContext",
9
+ "get_current",
10
+ "new_context",
11
+ "update",
12
+ "clear",
13
+ "ContextInjectingLoggingFilter",
14
+ ]
15
+
16
+
17
+ @dataclasses.dataclass(frozen=True)
18
+ class LogContext:
19
+ """Dataclass holding information about one specific log context.
20
+
21
+ This class is used to store key-value pairs that are relevant for the
22
+ current logging context. It is designed to be immutable to prevent
23
+ accidental mutations by users.
24
+
25
+ If you want to update the context, use `logctx.update()` or `logctx.new_context()`.
26
+
27
+ Attributes:
28
+ data (Mapping[str, Any]): A mapping of key-value pairs representing
29
+ the context data.
30
+ """
31
+
32
+ data: Mapping[str, Any] = dataclasses.field(default_factory=dict)
33
+
34
+ def with_values(self, **kwargs) -> "LogContext":
35
+ """Create a new context with additional key-value pairs.
36
+
37
+ This method returns a new instance of LogContext with the current
38
+ context data merged with the provided key-value pairs. Duplicate keys
39
+ will be overwritten by the new values.
40
+
41
+ Caution:
42
+ This method does not affect the current active context, meaning that the
43
+ resulting context will not be included in any log messages.
44
+
45
+ Args:
46
+ **kwargs: Key-value pairs to be added to the new context.
47
+ Returns:
48
+ LogContext: A new instance of LogContext with the merged data.
49
+ """
50
+
51
+ return LogContext({**self.data, **kwargs})
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ """Convert the context to a dictionary."""
55
+
56
+ return dict(self.data)
57
+
58
+
59
+ _mdc_context: contextvars.ContextVar[LogContext] = contextvars.ContextVar("_mdc_context")
60
+
61
+
62
+ # TODO: return None or raise on no context
63
+ def get_current() -> LogContext:
64
+ """Retrieve current context.
65
+
66
+ This function retrieves the current logging context from the context
67
+ variable. If no context is found, it returns an empty LogContext
68
+ instance.
69
+
70
+ Returns:
71
+ LogContext: The current logging context.
72
+ """
73
+ try:
74
+ return _mdc_context.get()
75
+ except LookupError:
76
+ return LogContext()
77
+
78
+
79
+ @contextmanager
80
+ def new_context(**kwargs) -> Generator[LogContext, None, None]:
81
+ """Create a new context with the provided key-value pairs.
82
+
83
+ The new context inherits all key-value pairs from the current context and
84
+ adds the provided pairs. Duplicate keys will be overwritten by the new values.
85
+
86
+ Args:
87
+ **kwargs: Key-value pairs to be included in the new context.
88
+
89
+ Yields:
90
+ LogContext: The new logging context.
91
+ """
92
+
93
+ current_log_ctx: LogContext = get_current()
94
+ new_log_ctx = current_log_ctx.with_values(**kwargs)
95
+ token = _mdc_context.set(new_log_ctx)
96
+ try:
97
+ yield new_log_ctx
98
+ finally:
99
+ _mdc_context.reset(token)
100
+
101
+
102
+ def update(**kwargs) -> LogContext:
103
+ """Append key-value pairs to the current context.
104
+
105
+ Duplicate keys will be overwritten by the new values.
106
+
107
+ Will not affect log calls in current context made before the update.
108
+
109
+ Args:
110
+ **kwargs: Key-value pairs to be added to the current context.
111
+
112
+ Returns:
113
+ LogContext: The updated logging context with the appended key-value
114
+ pairs.
115
+ """
116
+ current_log_ctx = get_current()
117
+ updated_log_ctx = current_log_ctx.with_values(**kwargs)
118
+ _mdc_context.set(updated_log_ctx)
119
+
120
+ return updated_log_ctx
121
+
122
+
123
+ def clear() -> None:
124
+ """Clear the current context.
125
+
126
+ Only affects current context. After leaving current context, the context
127
+ will be reset to its previous state.
128
+
129
+ Example:
130
+ ```python
131
+ with logctx.new_context(a=1, b=2):
132
+ with logctx.new_context(c=3):
133
+ # Context is now: {'a': 1, 'b': 2, 'c': 3}
134
+ logctx.clear()
135
+ # Context is now: {}
136
+ # Context is now: {'a': 1, 'b': 2}
137
+ ```
138
+ """
139
+ _mdc_context.set(LogContext())
140
+
141
+
142
+ class ContextInjectingLoggingFilter(logging.Filter):
143
+ """Logging filter that injects the current context into log records.
144
+
145
+ Attributes:
146
+ name (str): The name of the filter. This is used to identify the
147
+ filter in the logging system.
148
+
149
+ output_field (str): The name of the field in the log record where the
150
+ context data will be injected. If not provided, the context data
151
+ will be injected into the log record as root level attributes.
152
+ """
153
+
154
+ def __init__(self, name: str = "", output_field: Optional[str] = None) -> None:
155
+ super().__init__(name=name)
156
+ self._output_field: Optional[str] = output_field
157
+
158
+ def filter(self, record: logging.LogRecord) -> bool:
159
+ context: LogContext = get_current()
160
+ if self._output_field is not None:
161
+ setattr(record, self._output_field, context.to_dict())
162
+ else:
163
+ for k, v in context.to_dict().items():
164
+ setattr(record, k, v)
165
+ return True