github-actions-logger 0.1.0__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.
@@ -0,0 +1,133 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import traceback
5
+ from typing import Self
6
+
7
+
8
+ def is_github_actions() -> bool:
9
+ """Return True when running inside a GitHub Actions workflow."""
10
+ return os.environ.get("GITHUB_ACTIONS") == "true"
11
+
12
+
13
+ def _escape_data(message: str) -> str:
14
+ """Escape a message as required by workflow commands."""
15
+ return message.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
16
+
17
+
18
+ def _escape_property(value: object) -> str:
19
+ """Escape an annotation property value as required by workflow commands."""
20
+ return _escape_data(str(value)).replace(":", "%3A").replace(",", "%2C")
21
+
22
+
23
+ # `extra` key -> workflow command property name
24
+ _ANNOTATION_PROPERTIES = {
25
+ "title": "title",
26
+ "file": "file",
27
+ "line": "line",
28
+ "end_line": "endLine",
29
+ "col": "col",
30
+ "end_column": "endColumn",
31
+ }
32
+
33
+
34
+ def _exc_text(exc_info: object) -> str:
35
+ """Format an `exc_info` argument the way `logging` accepts it."""
36
+ if exc_info is True:
37
+ exc_info = sys.exc_info()
38
+ elif isinstance(exc_info, BaseException):
39
+ exc_info = (type(exc_info), exc_info, exc_info.__traceback__)
40
+ if not isinstance(exc_info, tuple) or exc_info[0] is None:
41
+ return ""
42
+ return "".join(traceback.format_exception(*exc_info)).rstrip("\n")
43
+
44
+
45
+ class Group:
46
+ """Context manager that wraps output in a collapsible log group.
47
+
48
+ Emits `::group::{title}` on entry and `::endgroup::` on exit. Does nothing
49
+ when not running in CI.
50
+ """
51
+
52
+ def __init__(self, title: str, in_ci: bool) -> None:
53
+ self.title = title
54
+ self._in_ci = in_ci
55
+
56
+ def __enter__(self) -> Self:
57
+ if self._in_ci:
58
+ sys.stdout.write(f"::group::{_escape_data(self.title)}\n")
59
+ sys.stdout.flush()
60
+ return self
61
+
62
+ def __exit__(self, *exc_info: object) -> None:
63
+ if self._in_ci:
64
+ sys.stdout.write("::endgroup::\n")
65
+ sys.stdout.flush()
66
+
67
+
68
+ class Logger(logging.Logger):
69
+ """Logger that emits GitHub Actions workflow commands when running in CI.
70
+
71
+ `debug` -> `::debug::`, `info` -> `::notice::`, `warning` -> `::warning::`,
72
+ `error`/`critical`/`exception` -> `::error::`. Outside CI, standard logging is used.
73
+
74
+ Annotation properties can be passed via `extra` (ignored for `debug`):
75
+ `title`, `file`, `line`, `end_line`, `col` and `end_column`.
76
+ """
77
+
78
+ def __init__(self, name: str, in_ci: bool | None = None) -> None:
79
+ super().__init__(name)
80
+ self._in_ci = is_github_actions() if in_ci is None else in_ci
81
+
82
+ def _command(self, command: str, msg: object, args: tuple, kwargs: dict) -> None:
83
+ message = str(msg) % args if args else str(msg)
84
+ if kwargs.get("exc_info"):
85
+ text = _exc_text(kwargs["exc_info"])
86
+ if text:
87
+ message = f"{message}\n{text}"
88
+ properties = ""
89
+ extra = kwargs.get("extra")
90
+ if command != "debug" and isinstance(extra, dict):
91
+ parts = [
92
+ f"{name}={_escape_property(extra[key])}"
93
+ for key, name in _ANNOTATION_PROPERTIES.items()
94
+ if extra.get(key) is not None
95
+ ]
96
+ if parts:
97
+ properties = " " + ",".join(parts)
98
+ sys.stdout.write(f"::{command}{properties}::{_escape_data(message)}\n")
99
+ sys.stdout.flush()
100
+
101
+ def group(self, title: str) -> Group:
102
+ """Return a context manager that wraps output in a collapsible log group."""
103
+ return Group(title, self._in_ci)
104
+
105
+ def debug(self, msg: object, *args: object, **kwargs: object) -> None:
106
+ if self._in_ci:
107
+ self._command("debug", msg, args, kwargs)
108
+ else:
109
+ super().debug(msg, *args, **kwargs) # type: ignore[arg-type]
110
+
111
+ def info(self, msg: object, *args: object, **kwargs: object) -> None:
112
+ if self._in_ci:
113
+ self._command("notice", msg, args, kwargs)
114
+ else:
115
+ super().info(msg, *args, **kwargs) # type: ignore[arg-type]
116
+
117
+ def warning(self, msg: object, *args: object, **kwargs: object) -> None:
118
+ if self._in_ci:
119
+ self._command("warning", msg, args, kwargs)
120
+ else:
121
+ super().warning(msg, *args, **kwargs) # type: ignore[arg-type]
122
+
123
+ def error(self, msg: object, *args: object, **kwargs: object) -> None:
124
+ if self._in_ci:
125
+ self._command("error", msg, args, kwargs)
126
+ else:
127
+ super().error(msg, *args, **kwargs) # type: ignore[arg-type]
128
+
129
+ def critical(self, msg: object, *args: object, **kwargs: object) -> None:
130
+ if self._in_ci:
131
+ self._command("error", msg, args, kwargs)
132
+ else:
133
+ super().critical(msg, *args, **kwargs) # type: ignore[arg-type]
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.3
2
+ Name: github-actions-logger
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Cyb3r-Jak3
6
+ Author-email: Cyb3r-Jak3 <git@cyberjake.xyz>
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Natural Language :: English
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+
21
+ # github-actions-logger
22
+
23
+ A `logging.Logger` subclass that emits [GitHub Actions workflow commands](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands) when running in CI, and behaves like a normal logger everywhere else.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install github-actions-logger
29
+ ```
30
+
31
+ Or with uv:
32
+
33
+ ```bash
34
+ uv add github-actions-logger
35
+ ```
36
+
37
+ Requires Python 3.12 or newer.
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from github_actions_logger import Logger
43
+
44
+ log = Logger("my-app")
45
+
46
+ log.debug("Cache key: %s", key)
47
+ log.info("Deployed")
48
+ log.warning("Deprecated option used")
49
+ log.error("Build failed")
50
+ ```
51
+
52
+ In GitHub Actions (detected via `GITHUB_ACTIONS=true`) these print:
53
+
54
+ ```text
55
+ ::debug::Cache key: abc123
56
+ ::notice::Deployed
57
+ ::warning::Deprecated option used
58
+ ::error::Build failed
59
+ ```
60
+
61
+ Anywhere else, the calls go through standard `logging`, so handlers, levels and formatters work as usual.
62
+
63
+ ### Level mapping
64
+
65
+ | Method | Workflow command |
66
+ | --- | --- |
67
+ | `debug` | `::debug::` |
68
+ | `info` | `::notice::` |
69
+ | `warning` | `::warning::` |
70
+ | `error`, `critical`, `exception` | `::error::` |
71
+
72
+ `exception()` (or any call with `exc_info`) appends the traceback to the message.
73
+
74
+ ### Annotations
75
+
76
+ `notice`, `warning` and `error` messages can be attached to a file and line by passing the standard `extra` argument:
77
+
78
+ ```python
79
+ log.warning(
80
+ "Unused import",
81
+ extra={"title": "Lint", "file": "src/app.py", "line": 12, "end_line": 12, "col": 1, "end_column": 20},
82
+ )
83
+ ```
84
+
85
+ ```text
86
+ ::warning title=Lint,file=src/app.py,line=12,endLine=12,col=1,endColumn=20::Unused import
87
+ ```
88
+
89
+ | `extra` key | Workflow property |
90
+ | --- | --- |
91
+ | `title` | `title` |
92
+ | `file` | `file` |
93
+ | `line` | `line` |
94
+ | `end_line` | `endLine` |
95
+ | `col` | `col` |
96
+ | `end_column` | `endColumn` |
97
+
98
+ Missing or `None` keys and unrelated keys are ignored, and `debug` ignores them entirely since `::debug::` takes no properties. Outside CI, `extra` becomes attributes on the `LogRecord`, so a formatter can use it (for example `%(file)s:%(line)s`).
99
+
100
+ ### Grouping log lines
101
+
102
+ `group()` returns a context manager that wraps output in a collapsible [log group](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#grouping-log-lines):
103
+
104
+ ```python
105
+ with log.group("Install dependencies"):
106
+ log.info("Resolving...")
107
+ ```
108
+
109
+ ```text
110
+ ::group::Install dependencies
111
+ ::notice::Resolving...
112
+ ::endgroup::
113
+ ```
114
+
115
+ `::endgroup::` is emitted even if the block raises. Outside CI, `group()` does nothing.
116
+
117
+ ### Overriding detection
118
+
119
+ CI detection happens once, when the logger is created. To force it on or off, pass `in_ci`:
120
+
121
+ ```python
122
+ Logger("my-app", in_ci=True) # always emit workflow commands
123
+ Logger("my-app", in_ci=False) # always use standard logging
124
+ ```
125
+
126
+ ### Notes
127
+
128
+ - In CI, messages are written straight to stdout and bypass the logger's level and handlers. GitHub only displays `::debug::` lines when step debug logging is enabled (`ACTIONS_STEP_DEBUG`).
129
+ - Create the logger with `Logger(...)` directly. `logging.setLoggerClass(Logger)` plus `logging.getLogger(name)` works, but only passes `name`, so `in_ci` can't be set that way.
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ uv run pytest
135
+ ```
136
+
137
+ Runs the tests with a coverage report.
@@ -0,0 +1,4 @@
1
+ github_actions_logger/__init__.py,sha256=Y4TQrxt5O74WiefEECZnF2gAOutaNk5PYoJeXEluDw4,4711
2
+ github_actions_logger-0.1.0.dist-info/WHEEL,sha256=9IUYslYuG_asT4WlWvfk_K76nveuLvSTKDoLeL0cV-Q,80
3
+ github_actions_logger-0.1.0.dist-info/METADATA,sha256=GBEs9htkvlRRrwFs0Pglnb8Qg7yTQ4vih6uSFslugG8,3988
4
+ github_actions_logger-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any