benethos-mailbox-common 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.
- benethos_mailbox_common-0.4.0/.gitignore +62 -0
- benethos_mailbox_common-0.4.0/LICENSE +21 -0
- benethos_mailbox_common-0.4.0/PKG-INFO +75 -0
- benethos_mailbox_common-0.4.0/README.md +37 -0
- benethos_mailbox_common-0.4.0/pyproject.toml +63 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/__init__.py +17 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/log/__init__.py +9 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/log/lines.py +131 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/log/redact.py +48 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/mail/__init__.py +8 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/mail/addresses.py +18 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/mail/plaintext.py +187 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/paths/__init__.py +9 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/paths/folders.py +53 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/py.typed +0 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/settings/__init__.py +9 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/settings/files.py +41 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/values/__init__.py +9 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/values/canonical.py +24 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/values/secret.py +50 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/values/sizes.py +10 -0
- benethos_mailbox_common-0.4.0/src/benethos_mailbox_common/values/text.py +49 -0
- benethos_mailbox_common-0.4.0/tests/log/test_lines.py +134 -0
- benethos_mailbox_common-0.4.0/tests/log/test_redact.py +33 -0
- benethos_mailbox_common-0.4.0/tests/mail/test_addresses.py +36 -0
- benethos_mailbox_common-0.4.0/tests/mail/test_plaintext.py +114 -0
- benethos_mailbox_common-0.4.0/tests/paths/test_folders.py +69 -0
- benethos_mailbox_common-0.4.0/tests/settings/test_files.py +67 -0
- benethos_mailbox_common-0.4.0/tests/test_architecture.py +152 -0
- benethos_mailbox_common-0.4.0/tests/values/test_canonical.py +20 -0
- benethos_mailbox_common-0.4.0/tests/values/test_secret.py +42 -0
- benethos_mailbox_common-0.4.0/tests/values/test_sizes.py +11 -0
- benethos_mailbox_common-0.4.0/tests/values/test_text.py +51 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Virtual environment
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.eggs/
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
|
|
13
|
+
# Caching / coverage
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.cache/
|
|
18
|
+
*.sqlite
|
|
19
|
+
.coverage
|
|
20
|
+
.coverage.*
|
|
21
|
+
htmlcov/
|
|
22
|
+
coverage.xml
|
|
23
|
+
|
|
24
|
+
# IDE / OS
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
|
|
30
|
+
# Local configuration and secrets
|
|
31
|
+
.env
|
|
32
|
+
# config/: per package, only the templates are versioned
|
|
33
|
+
config/**
|
|
34
|
+
!config/**/
|
|
35
|
+
!config/*/.env.example
|
|
36
|
+
*.token.json
|
|
37
|
+
# containers/*/secrets/: the master key of a deployment, the passwords
|
|
38
|
+
# and the CA of the test mail server
|
|
39
|
+
containers/**/secrets/
|
|
40
|
+
# containers/production/service.env: settings, among them perhaps a secret
|
|
41
|
+
containers/**/service.env
|
|
42
|
+
# containers/production/: the MCP server instances and their routes in Caddy
|
|
43
|
+
containers/production/compose.override.yaml
|
|
44
|
+
containers/production/caddy.d/*.caddy
|
|
45
|
+
|
|
46
|
+
# Service data: the database holds encrypted credentials, backups the same.
|
|
47
|
+
# data/: per package, only the folders are versioned (.gitkeep)
|
|
48
|
+
data/**
|
|
49
|
+
!data/**/
|
|
50
|
+
*.db
|
|
51
|
+
*.db-journal
|
|
52
|
+
*.db-wal
|
|
53
|
+
*.db-shm
|
|
54
|
+
**/backups/**
|
|
55
|
+
!**/backups/**/
|
|
56
|
+
|
|
57
|
+
# Live checks: test server address and test accounts stay local
|
|
58
|
+
live/.env
|
|
59
|
+
|
|
60
|
+
# .gitkeep holds an empty folder and is kept wherever it is. Only works
|
|
61
|
+
# below folders ignored file by file (as above), not below an ignored folder.
|
|
62
|
+
!.gitkeep
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benedikt Oswald
|
|
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.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: benethos-mailbox-common
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: What mailbox-service and mailbox-mcp of Mailbox share: mail as plain text, the folders of the operating system, settings from a file, the time of a log line, secrets masked in text, JSON for a hash.
|
|
5
|
+
Project-URL: Homepage, https://github.com/benethos-hub/mailbox/tree/main/packages/mailbox-common#readme
|
|
6
|
+
Project-URL: Repository, https://github.com/benethos-hub/mailbox
|
|
7
|
+
Project-URL: Issues, https://github.com/benethos-hub/mailbox/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/benethos-hub/mailbox/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Benedikt Oswald
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: email,html-to-text,logging,redaction,self-hosted
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
26
|
+
Classifier: Topic :: Communications :: Email
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: platformdirs>=4.3; extra == 'all'
|
|
32
|
+
Requires-Dist: pydantic-settings>=2.10; extra == 'all'
|
|
33
|
+
Provides-Extra: paths
|
|
34
|
+
Requires-Dist: platformdirs>=4.3; extra == 'paths'
|
|
35
|
+
Provides-Extra: settings
|
|
36
|
+
Requires-Dist: pydantic-settings>=2.10; extra == 'settings'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# <img src="https://raw.githubusercontent.com/benethos-hub/mailbox/main/assets/logo/icon-3d.svg" alt="" height="36" align="absmiddle"> mailbox-common
|
|
40
|
+
|
|
41
|
+
[](https://github.com/benethos-hub/mailbox/actions/workflows/ci.yml)
|
|
42
|
+
[](https://pypi.org/project/benethos-mailbox-common/)
|
|
43
|
+
[](https://pypi.org/project/benethos-mailbox-common/)
|
|
44
|
+
[](https://github.com/benethos-hub/mailbox/blob/main/LICENSE)
|
|
45
|
+
|
|
46
|
+
> **Beta, version 0.4.0.** Usable with real accounts. A breaking change
|
|
47
|
+
> of the API or the configuration is announced in the changelog. Stored
|
|
48
|
+
> data is carried forward by migrations.
|
|
49
|
+
|
|
50
|
+
What
|
|
51
|
+
[`mailbox-service`](https://github.com/benethos-hub/mailbox/tree/main/packages/mailbox-service)
|
|
52
|
+
and
|
|
53
|
+
[`mailbox-mcp`](https://github.com/benethos-hub/mailbox/tree/main/packages/mailbox-mcp)
|
|
54
|
+
of [Mailbox](https://github.com/benethos-hub/mailbox) both need, held
|
|
55
|
+
once, on PyPI as `benethos-mailbox-common`. The MCP server cannot see
|
|
56
|
+
the service, so what both use lives here.
|
|
57
|
+
|
|
58
|
+
It is installed with them and is not meant to be used on its own. It
|
|
59
|
+
is built for the packages of this repository, and its modules, groups
|
|
60
|
+
and extras may change between versions without notice. It carries the
|
|
61
|
+
version of the other packages, and the service and the MCP server pin
|
|
62
|
+
it to their own.
|
|
63
|
+
|
|
64
|
+
Its modules are in five groups, one per concern: `mail` (addresses
|
|
65
|
+
and the plain text of an HTML body), `log` (the log line and the
|
|
66
|
+
masking of secrets), `paths` (the folders of the operating system),
|
|
67
|
+
`settings` (a program's settings from the environment and a file)
|
|
68
|
+
and `values` (secrets, sizes, text and canonical JSON).
|
|
69
|
+
|
|
70
|
+
It needs the standard library alone. The extra `paths` brings
|
|
71
|
+
platformdirs for the folders of the operating system, `settings`
|
|
72
|
+
pydantic-settings for a program's settings, and `all` brings every
|
|
73
|
+
extra.
|
|
74
|
+
|
|
75
|
+
It sees neither the service, nor the MCP server, nor the client.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# <img src="https://raw.githubusercontent.com/benethos-hub/mailbox/main/assets/logo/icon-3d.svg" alt="" height="36" align="absmiddle"> mailbox-common
|
|
2
|
+
|
|
3
|
+
[](https://github.com/benethos-hub/mailbox/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/benethos-mailbox-common/)
|
|
5
|
+
[](https://pypi.org/project/benethos-mailbox-common/)
|
|
6
|
+
[](https://github.com/benethos-hub/mailbox/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
> **Beta, version 0.4.0.** Usable with real accounts. A breaking change
|
|
9
|
+
> of the API or the configuration is announced in the changelog. Stored
|
|
10
|
+
> data is carried forward by migrations.
|
|
11
|
+
|
|
12
|
+
What
|
|
13
|
+
[`mailbox-service`](https://github.com/benethos-hub/mailbox/tree/main/packages/mailbox-service)
|
|
14
|
+
and
|
|
15
|
+
[`mailbox-mcp`](https://github.com/benethos-hub/mailbox/tree/main/packages/mailbox-mcp)
|
|
16
|
+
of [Mailbox](https://github.com/benethos-hub/mailbox) both need, held
|
|
17
|
+
once, on PyPI as `benethos-mailbox-common`. The MCP server cannot see
|
|
18
|
+
the service, so what both use lives here.
|
|
19
|
+
|
|
20
|
+
It is installed with them and is not meant to be used on its own. It
|
|
21
|
+
is built for the packages of this repository, and its modules, groups
|
|
22
|
+
and extras may change between versions without notice. It carries the
|
|
23
|
+
version of the other packages, and the service and the MCP server pin
|
|
24
|
+
it to their own.
|
|
25
|
+
|
|
26
|
+
Its modules are in five groups, one per concern: `mail` (addresses
|
|
27
|
+
and the plain text of an HTML body), `log` (the log line and the
|
|
28
|
+
masking of secrets), `paths` (the folders of the operating system),
|
|
29
|
+
`settings` (a program's settings from the environment and a file)
|
|
30
|
+
and `values` (secrets, sizes, text and canonical JSON).
|
|
31
|
+
|
|
32
|
+
It needs the standard library alone. The extra `paths` brings
|
|
33
|
+
platformdirs for the folders of the operating system, `settings`
|
|
34
|
+
pydantic-settings for a program's settings, and `all` brings every
|
|
35
|
+
extra.
|
|
36
|
+
|
|
37
|
+
It sees neither the service, nor the MCP server, nor the client.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "benethos-mailbox-common"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "What mailbox-service and mailbox-mcp of Mailbox share: mail as plain text, the folders of the operating system, settings from a file, the time of a log line, secrets masked in text, JSON for a hash."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Benedikt Oswald" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"email",
|
|
16
|
+
"html-to-text",
|
|
17
|
+
"logging",
|
|
18
|
+
"redaction",
|
|
19
|
+
"self-hosted",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Intended Audience :: Information Technology",
|
|
25
|
+
"Intended Audience :: System Administrators",
|
|
26
|
+
"Natural Language :: English",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Programming Language :: Python :: 3.15",
|
|
35
|
+
"Topic :: Communications :: Email",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
# It sees neither the service, nor the MCP server, nor the client.
|
|
40
|
+
# The standard library only. A group with a library of its own names it
|
|
41
|
+
# in an extra named after the group.
|
|
42
|
+
dependencies = []
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
# The minimum is a version without a known vulnerability: the job
|
|
46
|
+
# lowest-versions of ci.yml checks it against OSV.
|
|
47
|
+
# The folders of the operating system for settings and data
|
|
48
|
+
# (paths/folders.py).
|
|
49
|
+
paths = ["platformdirs>=4.3"]
|
|
50
|
+
# The settings of a program from the environment and a settings file
|
|
51
|
+
# (settings/files.py).
|
|
52
|
+
settings = ["pydantic-settings>=2.10"]
|
|
53
|
+
# Every group.
|
|
54
|
+
all = ["benethos-mailbox-common[paths,settings]"]
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
Homepage = "https://github.com/benethos-hub/mailbox/tree/main/packages/mailbox-common#readme"
|
|
58
|
+
Repository = "https://github.com/benethos-hub/mailbox"
|
|
59
|
+
Issues = "https://github.com/benethos-hub/mailbox/issues"
|
|
60
|
+
Changelog = "https://github.com/benethos-hub/mailbox/blob/main/CHANGELOG.md"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.wheel]
|
|
63
|
+
packages = ["src/benethos_mailbox_common"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""mailbox-common: what the service and the MCP server of Mailbox both
|
|
2
|
+
need, held once, in groups, one per concern: ``mail``, ``log``, ``paths``
|
|
3
|
+
and ``values``. A module imports only inside its group, so a group can
|
|
4
|
+
be cut out as a package of its own. Callers import a module from its
|
|
5
|
+
group, ``from benethos_mailbox_common.log import lines``, never deeper.
|
|
6
|
+
This file imports no group."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = version("benethos-mailbox-common")
|
|
14
|
+
except PackageNotFoundError: # pragma: no cover - running from a bare tree
|
|
15
|
+
__version__ = "0.0.0"
|
|
16
|
+
|
|
17
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The lines of a log and the secrets kept out of them. Callers import
|
|
2
|
+
the modules from here, ``from benethos_mailbox_common.log import lines,
|
|
3
|
+
redact``."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from . import lines, redact
|
|
8
|
+
|
|
9
|
+
__all__ = ["lines", "redact"]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""The lines of a log, alike in every program of Mailbox (docs/LOGGING.md
|
|
2
|
+
6.9): the time, the level, where a line comes from and the message, a
|
|
3
|
+
noted secret masked (``redact``).
|
|
4
|
+
|
|
5
|
+
On a terminal a line is short and in colour, elsewhere, in a container
|
|
6
|
+
log or the journal, plain text. What a program logs, and its own kinds
|
|
7
|
+
of lines such as the access line of a web server, stay its own: it
|
|
8
|
+
hands ``Console`` a ``line_of`` for them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
from . import redact
|
|
20
|
+
|
|
21
|
+
FORMAT = "%(asctime)s %(levelname)-8s %(name)s: %(message)s"
|
|
22
|
+
# As uvicorn names them. Its trace level is below debug.
|
|
23
|
+
LEVELS = {
|
|
24
|
+
"critical": logging.CRITICAL,
|
|
25
|
+
"error": logging.ERROR,
|
|
26
|
+
"warning": logging.WARNING,
|
|
27
|
+
"info": logging.INFO,
|
|
28
|
+
"debug": logging.DEBUG,
|
|
29
|
+
"trace": 5,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ANSI colours, for a terminal only.
|
|
33
|
+
RESET = "\033[0m"
|
|
34
|
+
DIM = "\033[2m"
|
|
35
|
+
_LEVEL_COLOURS = {
|
|
36
|
+
logging.DEBUG: "\033[34m",
|
|
37
|
+
logging.INFO: "\033[32m",
|
|
38
|
+
logging.WARNING: "\033[33m",
|
|
39
|
+
logging.ERROR: "\033[31m",
|
|
40
|
+
logging.CRITICAL: "\033[1;31m",
|
|
41
|
+
}
|
|
42
|
+
_SOURCE = "\033[36m"
|
|
43
|
+
# The column of the source: the longest name of an activity of the
|
|
44
|
+
# service, ``activity.<area>.<name>``, has 32 characters (docs/LOGGING.md
|
|
45
|
+
# 7.2).
|
|
46
|
+
SOURCE_WIDTH = 32
|
|
47
|
+
|
|
48
|
+
# A program's own text for a record, None for the message as it is.
|
|
49
|
+
LineOf = Callable[[logging.LogRecord], str | None]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def log_time(value: datetime) -> str:
|
|
53
|
+
"""A time as every line of the log writes it, wherever it stands:
|
|
54
|
+
ISO 8601, the local time of this machine, to the millisecond, with the
|
|
55
|
+
offset. ``2026-09-28T10:12:22.123+02:00``."""
|
|
56
|
+
return value.astimezone().isoformat(timespec="milliseconds")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def colours_wanted() -> bool:
|
|
60
|
+
"""Colours on a terminal, unless ``NO_COLOR`` is set (no-color.org)."""
|
|
61
|
+
return sys.stderr.isatty() and "NO_COLOR" not in os.environ
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def short_source(name: str, package: str) -> str:
|
|
65
|
+
"""``domain.auth`` for the loggers of ``package``, ``http`` for the
|
|
66
|
+
access log of uvicorn, ``uvicorn`` for the server."""
|
|
67
|
+
if name == "uvicorn.access":
|
|
68
|
+
return "http"
|
|
69
|
+
if name.startswith("uvicorn"):
|
|
70
|
+
return "uvicorn"
|
|
71
|
+
return name.removeprefix(f"{package}.")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Redacting(logging.Formatter):
|
|
75
|
+
"""Each line as written, a noted secret masked: in the message, its
|
|
76
|
+
arguments and a traceback alike. The time as every line writes it
|
|
77
|
+
(``log_time``)."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, fmt: str = FORMAT) -> None:
|
|
80
|
+
super().__init__(fmt)
|
|
81
|
+
|
|
82
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
83
|
+
return redact.redact(super().format(record))
|
|
84
|
+
|
|
85
|
+
def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str:
|
|
86
|
+
return log_time(datetime.fromtimestamp(record.created).astimezone())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Console(Redacting):
|
|
90
|
+
"""A line for a person at a terminal: the time dim, the level in
|
|
91
|
+
colour, the source short (``short_source`` of ``package``), and the
|
|
92
|
+
text ``line_of`` gives, else the message. The time as in every other
|
|
93
|
+
line."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, package: str = "", line_of: LineOf | None = None) -> None:
|
|
96
|
+
super().__init__()
|
|
97
|
+
self._package = package
|
|
98
|
+
self._line_of = line_of
|
|
99
|
+
|
|
100
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
101
|
+
colour = _LEVEL_COLOURS.get(record.levelno, "")
|
|
102
|
+
source = short_source(record.name, self._package)
|
|
103
|
+
head = (
|
|
104
|
+
f"{DIM}{self.formatTime(record)}{RESET} "
|
|
105
|
+
f"{colour}{record.levelname:<8}{RESET} "
|
|
106
|
+
f"{_SOURCE}{source:<{SOURCE_WIDTH}}{RESET} "
|
|
107
|
+
)
|
|
108
|
+
own = self._line_of(record) if self._line_of is not None else None
|
|
109
|
+
text = own or record.getMessage()
|
|
110
|
+
if record.exc_info:
|
|
111
|
+
text += "\n" + self.formatException(record.exc_info)
|
|
112
|
+
return redact.redact(head + text)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def formatter(
|
|
116
|
+
package: str, *, colours: bool | None = None, line_of: LineOf | None = None
|
|
117
|
+
) -> logging.Formatter:
|
|
118
|
+
"""``Console`` where ``colours`` is true, by default on a terminal,
|
|
119
|
+
else the plain ``Redacting``."""
|
|
120
|
+
if colours is None:
|
|
121
|
+
colours = colours_wanted()
|
|
122
|
+
return Console(package, line_of) if colours else Redacting()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def stderr_handler(
|
|
126
|
+
package: str, *, colours: bool | None = None, line_of: LineOf | None = None
|
|
127
|
+
) -> logging.Handler:
|
|
128
|
+
"""A handler that writes to stderr with the ``formatter`` that fits."""
|
|
129
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
130
|
+
handler.setFormatter(formatter(package, colours=colours, line_of=line_of))
|
|
131
|
+
return handler
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""The secrets a program holds in plain text, kept out of what it writes.
|
|
2
|
+
|
|
3
|
+
The second line of defence (CONCEPT 7.4): no code logs a secret or puts
|
|
4
|
+
one into an error text on purpose. Whatever still carries one, a
|
|
5
|
+
library's message or a traceback, is written with ``***`` in its place.
|
|
6
|
+
A secret is noted where it is decrypted or received. The newest
|
|
7
|
+
``KEPT`` are held: access tokens come anew every hour, and one in use
|
|
8
|
+
is noted again on its next decryption.
|
|
9
|
+
|
|
10
|
+
The one module of the package with state of its own: a secret noted
|
|
11
|
+
where it is decrypted must be masked wherever a text is written, in every
|
|
12
|
+
layer and in the log, so the noted secrets are held process-wide.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import threading
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
|
|
20
|
+
MASK = "***"
|
|
21
|
+
# Shorter values are not masked: a password "a" would hide every "a".
|
|
22
|
+
SHORTEST = 6
|
|
23
|
+
KEPT = 256
|
|
24
|
+
|
|
25
|
+
_known: OrderedDict[str, None] = OrderedDict()
|
|
26
|
+
_lock = threading.Lock()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def note(secret: str) -> None:
|
|
30
|
+
"""Mask ``secret`` from now on, in every text ``redact`` sees."""
|
|
31
|
+
if len(secret) < SHORTEST:
|
|
32
|
+
return
|
|
33
|
+
with _lock:
|
|
34
|
+
_known[secret] = None
|
|
35
|
+
_known.move_to_end(secret)
|
|
36
|
+
while len(_known) > KEPT:
|
|
37
|
+
_known.popitem(last=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def redact(text: str) -> str:
|
|
41
|
+
"""``text`` with every noted secret masked, the longest first, so one
|
|
42
|
+
secret inside another leaves nothing of the longer behind."""
|
|
43
|
+
with _lock:
|
|
44
|
+
known = sorted(_known, key=len, reverse=True)
|
|
45
|
+
for secret in known:
|
|
46
|
+
if secret in text:
|
|
47
|
+
text = text.replace(secret, MASK)
|
|
48
|
+
return text
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""An address as people read it: ``Name <email>``, or the email alone.
|
|
2
|
+
Not the header of a message, which ``email.utils.formataddr`` writes,
|
|
3
|
+
encoding a name beyond ASCII."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def readable(name: str | None, email: str, *, quoted: bool = False) -> str:
|
|
9
|
+
"""``Name <email>``, the email alone without a name. ``quoted`` puts
|
|
10
|
+
the name in a quoted string (RFC 5322), with a backslash before a
|
|
11
|
+
backslash or a double quote in it, so a form reads it back as it was:
|
|
12
|
+
``"Smith, Ann" <ann@example.org>``."""
|
|
13
|
+
if not name:
|
|
14
|
+
return email
|
|
15
|
+
if quoted:
|
|
16
|
+
escaped = name.replace("\\", "\\\\").replace('"', '\\"')
|
|
17
|
+
return f'"{escaped}" <{email}>'
|
|
18
|
+
return f"{name} <{email}>"
|