nui-python-shared-utils 2.0__tar.gz → 2.2__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-2.0 → nui_python_shared_utils-2.2}/.github/workflows/ci.yml +39 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/AGENTS.md +2 -1
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/PKG-INFO +7 -5
- nui_python_shared_utils-2.2/docs/decisions/ADR-007-elasticsearch-client-version-range.md +85 -0
- nui_python_shared_utils-2.2/docs/decisions/ADR-008-stdlib-zoneinfo-over-pytz.md +89 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/decisions/README.md +2 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/getting-started/installation.md +25 -2
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/elasticsearch-integration.md +17 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/slack-integration.md +150 -45
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_python_shared_utils.egg-info/SOURCES.txt +3 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/__init__.py +2 -1
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/es_client.py +22 -8
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/slack_client.py +471 -121
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/slack_formatter.py +10 -11
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/timezone.py +13 -9
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/pyproject.toml +15 -4
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/requirements-test.txt +7 -2
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/setup.py +15 -4
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_es_client.py +42 -5
- nui_python_shared_utils-2.2/tests/test_es_client_compat.py +159 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_es_query_builder.py +24 -25
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_slack_client.py +707 -1
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_slack_formatter.py +42 -7
- nui_python_shared_utils-2.2/tests/test_timezone.py +277 -0
- nui_python_shared_utils-2.0/tests/test_timezone.py +0 -148
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/.editorconfig +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/.github/workflows/publish.yml +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/.github/workflows/test.yml +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/.markdownlint-cli2.yaml +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/CLAUDE.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/CONTRIBUTING.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/LICENSE +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/MANIFEST.in +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/README.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/README.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/decisions/ADR-002-keep-shared-utilities-generic.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/decisions/ADR-003-lazy-load-integrations.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/decisions/ADR-004-credential-resolution-precedence.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/decisions/ADR-005-explicit-secrets-manager-region.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/decisions/ADR-006-snowflake-sql-api-client.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/development/testing.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/getting-started/configuration.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/getting-started/quickstart.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/cli-tools.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/jwt-authentication.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/lambda-utilities.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/llm-integration.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/log-processing.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/powertools-integration.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/shared-types.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/snowflake-integration.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/structured-logging-contract.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/mypy.ini +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/base_client.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/cli.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/cloudwatch_metrics.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/config.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/db_client.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/error_handler.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/es_query_builder.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/jwt_auth.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/lambda_helpers.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/llm.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/log_processors.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/logging/__init__.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/logging/binding.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/logging/fields.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/logging/redaction.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/powertools_helpers.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/secrets_helper.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/slack_setup/__init__.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/snowflake_client.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/nui_shared_utils/utils.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/pytest.ini +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/redirect/README.md +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/redirect/pyproject.toml +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/redirect/setup.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/scripts/bench_imports.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/setup.cfg +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/__init__.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_aws_utils.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_base_client.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_cloudwatch_metrics.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_config.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_db_client.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_error_handler.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_jwt_auth.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_lambda_helpers.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_lazy_imports.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_llm.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_log_processors.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_logging_binding.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_logging_fields.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_powertools_helpers.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_redaction.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_secrets_helper.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_snowflake_client.py +0 -0
- {nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/tests/test_utils.py +0 -0
|
@@ -6,6 +6,9 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [ main, master, develop ]
|
|
8
8
|
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
9
12
|
jobs:
|
|
10
13
|
test:
|
|
11
14
|
name: Test Python ${{ matrix.python-version }}
|
|
@@ -74,6 +77,42 @@ jobs:
|
|
|
74
77
|
name: codecov-umbrella
|
|
75
78
|
fail_ci_if_error: false
|
|
76
79
|
|
|
80
|
+
es-compat:
|
|
81
|
+
name: Elasticsearch client ${{ matrix.elasticsearch-version }}
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
strategy:
|
|
84
|
+
fail-fast: false
|
|
85
|
+
matrix:
|
|
86
|
+
# Every major the [elasticsearch] extra allows. The ES tests drive the
|
|
87
|
+
# real client with a stubbed transport, so a call shape that a major
|
|
88
|
+
# stops accepting fails here. 8.x is pinned at three points because the
|
|
89
|
+
# client handles `body=` differently across the major: 8.0-8.11 route it
|
|
90
|
+
# through the parameter-rewriting fallback, 8.12+ take it as a declared
|
|
91
|
+
# parameter, and a bare `8.*` only ever resolves to the latter.
|
|
92
|
+
elasticsearch-version: ['7.17.*', '8.0.*', '8.11.*', '8.*', '9.*']
|
|
93
|
+
|
|
94
|
+
steps:
|
|
95
|
+
- uses: actions/checkout@v5
|
|
96
|
+
with:
|
|
97
|
+
persist-credentials: false
|
|
98
|
+
|
|
99
|
+
- name: Set up Python
|
|
100
|
+
uses: actions/setup-python@v6
|
|
101
|
+
with:
|
|
102
|
+
python-version: '3.11'
|
|
103
|
+
|
|
104
|
+
- name: Install dependencies
|
|
105
|
+
run: |
|
|
106
|
+
python -m pip install --upgrade pip
|
|
107
|
+
pip install pytest pytest-mock
|
|
108
|
+
pip install -e .
|
|
109
|
+
pip install "elasticsearch==${{ matrix.elasticsearch-version }}"
|
|
110
|
+
python -c "import elasticsearch; print('elasticsearch', elasticsearch.__version__)"
|
|
111
|
+
|
|
112
|
+
- name: Test Elasticsearch client
|
|
113
|
+
run: |
|
|
114
|
+
pytest tests/test_es_client.py tests/test_es_client_compat.py tests/test_es_query_builder.py -v
|
|
115
|
+
|
|
77
116
|
build:
|
|
78
117
|
name: Build Package
|
|
79
118
|
runs-on: ubuntu-latest
|
|
@@ -40,7 +40,7 @@ This is `nui-python-shared-utils`, a Python package providing production-ready u
|
|
|
40
40
|
- **JWT Authentication** (`jwt_auth.py`) - RS256 token validation for API Gateway Lambdas (uses `rsa` package)
|
|
41
41
|
- **LLM Helper** (`llm.py`) - Generic Anthropic (Claude) client plumbing: API-key/Bedrock auth, forced tool-use call, text call (uses `anthropic`)
|
|
42
42
|
- **Error Handling** (`error_handler.py`) - Retry patterns with exponential backoff
|
|
43
|
-
- **Timezone Utils** (`timezone.py`) - Timezone conversion and formatting utilities
|
|
43
|
+
- **Timezone Utils** (`timezone.py`) - Timezone conversion and formatting utilities (stdlib `zoneinfo`)
|
|
44
44
|
|
|
45
45
|
### Optional Dependencies
|
|
46
46
|
|
|
@@ -52,6 +52,7 @@ The package uses optional extras to minimize Lambda bundle size:
|
|
|
52
52
|
- `jwt` - RS256 JWT validation (`rsa` package)
|
|
53
53
|
- `snowflake` - Pure-Python Snowflake SQL API client (`snowflake-sql-api`)
|
|
54
54
|
- `llm` - Anthropic (Claude) client helper (`anthropic[bedrock]`, both API-key and Bedrock IAM auth)
|
|
55
|
+
- `timezone` - `tzdata`, for minimal images with no system IANA database (Alpine, distroless)
|
|
55
56
|
- `all` - All integrations
|
|
56
57
|
- `dev` - Development and testing tools
|
|
57
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nui-python-shared-utils
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
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
|
|
@@ -49,11 +49,11 @@ Requires-Python: >=3.9
|
|
|
49
49
|
Description-Content-Type: text/markdown
|
|
50
50
|
License-File: LICENSE
|
|
51
51
|
Requires-Dist: boto3>=1.20.0
|
|
52
|
-
Requires-Dist: pytz>=2021.3
|
|
53
52
|
Requires-Dist: click>=8.0.0
|
|
54
53
|
Requires-Dist: pyyaml>=6.0
|
|
54
|
+
Requires-Dist: tzdata>=2023.3; platform_system == "Windows"
|
|
55
55
|
Provides-Extra: elasticsearch
|
|
56
|
-
Requires-Dist: elasticsearch<
|
|
56
|
+
Requires-Dist: elasticsearch<10.0.0,>=7.17.0; extra == "elasticsearch"
|
|
57
57
|
Provides-Extra: database
|
|
58
58
|
Requires-Dist: pymysql>=1.0.0; extra == "database"
|
|
59
59
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "database"
|
|
@@ -64,12 +64,14 @@ Requires-Dist: aws-lambda-powertools<4.0.0,>=3.6.0; extra == "powertools"
|
|
|
64
64
|
Requires-Dist: coloredlogs>=15.0; extra == "powertools"
|
|
65
65
|
Provides-Extra: jwt
|
|
66
66
|
Requires-Dist: rsa>=4.9; extra == "jwt"
|
|
67
|
+
Provides-Extra: timezone
|
|
68
|
+
Requires-Dist: tzdata>=2023.3; extra == "timezone"
|
|
67
69
|
Provides-Extra: snowflake
|
|
68
70
|
Requires-Dist: snowflake-sql-api<0.2.0,>=0.1.1; extra == "snowflake"
|
|
69
71
|
Provides-Extra: llm
|
|
70
72
|
Requires-Dist: anthropic[bedrock]<1.0.0,>=0.45.0; extra == "llm"
|
|
71
73
|
Provides-Extra: all
|
|
72
|
-
Requires-Dist: elasticsearch<
|
|
74
|
+
Requires-Dist: elasticsearch<10.0.0,>=7.17.0; extra == "all"
|
|
73
75
|
Requires-Dist: pymysql>=1.0.0; extra == "all"
|
|
74
76
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
75
77
|
Requires-Dist: slack-sdk>=3.19.0; extra == "all"
|
|
@@ -85,11 +87,11 @@ Requires-Dist: black>=22.0.0; extra == "dev"
|
|
|
85
87
|
Requires-Dist: mypy>=0.990; extra == "dev"
|
|
86
88
|
Requires-Dist: boto3-stubs[essential]>=1.20.0; extra == "dev"
|
|
87
89
|
Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
|
|
88
|
-
Requires-Dist: types-pytz>=2021.3.0; extra == "dev"
|
|
89
90
|
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
90
91
|
Requires-Dist: build>=0.8.0; extra == "dev"
|
|
91
92
|
Requires-Dist: rsa>=4.9; extra == "dev"
|
|
92
93
|
Requires-Dist: cryptography>=41.0.0; extra == "dev"
|
|
94
|
+
Requires-Dist: tzdata>=2023.3; extra == "dev"
|
|
93
95
|
Requires-Dist: snowflake-sql-api<0.2.0,>=0.1.1; extra == "dev"
|
|
94
96
|
Requires-Dist: anthropic[bedrock]<1.0.0,>=0.45.0; extra == "dev"
|
|
95
97
|
Dynamic: author
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ADR-007: Support elasticsearch-py 7.17, 8.x and 9.x from one call shape
|
|
2
|
+
|
|
3
|
+
**Status**: Proposed
|
|
4
|
+
**Date**: 2026-07-27
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The `[elasticsearch]` extra selects the client library a consumer installs alongside this
|
|
9
|
+
package. Because an extra is a hard requirement range, a narrow range is not a
|
|
10
|
+
conservative default: it is a wall. A consumer already on `elasticsearch>=8` cannot install
|
|
11
|
+
`nui-python-shared-utils[elasticsearch]` at all if the extra caps at `<8` -- the resolver
|
|
12
|
+
fails outright rather than degrading. Consumers span clusters of different vintages, so any
|
|
13
|
+
single-major range excludes part of the audience.
|
|
14
|
+
|
|
15
|
+
The client majors differ in how a request body is passed:
|
|
16
|
+
|
|
17
|
+
- **7.17** takes the whole request as `body=`, with URL parameters (`size`, `ignore_unavailable`)
|
|
18
|
+
as separate keywords. It emits a deprecation warning for `body=` pointing at 8.x.
|
|
19
|
+
- **8.x / 9.x** expose body fields as first-class keywords (`query=`, `aggs=`, `size=`) and keep
|
|
20
|
+
`body=` as a raw escape hatch. Mixing the two forms is the trap: passing `body=` *and* a body
|
|
21
|
+
field as a keyword folds that keyword into the caller's dict (mutating it in place) and raises
|
|
22
|
+
`ValueError: Received multiple values for 'size'` if the key is already present. So
|
|
23
|
+
`search(index=..., body=body, size=n)` works once, then fails the second time the same body
|
|
24
|
+
dict is reused with a different size.
|
|
25
|
+
|
|
26
|
+
That mixed form is what `es_client` sent, which means the `<8` cap was not merely stale: the
|
|
27
|
+
code as written was correct only for 7.x.
|
|
28
|
+
|
|
29
|
+
## Decision
|
|
30
|
+
|
|
31
|
+
The `[elasticsearch]` extra allows `>=7.17.0,<10.0.0`, and `es_client` sends the one call
|
|
32
|
+
shape all three majors accept: `size` folded into a **copy** of the caller's body
|
|
33
|
+
(`_body_with_size`), passed as `body=` with only genuine URL parameters
|
|
34
|
+
(`ignore_unavailable`) as keywords. No version sniffing, no branching.
|
|
35
|
+
|
|
36
|
+
- `search` / `aggregate` carry `size` inside the body copy; an explicit `size` argument wins
|
|
37
|
+
over a `size` already in the body, and the caller's dict is never mutated.
|
|
38
|
+
- `count`, `cat.indices`, `info` and `cluster.health` already used a form valid on every major.
|
|
39
|
+
- `get_indices_info` normalises the 8.x/9.x response wrapper to a plain `list`.
|
|
40
|
+
|
|
41
|
+
`tests/test_es_client_compat.py` drives the real client with a stubbed transport (no server),
|
|
42
|
+
so an incompatible call shape fails as a test rather than in a consumer's Lambda. The CI
|
|
43
|
+
`es-compat` matrix runs it against 7.17, 8.x and 9.x, pinning 8.x at three points (8.0, 8.11
|
|
44
|
+
and latest) because the client routes `body=` through the parameter-rewriting fallback up to
|
|
45
|
+
8.11 and takes it as a declared parameter from 8.12 on.
|
|
46
|
+
|
|
47
|
+
The `<10` ceiling is the untested next major, not a known incompatibility.
|
|
48
|
+
|
|
49
|
+
## Alternatives considered
|
|
50
|
+
|
|
51
|
+
- **Cap at `<8` and tell consumers to pin down.** The status quo. Rejected: it forecloses the
|
|
52
|
+
extra for anyone on a current client, and the resolver failure (`ResolutionImpossible`) gives
|
|
53
|
+
no path forward short of forking.
|
|
54
|
+
- **Move to `>=8` only and use the native keyword API** (`query=`, `aggs=`, `size=`). Cleaner
|
|
55
|
+
code, and drops a deprecated form. Rejected because it moves the wall rather than removing
|
|
56
|
+
it: consumers on 7.17 clusters would be the ones locked out, and this package exists to be
|
|
57
|
+
installable next to what a consumer already runs.
|
|
58
|
+
- **Detect the installed major at runtime and branch** (`body=` on 7.x, keywords on 8/9).
|
|
59
|
+
Rejected: two call paths, one of which is exercised only on whichever major CI happens to
|
|
60
|
+
install, for no behavioural gain over a shape that is already valid everywhere.
|
|
61
|
+
- **Mock-only tests.** The existing ES tests replace `Elasticsearch` with a `Mock`, which pins
|
|
62
|
+
our call shape but cannot tell whether the installed client accepts it -- precisely the bug
|
|
63
|
+
class here. Hence the stubbed-transport tests against the real client.
|
|
64
|
+
|
|
65
|
+
## Consequences
|
|
66
|
+
|
|
67
|
+
- The extra installs alongside 7.17, 8.x or 9.x, so the ES helpers are reusable by consumers on
|
|
68
|
+
current clients instead of only legacy ones.
|
|
69
|
+
- Body dicts are safe to reuse across calls on every major; the 8.x/9.x in-place mutation is gone.
|
|
70
|
+
- `size` is a body field in the request we send. A consumer reading the wire format sees it in
|
|
71
|
+
the body rather than the query string, on every major.
|
|
72
|
+
- The compat tests bind us to the real client's request-building layer, so a breaking change in a
|
|
73
|
+
future client shows up as a CI failure in the `es-compat` job.
|
|
74
|
+
|
|
75
|
+
## Load-bearing premises (supersede triggers)
|
|
76
|
+
|
|
77
|
+
- **`body=` remains a supported escape hatch on 8.x/9.x.** It is documented as such today. If a
|
|
78
|
+
future major removes it, the single-shape decision breaks and the choice becomes: drop 7.x and
|
|
79
|
+
move to the keyword API, or branch by version.
|
|
80
|
+
- **Consumers still run 7.17 clients.** The whole cost here is keeping a deprecated call form
|
|
81
|
+
alive. Once no supported consumer needs 7.x, raising the floor to `>=8` and moving to the
|
|
82
|
+
native keyword API is strictly simpler.
|
|
83
|
+
- **The extra's range is a hard constraint on the consumer's environment.** If the ES helpers
|
|
84
|
+
ever moved behind a runtime-optional import with no declared range, the reasoning for a wide
|
|
85
|
+
range would change.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ADR-008: Zone data from stdlib `zoneinfo`, not a bundled `pytz`
|
|
2
|
+
|
|
3
|
+
**Status**: Proposed
|
|
4
|
+
**Date**: 2026-07-27
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Every install of this package pays for its base dependencies, whatever the consumer imports.
|
|
9
|
+
`pytz` sat in that base set for two modules (`timezone.py`, `slack_formatter.py`), so a Lambda
|
|
10
|
+
that wanted only the logging contract or the metrics helper still shipped it.
|
|
11
|
+
|
|
12
|
+
The cost is not the code. `pytz` bundles its own copy of the Olson/IANA database (0.86 MB of a
|
|
13
|
+
1.0 MB installed package, measured on `pytz==2025.2`), and that copy is redundant on
|
|
14
|
+
every interpreter this package supports: `requires-python = ">=3.9"`, and `zoneinfo` has been in
|
|
15
|
+
the standard library since 3.9, reading the system database that Linux, macOS, and the AWS Lambda
|
|
16
|
+
runtimes all ship.
|
|
17
|
+
|
|
18
|
+
`pytz` also carries an API predating PEP 495. Its zones must be attached through
|
|
19
|
+
`tz.localize(dt)` rather than `datetime(..., tzinfo=tz)`, because a bare `pytz` zone object
|
|
20
|
+
defaults to whatever offset was in effect at the start of that zone's records: attaching
|
|
21
|
+
`pytz.timezone("Pacific/Auckland")` to a 2024 datetime yields the 1868 LMT offset of `+11:39`.
|
|
22
|
+
`ZoneInfo` resolves the offset from the datetime itself, giving `+13:00`, so the direct
|
|
23
|
+
constructor form is correct.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
Zone data comes from stdlib `zoneinfo`. `pytz` is dropped from the base dependencies and from
|
|
28
|
+
the package entirely, with no compatibility shim.
|
|
29
|
+
|
|
30
|
+
- `NZ_TZ`, `AUS_TZ`, `US_EAST_TZ`, `US_WEST_TZ`, `EUR_TZ` are `ZoneInfo` instances.
|
|
31
|
+
- `pytz.UTC.localize(dt)` becomes `dt.replace(tzinfo=timezone.utc)`.
|
|
32
|
+
- `slack_formatter` imports `NZ_TZ` from `timezone` rather than constructing its own zone, so
|
|
33
|
+
one module owns the zone constants.
|
|
34
|
+
- The package's own conversions take UTC input, so no code path here has to resolve an ambiguous
|
|
35
|
+
wall time. Callers attaching a zone to a naive local time do, and the default differs from
|
|
36
|
+
`pytz`: `datetime(..., tzinfo=NZ_TZ)` uses `fold=0`, the first (still-daylight) occurrence of a
|
|
37
|
+
repeated hour, where `pytz.localize()` defaulted to `is_dst=False`, the second. `fold=1`
|
|
38
|
+
reproduces the old default. `TestAmbiguousWallTimes` pins both sides.
|
|
39
|
+
|
|
40
|
+
Zone data availability is handled where it actually differs by platform, rather than by shipping
|
|
41
|
+
a database to everyone:
|
|
42
|
+
|
|
43
|
+
- `tzdata>=2023.3; platform_system == "Windows"` in the base dependencies. Windows ships no
|
|
44
|
+
system database; POSIX consumers resolve the marker to nothing and install neither package.
|
|
45
|
+
- A `[timezone]` extra (`tzdata`) for minimal images (Alpine, distroless) that carry no
|
|
46
|
+
`/usr/share/zoneinfo`. No environment marker can detect those, so it is opt-in.
|
|
47
|
+
|
|
48
|
+
## Alternatives considered
|
|
49
|
+
|
|
50
|
+
- **Move `pytz` into an extra, keep the code as-is.** Fixes the unconditional cost and nothing
|
|
51
|
+
else: consumers of `timezone.py` still install a redundant database, and the `localize()` API
|
|
52
|
+
stays. Rejected as half the change for most of the churn.
|
|
53
|
+
- **Keep `pytz` and accept the base-dependency cost.** The status quo. Rejected: the package
|
|
54
|
+
exists to be bundled into Lambdas, where the base set is the floor nobody can opt out of.
|
|
55
|
+
- **Depend on `tzdata` unconditionally instead of the system database.** Trades one bundled
|
|
56
|
+
database for another (a 0.35 MB wheel) on platforms that already have one. Rejected; the
|
|
57
|
+
marker plus the extra cover the platforms that genuinely need it.
|
|
58
|
+
- **Ship a `localize()`-compatible wrapper for the exported zone constants.** Rejected: it
|
|
59
|
+
preserves a pre-PEP-495 idiom indefinitely to spare a one-line call-site change.
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
|
|
63
|
+
- A base install drops ~1 MB and one runtime dependency. Nothing in the package imports a
|
|
64
|
+
third-party timezone library.
|
|
65
|
+
- **Breaking for code importing the zone constants from `nui_shared_utils.timezone`.**
|
|
66
|
+
`NZ_TZ.localize(dt)` is gone; use `datetime(..., tzinfo=NZ_TZ)` or `dt.replace(tzinfo=NZ_TZ)`,
|
|
67
|
+
with `fold=1` where the old `is_dst=False` default mattered. `NZ_TZ.zone` is now `NZ_TZ.key`.
|
|
68
|
+
The top-level package exports only `nz_time` / `format_nz_time`, whose behaviour is unchanged,
|
|
69
|
+
so consumers going through `nui_shared_utils` see nothing.
|
|
70
|
+
- Attaching a zone at construction is now correct rather than a bug, so the sharp edge that made
|
|
71
|
+
`localize()` mandatory is gone.
|
|
72
|
+
- `slack_formatter.format_nz_time()` called with no argument is fixed as a side effect. It built
|
|
73
|
+
a naive `datetime.utcnow()` and handed it to `astimezone()`, which reads a naive datetime as
|
|
74
|
+
*local* time: right on a UTC host, an hour or thirteen out anywhere else. It now starts from an
|
|
75
|
+
aware UTC now, so the result no longer depends on the host clock's zone.
|
|
76
|
+
- On an image with no system database, `import nui_shared_utils.timezone` raises
|
|
77
|
+
`ZoneInfoNotFoundError` instead of silently working. That is a louder failure than `pytz` gave,
|
|
78
|
+
and the `[timezone]` extra is the fix.
|
|
79
|
+
- Zone data tracks the host, not a pinned package version. DST rule changes arrive with system
|
|
80
|
+
updates instead of a dependency bump, which is the desired direction for long-lived Lambda
|
|
81
|
+
images but does mean a stale base image carries stale rules.
|
|
82
|
+
|
|
83
|
+
## Load-bearing premises (supersede triggers)
|
|
84
|
+
|
|
85
|
+
- **The interpreter floor stays at 3.9+.** `zoneinfo` is stdlib from 3.9. A drop below that
|
|
86
|
+
would need `backports.zoneinfo`, at which point the calculus changes.
|
|
87
|
+
- **Target runtimes ship a system IANA database.** True of Linux, macOS, and the AWS Lambda
|
|
88
|
+
runtimes today. If the primary deployment target became one that does not, `tzdata` belongs in
|
|
89
|
+
the base dependencies unconditionally and this ADR's marker split is obsolete.
|
|
@@ -29,3 +29,5 @@ Keep each ADR under two pages. Longer than that means it is a plan, not an ADR.
|
|
|
29
29
|
| [004](ADR-004-credential-resolution-precedence.md) | Credential resolution precedence: explicit dict, then env vars, then Secrets Manager | Proposed | 2026-07-13 |
|
|
30
30
|
| [005](ADR-005-explicit-secrets-manager-region.md) | Resolve the Secrets Manager region explicitly (session, then env); no hidden default, fail loud | Proposed | 2026-07-13 |
|
|
31
31
|
| [006](ADR-006-snowflake-sql-api-client.md) | Snowflake access via the pure-Python SQL API client, not the native connector | Proposed | 2026-07-13 |
|
|
32
|
+
| [007](ADR-007-elasticsearch-client-version-range.md) | Support elasticsearch-py 7.17, 8.x and 9.x from one call shape | Proposed | 2026-07-27 |
|
|
33
|
+
| [008](ADR-008-stdlib-zoneinfo-over-pytz.md) | Zone data from stdlib `zoneinfo`, not a bundled `pytz` | Proposed | 2026-07-27 |
|
{nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/getting-started/installation.md
RENAMED
|
@@ -34,6 +34,9 @@ pip install nui-python-shared-utils[database]
|
|
|
34
34
|
# JWT authentication only
|
|
35
35
|
pip install nui-python-shared-utils[jwt]
|
|
36
36
|
|
|
37
|
+
# Zone data, for minimal images with no system IANA database (Alpine, distroless)
|
|
38
|
+
pip install nui-python-shared-utils[timezone]
|
|
39
|
+
|
|
37
40
|
# All integrations
|
|
38
41
|
pip install nui-python-shared-utils[all]
|
|
39
42
|
|
|
@@ -59,15 +62,23 @@ pip install -e .[dev]
|
|
|
59
62
|
### Core Dependencies (Always Installed)
|
|
60
63
|
|
|
61
64
|
- `boto3` - AWS SDK for Python
|
|
62
|
-
- `pytz` - Timezone handling
|
|
63
65
|
- `click` - CLI framework
|
|
64
66
|
- `pyyaml` - YAML configuration parsing
|
|
67
|
+
- `tzdata` - Zone data, **Windows only** (installed via an environment marker)
|
|
68
|
+
|
|
69
|
+
Timezone conversion uses the standard library's `zoneinfo`, which reads the
|
|
70
|
+
system IANA database. Linux, macOS, and the AWS Lambda runtimes all ship one,
|
|
71
|
+
so nothing extra is installed there.
|
|
65
72
|
|
|
66
73
|
### Optional Dependencies by Extra
|
|
67
74
|
|
|
68
75
|
#### elasticsearch
|
|
69
76
|
|
|
70
|
-
- `elasticsearch>=7.17.0,<
|
|
77
|
+
- `elasticsearch>=7.17.0,<10.0.0` - Elasticsearch client
|
|
78
|
+
|
|
79
|
+
Client majors 7.17, 8.x and 9.x are all supported and tested; pick whichever
|
|
80
|
+
your cluster needs. The `<10` ceiling is the untested next major, not a known
|
|
81
|
+
incompatibility. See [ADR-007](../decisions/ADR-007-elasticsearch-client-version-range.md).
|
|
71
82
|
|
|
72
83
|
#### database
|
|
73
84
|
|
|
@@ -82,6 +93,18 @@ pip install -e .[dev]
|
|
|
82
93
|
|
|
83
94
|
- `rsa>=4.9` - Pure Python RSA implementation (~100KB, no C extensions)
|
|
84
95
|
|
|
96
|
+
#### timezone
|
|
97
|
+
|
|
98
|
+
- `tzdata>=2023.3` - Zone data for images that carry no system IANA database
|
|
99
|
+
|
|
100
|
+
Only needed on a minimal image (Alpine, distroless) where `/usr/share/zoneinfo`
|
|
101
|
+
is absent, which no environment marker can detect. Without it, importing
|
|
102
|
+
`nui_shared_utils.timezone` raises `ZoneInfoNotFoundError`. Windows already
|
|
103
|
+
gets `tzdata` from the core dependencies.
|
|
104
|
+
|
|
105
|
+
`[all]` covers the integrations and does **not** include this; on a minimal
|
|
106
|
+
image ask for it by name (`[all,timezone]`).
|
|
107
|
+
|
|
85
108
|
#### dev
|
|
86
109
|
|
|
87
110
|
- `pytest>=7.0.0` - Testing framework
|
{nui_python_shared_utils-2.0 → nui_python_shared_utils-2.2}/docs/guides/elasticsearch-integration.md
RENAMED
|
@@ -35,6 +35,23 @@ Install with the elasticsearch extra:
|
|
|
35
35
|
pip install nui-python-shared-utils[elasticsearch]
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### Supported client versions
|
|
39
|
+
|
|
40
|
+
The extra allows `elasticsearch>=7.17.0,<10.0.0`. Client majors 7.17, 8.x and 9.x are all
|
|
41
|
+
supported and covered by CI, so the extra installs alongside whichever client your cluster
|
|
42
|
+
needs. `ElasticsearchClient` sends one request form that every one of those majors accepts;
|
|
43
|
+
`size` travels inside the request body rather than as a separate argument, and your body dict
|
|
44
|
+
is copied, never mutated, so it is safe to reuse across calls.
|
|
45
|
+
|
|
46
|
+
Pin the client yourself if you need a specific major:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install nui-python-shared-utils[elasticsearch] "elasticsearch<9"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
See [ADR-007](../decisions/ADR-007-elasticsearch-client-version-range.md) for the reasoning
|
|
53
|
+
and the `<10` ceiling.
|
|
54
|
+
|
|
38
55
|
## Configuration
|
|
39
56
|
|
|
40
57
|
### AWS Secrets Manager Setup
|