nui-python-shared-utils 1.5.0__tar.gz → 1.6.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.
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/.github/workflows/ci.yml +11 -7
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/.github/workflows/publish.yml +18 -16
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/.github/workflows/test.yml +4 -2
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/PKG-INFO +1 -1
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/README.md +1 -0
- nui_python_shared_utils-1.6.0/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +108 -0
- nui_python_shared_utils-1.6.0/docs/decisions/README.md +26 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/powertools-integration.md +32 -16
- nui_python_shared_utils-1.6.0/docs/guides/structured-logging-contract.md +251 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_python_shared_utils.egg-info/SOURCES.txt +10 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/__init__.py +39 -0
- nui_python_shared_utils-1.6.0/nui_shared_utils/logging/__init__.py +68 -0
- nui_python_shared_utils-1.6.0/nui_shared_utils/logging/binding.py +355 -0
- nui_python_shared_utils-1.6.0/nui_shared_utils/logging/fields.py +253 -0
- nui_python_shared_utils-1.6.0/nui_shared_utils/logging/redaction.py +189 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_lazy_imports.py +29 -0
- nui_python_shared_utils-1.6.0/tests/test_logging_binding.py +369 -0
- nui_python_shared_utils-1.6.0/tests/test_logging_fields.py +150 -0
- nui_python_shared_utils-1.6.0/tests/test_redaction.py +127 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/.editorconfig +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/.markdownlint-cli2.yaml +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/AGENTS.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/CLAUDE.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/CONTRIBUTING.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/LICENSE +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/MANIFEST.in +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/README.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/development/testing.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/getting-started/configuration.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/getting-started/installation.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/getting-started/quickstart.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/cli-tools.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/elasticsearch-integration.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/jwt-authentication.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/lambda-utilities.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/llm-integration.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/log-processing.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/shared-types.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/slack-integration.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/docs/guides/snowflake-integration.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/mypy.ini +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/__init__.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/base_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/cli.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/cloudwatch_metrics.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/config.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/db_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/error_handler.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/es_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/es_query_builder.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/jwt_auth.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/lambda_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/log_processors.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/powertools_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/secrets_helper.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_formatter.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/__init__.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/channel_creator.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/channel_definitions.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/slack_setup/setup_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/snowflake_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/timezone.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_lambda_shared_utils/utils.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/base_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/cli.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/cloudwatch_metrics.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/config.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/db_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/error_handler.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/es_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/es_query_builder.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/jwt_auth.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/lambda_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/llm.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/log_processors.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/powertools_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/secrets_helper.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_formatter.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/__init__.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/snowflake_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/timezone.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/nui_shared_utils/utils.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/pyproject.toml +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/pytest.ini +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/redirect/README.md +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/redirect/pyproject.toml +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/redirect/setup.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/requirements-test.txt +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/scripts/bench_imports.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/setup.cfg +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/setup.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/__init__.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_aws_utils.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_base_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_cloudwatch_metrics.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_config.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_db_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_error_handler.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_es_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_es_query_builder.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_jwt_auth.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_lambda_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_llm.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_log_processors.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_powertools_helpers.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_secrets_helper.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_slack_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_slack_formatter.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_snowflake_client.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_timezone.py +0 -0
- {nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/tests/test_utils.py +0 -0
|
@@ -15,15 +15,17 @@ jobs:
|
|
|
15
15
|
python-version: ['3.9', '3.10', '3.11', '3.12']
|
|
16
16
|
|
|
17
17
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
18
|
+
- uses: actions/checkout@v5
|
|
19
|
+
with:
|
|
20
|
+
persist-credentials: false
|
|
19
21
|
|
|
20
22
|
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
-
uses: actions/setup-python@
|
|
23
|
+
uses: actions/setup-python@v6
|
|
22
24
|
with:
|
|
23
25
|
python-version: ${{ matrix.python-version }}
|
|
24
26
|
|
|
25
27
|
- name: Cache pip dependencies
|
|
26
|
-
uses: actions/cache@
|
|
28
|
+
uses: actions/cache@v6
|
|
27
29
|
with:
|
|
28
30
|
path: ~/.cache/pip
|
|
29
31
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml', '**/setup.py') }}
|
|
@@ -65,9 +67,9 @@ jobs:
|
|
|
65
67
|
pytest --cov=nui_shared_utils --cov-report=xml --cov-report=term-missing -v
|
|
66
68
|
|
|
67
69
|
- name: Upload coverage to Codecov
|
|
68
|
-
uses: codecov/codecov-action@
|
|
70
|
+
uses: codecov/codecov-action@v5
|
|
69
71
|
with:
|
|
70
|
-
|
|
72
|
+
files: ./coverage.xml
|
|
71
73
|
flags: unittests
|
|
72
74
|
name: codecov-umbrella
|
|
73
75
|
fail_ci_if_error: false
|
|
@@ -78,10 +80,12 @@ jobs:
|
|
|
78
80
|
needs: test
|
|
79
81
|
|
|
80
82
|
steps:
|
|
81
|
-
- uses: actions/checkout@
|
|
83
|
+
- uses: actions/checkout@v5
|
|
84
|
+
with:
|
|
85
|
+
persist-credentials: false
|
|
82
86
|
|
|
83
87
|
- name: Set up Python
|
|
84
|
-
uses: actions/setup-python@
|
|
88
|
+
uses: actions/setup-python@v6
|
|
85
89
|
with:
|
|
86
90
|
python-version: '3.11'
|
|
87
91
|
|
{nui_python_shared_utils-1.5.0 → nui_python_shared_utils-1.6.0}/.github/workflows/publish.yml
RENAMED
|
@@ -3,9 +3,7 @@ name: Build and Publish to PyPI
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
|
-
- '[0-9]*' # Trigger on version tags like 1.3.0
|
|
7
|
-
release:
|
|
8
|
-
types: [published]
|
|
6
|
+
- '[0-9]*' # Trigger on version tags like 1.3.0 (a published GitHub Release also creates the tag)
|
|
9
7
|
workflow_dispatch: # Allow manual triggering
|
|
10
8
|
|
|
11
9
|
jobs:
|
|
@@ -17,10 +15,12 @@ jobs:
|
|
|
17
15
|
python-version: ['3.9', '3.10', '3.11', '3.12']
|
|
18
16
|
|
|
19
17
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
21
|
-
|
|
18
|
+
- uses: actions/checkout@v5
|
|
19
|
+
with:
|
|
20
|
+
persist-credentials: false
|
|
21
|
+
|
|
22
22
|
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
-
uses: actions/setup-python@
|
|
23
|
+
uses: actions/setup-python@v6
|
|
24
24
|
with:
|
|
25
25
|
python-version: ${{ matrix.python-version }}
|
|
26
26
|
|
|
@@ -41,9 +41,9 @@ jobs:
|
|
|
41
41
|
pytest --cov=nui_shared_utils --cov-report=xml --cov-report=term-missing
|
|
42
42
|
|
|
43
43
|
- name: Upload coverage to Codecov
|
|
44
|
-
uses: codecov/codecov-action@
|
|
44
|
+
uses: codecov/codecov-action@v5
|
|
45
45
|
with:
|
|
46
|
-
|
|
46
|
+
files: ./coverage.xml
|
|
47
47
|
fail_ci_if_error: false
|
|
48
48
|
|
|
49
49
|
build:
|
|
@@ -52,12 +52,13 @@ jobs:
|
|
|
52
52
|
needs: test
|
|
53
53
|
|
|
54
54
|
steps:
|
|
55
|
-
- uses: actions/checkout@
|
|
55
|
+
- uses: actions/checkout@v5
|
|
56
56
|
with:
|
|
57
|
+
persist-credentials: false
|
|
57
58
|
fetch-depth: 0 # Full history for version calculation
|
|
58
59
|
|
|
59
60
|
- name: Set up Python
|
|
60
|
-
uses: actions/setup-python@
|
|
61
|
+
uses: actions/setup-python@v6
|
|
61
62
|
with:
|
|
62
63
|
python-version: '3.11'
|
|
63
64
|
|
|
@@ -75,7 +76,7 @@ jobs:
|
|
|
75
76
|
python -m tarfile -l dist/*.tar.gz
|
|
76
77
|
|
|
77
78
|
- name: Upload artifacts
|
|
78
|
-
uses: actions/upload-artifact@
|
|
79
|
+
uses: actions/upload-artifact@v6
|
|
79
80
|
with:
|
|
80
81
|
name: dist
|
|
81
82
|
path: dist/
|
|
@@ -85,7 +86,7 @@ jobs:
|
|
|
85
86
|
runs-on: ubuntu-latest
|
|
86
87
|
needs: [test, build]
|
|
87
88
|
# Only publish on tagged releases
|
|
88
|
-
if:
|
|
89
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
89
90
|
environment:
|
|
90
91
|
name: pypi
|
|
91
92
|
url: https://pypi.org/project/nui-python-shared-utils/
|
|
@@ -96,7 +97,7 @@ jobs:
|
|
|
96
97
|
|
|
97
98
|
steps:
|
|
98
99
|
- name: Download artifacts
|
|
99
|
-
uses: actions/download-artifact@
|
|
100
|
+
uses: actions/download-artifact@v7
|
|
100
101
|
with:
|
|
101
102
|
name: dist
|
|
102
103
|
path: dist/
|
|
@@ -106,13 +107,14 @@ jobs:
|
|
|
106
107
|
with:
|
|
107
108
|
verbose: true
|
|
108
109
|
print-hash: true
|
|
110
|
+
skip-existing: true # No-op instead of failing if the version is already on PyPI (safe re-runs)
|
|
109
111
|
|
|
110
112
|
publish-test:
|
|
111
113
|
name: Publish to TestPyPI
|
|
112
114
|
runs-on: ubuntu-latest
|
|
113
115
|
needs: [test, build]
|
|
114
|
-
# Publish to TestPyPI on manual
|
|
115
|
-
if: github.event_name == 'workflow_dispatch'
|
|
116
|
+
# Publish to TestPyPI on manual (non-tag) dispatch only; tag pushes go to PyPI above
|
|
117
|
+
if: github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/')
|
|
116
118
|
environment:
|
|
117
119
|
name: testpypi
|
|
118
120
|
url: https://test.pypi.org/project/nui-python-shared-utils/
|
|
@@ -123,7 +125,7 @@ jobs:
|
|
|
123
125
|
|
|
124
126
|
steps:
|
|
125
127
|
- name: Download artifacts
|
|
126
|
-
uses: actions/download-artifact@
|
|
128
|
+
uses: actions/download-artifact@v7
|
|
127
129
|
with:
|
|
128
130
|
name: dist
|
|
129
131
|
path: dist/
|
|
@@ -14,10 +14,12 @@ jobs:
|
|
|
14
14
|
python-version: ['3.9', '3.10', '3.11']
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
|
+
with:
|
|
19
|
+
persist-credentials: false
|
|
18
20
|
|
|
19
21
|
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
-
uses: actions/setup-python@
|
|
22
|
+
uses: actions/setup-python@v6
|
|
21
23
|
with:
|
|
22
24
|
python-version: ${{ matrix.python-version }}
|
|
23
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nui-python-shared-utils
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Shared Python utilities for AWS Lambda, CLI tools, and agents with Slack, Elasticsearch, and monitoring integrations
|
|
5
5
|
Home-page: https://github.com/nuimarkets/nui-python-shared-utils
|
|
6
6
|
Author: NUI Markets
|
|
@@ -17,6 +17,7 @@ Welcome to the comprehensive documentation for `nui-python-shared-utils` (former
|
|
|
17
17
|
Component-specific guides for major features:
|
|
18
18
|
|
|
19
19
|
- **[AWS Powertools Integration](guides/powertools-integration.md)** - Standardized logging, metrics, and error handling
|
|
20
|
+
- **[Structured Logging Contract](guides/structured-logging-contract.md)** - Canonical dotted field registry, request-scoped binders, and payload redaction
|
|
20
21
|
- **[Lambda Context Helpers](guides/lambda-utilities.md)** - Environment info extraction for logging and metrics
|
|
21
22
|
- **[Slack Integration](guides/slack-integration.md)** - Messaging, formatting, and file uploads
|
|
22
23
|
- **[Elasticsearch Integration](guides/elasticsearch-integration.md)** - Search, bulk indexing, health checks
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# ADR-001: Structured logging field vocabulary
|
|
2
|
+
|
|
3
|
+
**Status**: Proposed
|
|
4
|
+
**Date**: 2026-07-11
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
This package is the shared logging entry point for a fleet of Python Lambda functions and
|
|
9
|
+
long-running services whose structured logs are shipped to a central log store and queried by
|
|
10
|
+
operators and automated agents. Reliable querying, dashboards, and alerting need a predictable
|
|
11
|
+
field shape: consistent key names and stable leaf types across every producer.
|
|
12
|
+
|
|
13
|
+
Today the logger helper (`get_powertools_logger`) sets service, level, timezone, and timestamp
|
|
14
|
+
format, but takes no position on field names or shape. Each call site hand-assembles its own
|
|
15
|
+
`extra={}` keys. Across the fleet this has grown several incompatible conventions in the same log
|
|
16
|
+
stream: flat single-word keys (`order_id`), dotted namespaced keys (`request.order_id`), nested
|
|
17
|
+
dicts (`extra={"request": {...}}`), and unstructured message strings.
|
|
18
|
+
|
|
19
|
+
This is a **producer-side** problem, and its cost is independent of which store the logs land in:
|
|
20
|
+
|
|
21
|
+
- A **schema-enforcing** store (Elasticsearch) turns field drift into loud failures: mapping
|
|
22
|
+
conflicts and silent nulls when `status` is a number in one document and a string in the next.
|
|
23
|
+
- A **schema-less** store (VictoriaLogs, Loki) accepts the drift silently. `{"status": 200}` and
|
|
24
|
+
`{"status": "timeout"}` are both ingested with no error, and the skew surfaces later as a
|
|
25
|
+
dashboard panel or an alert rule (`5xx rate`, `exception spike`) that quietly stops matching.
|
|
26
|
+
|
|
27
|
+
So the log store is deliberately out of scope for this decision (it is itself under evaluation),
|
|
28
|
+
and the contract is **store-neutral**. The one property of the stack that does constrain the wire
|
|
29
|
+
shape is producer-side:
|
|
30
|
+
|
|
31
|
+
**The Powertools `Logger.append_keys` merge is shallow.** Binding context incrementally with
|
|
32
|
+
nested dicts clobbers earlier keys: `append_keys(request={"a": 1})` then `append_keys(request={"b": 2})`
|
|
33
|
+
emits `request={"b": 2}` and loses `a`. Dotted keys (`append_keys(**{"request.a": 1})`, then
|
|
34
|
+
`request.b`) are independent top-level keys and both survive. (Confirmed on `aws-lambda-powertools`
|
|
35
|
+
3.6.0.) Incremental binding (bind request at entry, add more later) therefore requires dotted keys.
|
|
36
|
+
|
|
37
|
+
(Incidental, not load-bearing: while Elasticsearch is a store, it expands dotted field names into
|
|
38
|
+
`object`-datatype paths at index time, so `request.order_id` and `{"request": {"order_id": ...}}`
|
|
39
|
+
map and query identically. Wire shape does not change queryability there. A store swap does not
|
|
40
|
+
disturb this decision because the decision does not rest on it.)
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
This package owns a canonical **dotted** field vocabulary as the fleet's logging contract, defined
|
|
45
|
+
independently of any log store:
|
|
46
|
+
|
|
47
|
+
- **A field registry (schema object)**: one importable module where each canonical dotted field
|
|
48
|
+
carries its type, whether it is indexed (i.e. meant to be queried vs retrieval-only), a
|
|
49
|
+
sensitivity flag, and any deprecated aliases. Covers `request.*`, `user.*`, `route.*`,
|
|
50
|
+
`response.*`, `error.*`, `trace.*`, `request.body_json` / `response.output_json`, plus a `target`
|
|
51
|
+
routing field. Single source of truth from which the default redaction set and the field-migration
|
|
52
|
+
map are derived. (A plain `dataclass`, not a heavyweight model dependency: the registry is static
|
|
53
|
+
metadata.)
|
|
54
|
+
- **Binding helpers** (`bind_request`, `bind_user`, `log_exception`, and a body helper that
|
|
55
|
+
serialises to a single `request.body_json` string with redaction) so callers never hand-type keys.
|
|
56
|
+
- **A consistency test** that asserts the registry's names, importable so consumers guard against
|
|
57
|
+
drift in CI.
|
|
58
|
+
- The vocabulary is **backend-agnostic**: consumed identically from the Powertools path and from a
|
|
59
|
+
standard-library JSON-formatter path, so short-lived functions and long-running services share one
|
|
60
|
+
contract.
|
|
61
|
+
|
|
62
|
+
Wire shape is **dotted**, names are **snake_case** dotted paths. Nested-dict emission is not used.
|
|
63
|
+
|
|
64
|
+
## Alternatives considered
|
|
65
|
+
|
|
66
|
+
- **Nested dicts as the emitted shape.** Ergonomic to build in one call and natural in Python.
|
|
67
|
+
Rejected because it clobbers under incremental `append_keys` binding, and because the fleet's
|
|
68
|
+
existing namespaced emitters already use dotted. Nested-*feeling* ergonomics are kept: the binding
|
|
69
|
+
helpers take keyword args and emit dotted keys.
|
|
70
|
+
- **Flat single-word keys** (`order_id`, `user_id`). Simplest, but no namespacing, so unrelated
|
|
71
|
+
fields collide and there is no structural grouping for operators or mappings.
|
|
72
|
+
- **A per-repo constants module.** Two codebases independently grew partial dotted taxonomies, which
|
|
73
|
+
proves the convention is wanted, but a per-repo copy re-introduces drift. One shared registry is
|
|
74
|
+
the point.
|
|
75
|
+
- **Bake the vocabulary into a single logger factory.** Cannot span the two logging backends in use,
|
|
76
|
+
so the contract lives in a backend-agnostic registry plus helpers rather than a factory.
|
|
77
|
+
- **Defer the contract until the log store is chosen.** Rejected: the drift is producer-side and
|
|
78
|
+
costs the same under either candidate store. A store-neutral contract is the piece that survives
|
|
79
|
+
(and is required by) a store migration, not one that blocks on it. Under a schema-less store it is
|
|
80
|
+
the *only* drift guard, since nothing downstream rejects a malformed field.
|
|
81
|
+
|
|
82
|
+
## Consequences
|
|
83
|
+
|
|
84
|
+
- One field shape across the fleet; the cost of field-name/type drift (loud mapping conflicts on a
|
|
85
|
+
schema-enforcing store, silent query/alert skew on a schema-less one) goes away as consumers adopt
|
|
86
|
+
it.
|
|
87
|
+
- `request.body_json` as one opaque, retrieval-only string keeps unbounded, caller-controlled payloads
|
|
88
|
+
from bloating the field set (a concern under any store, acute on a mapping-based one), and
|
|
89
|
+
centralises redaction in one helper. A response-payload twin (`response.output_json`) applies the
|
|
90
|
+
same treatment where a service logs unbounded response bodies.
|
|
91
|
+
- Callers stop hand-assembling keys; a name change happens in one registry, not N call sites; CI
|
|
92
|
+
catches drift.
|
|
93
|
+
- Consumers must migrate off flat/nested/message-only shapes. Adoption is opt-in per repo and staged,
|
|
94
|
+
so no consumer breaks on adoption.
|
|
95
|
+
- The registry's `type` / `indexed` metadata lets a schema-enforcing store derive its index mapping
|
|
96
|
+
from the registry (define once, derive the mapping). That export is store-specific and belongs with
|
|
97
|
+
the store's own tooling, not in this store-neutral core.
|
|
98
|
+
|
|
99
|
+
## Load-bearing premises (supersede triggers)
|
|
100
|
+
|
|
101
|
+
- Powertools `append_keys` stays a shallow merge. If it gained deep-merge semantics, the nested
|
|
102
|
+
ergonomics argument would reopen.
|
|
103
|
+
- Request-scoped context (bind at entry, clear at exit) is expressible on both backends without a
|
|
104
|
+
per-request logger factory. If a `contextvars`-based filter cannot isolate concurrent requests
|
|
105
|
+
cleanly on the long-running (non-Lambda) services, the backend-agnostic claim reopens.
|
|
106
|
+
- The value of a producer-side vocabulary does not depend on the store enforcing a schema. (It is
|
|
107
|
+
higher, not lower, when the store enforces nothing.) A change of log store does not supersede this
|
|
108
|
+
ADR.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Architecture Decision Records
|
|
2
|
+
|
|
3
|
+
Decisions that outlive any single change: the "why" behind a choice future readers will want to
|
|
4
|
+
verify, challenge, or build on. These are scoped to this package.
|
|
5
|
+
|
|
6
|
+
## Format
|
|
7
|
+
|
|
8
|
+
```markdown
|
|
9
|
+
# ADR-NNN: <Short title>
|
|
10
|
+
|
|
11
|
+
**Status**: Proposed | Accepted | Superseded by ADR-XXX
|
|
12
|
+
**Date**: YYYY-MM-DD
|
|
13
|
+
|
|
14
|
+
## Context # the situation forcing the decision (present tense, the standing rationale)
|
|
15
|
+
## Decision # what we decide to do
|
|
16
|
+
## Alternatives considered
|
|
17
|
+
## Consequences # what this makes easier, harder, or rules out
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Keep each ADR under two pages. Longer than that means it is a plan, not an ADR.
|
|
21
|
+
|
|
22
|
+
## Index
|
|
23
|
+
|
|
24
|
+
| # | Decision | Status | Date |
|
|
25
|
+
|---|----------|--------|------|
|
|
26
|
+
| [001](ADR-001-structured-logging-field-vocabulary.md) | Structured logging field vocabulary (dotted registry + helpers + consistency test) | Proposed | 2026-07-11 |
|
|
@@ -106,20 +106,29 @@ logger = get_powertools_logger("my-service", local_dev_colors=False)
|
|
|
106
106
|
|
|
107
107
|
### Structured Logging
|
|
108
108
|
|
|
109
|
-
The logger supports structured logging with the `extra` parameter
|
|
109
|
+
The logger supports structured logging with the `extra` parameter. Use the shared
|
|
110
|
+
**[structured logging contract](structured-logging-contract.md)** so field names are
|
|
111
|
+
consistent and queryable across the fleet: dotted, snake_case keys via the `F`
|
|
112
|
+
constants (and consumer-defined domain fields like `order.*`), not ad-hoc flat keys.
|
|
110
113
|
|
|
111
114
|
```python
|
|
115
|
+
from nui_shared_utils import F
|
|
116
|
+
|
|
112
117
|
logger.info(
|
|
113
118
|
"Order processed successfully",
|
|
114
119
|
extra={
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
+
"order.id": "ORD-12345", # consumer domain field (order.* namespace)
|
|
121
|
+
F.USER_ID: 789,
|
|
122
|
+
F.RESPONSE_DURATION_MS: 123,
|
|
123
|
+
},
|
|
120
124
|
)
|
|
121
125
|
```
|
|
122
126
|
|
|
127
|
+
Better still, bind request/user context once per request with the contract's binding
|
|
128
|
+
helpers so every log line carries it automatically. See the
|
|
129
|
+
[contract guide](structured-logging-contract.md) for `bind_request` / `bind_user` /
|
|
130
|
+
`log_exception` and the full field registry.
|
|
131
|
+
|
|
123
132
|
### Elasticsearch-Compatible Timestamps
|
|
124
133
|
|
|
125
134
|
The logger automatically formats timestamps for Elasticsearch compatibility:
|
|
@@ -209,7 +218,7 @@ def handler(event, context):
|
|
|
209
218
|
### Complete Example (All Features)
|
|
210
219
|
|
|
211
220
|
```python
|
|
212
|
-
from nui_shared_utils import get_powertools_logger, powertools_handler
|
|
221
|
+
from nui_shared_utils import get_powertools_logger, powertools_handler, F
|
|
213
222
|
|
|
214
223
|
logger = get_powertools_logger("order-processor")
|
|
215
224
|
|
|
@@ -230,7 +239,7 @@ def handler(event, context):
|
|
|
230
239
|
- Proper error response formatting
|
|
231
240
|
"""
|
|
232
241
|
|
|
233
|
-
logger.info("Processing order", extra={
|
|
242
|
+
logger.info("Processing order", extra={F.REQUEST_ID: event.get("id")})
|
|
234
243
|
|
|
235
244
|
try:
|
|
236
245
|
order = validate_order(event)
|
|
@@ -239,8 +248,8 @@ def handler(event, context):
|
|
|
239
248
|
logger.info(
|
|
240
249
|
"Order processed successfully",
|
|
241
250
|
extra={
|
|
242
|
-
"
|
|
243
|
-
|
|
251
|
+
"order.id": order["id"], # consumer domain field
|
|
252
|
+
F.RESPONSE_DURATION_MS: result["duration"],
|
|
244
253
|
}
|
|
245
254
|
)
|
|
246
255
|
|
|
@@ -250,7 +259,7 @@ def handler(event, context):
|
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
except ValidationError as e:
|
|
253
|
-
logger.error("Order validation failed", extra={
|
|
262
|
+
logger.error("Order validation failed", extra={F.ERROR_TYPE: type(e).__name__, F.ERROR_MESSAGE: str(e)})
|
|
254
263
|
return {
|
|
255
264
|
"statusCode": 400,
|
|
256
265
|
"body": json.dumps({"error": "Invalid order"})
|
|
@@ -510,12 +519,17 @@ logger: Union[PowertoolsLogger, PythonLogger] = get_powertools_logger("my-servic
|
|
|
510
519
|
|
|
511
520
|
### 1. Use Structured Logging
|
|
512
521
|
|
|
522
|
+
Use the [structured logging contract](structured-logging-contract.md)'s dotted field
|
|
523
|
+
names (via `F` and consumer domain fields), not ad-hoc flat keys:
|
|
524
|
+
|
|
513
525
|
```python
|
|
514
|
-
|
|
526
|
+
from nui_shared_utils import F
|
|
527
|
+
|
|
528
|
+
# ✓ Good - Searchable, filterable, consistent field names
|
|
515
529
|
logger.info("Order created", extra={
|
|
516
|
-
"
|
|
517
|
-
|
|
518
|
-
|
|
530
|
+
"order.id": order.id, # consumer domain field
|
|
531
|
+
F.USER_ID: user.id,
|
|
532
|
+
F.RESPONSE_STATUS: 200,
|
|
519
533
|
})
|
|
520
534
|
|
|
521
535
|
# ✗ Bad - Unstructured string
|
|
@@ -563,6 +577,8 @@ def handler(event, context):
|
|
|
563
577
|
### 4. Graceful Error Handling
|
|
564
578
|
|
|
565
579
|
```python
|
|
580
|
+
from nui_shared_utils import F
|
|
581
|
+
|
|
566
582
|
@powertools_handler(service_name="my-service", slack_alert_channel="#errors")
|
|
567
583
|
@logger.inject_lambda_context
|
|
568
584
|
def handler(event, context):
|
|
@@ -573,7 +589,7 @@ def handler(event, context):
|
|
|
573
589
|
|
|
574
590
|
except ValidationError as e:
|
|
575
591
|
# Expected errors - log and return 400
|
|
576
|
-
logger.warning("Validation failed", extra={
|
|
592
|
+
logger.warning("Validation failed", extra={F.ERROR_TYPE: type(e).__name__, F.ERROR_MESSAGE: str(e)})
|
|
577
593
|
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
|
|
578
594
|
|
|
579
595
|
# Unexpected errors caught by decorator -> 500 + Slack alert
|