nui-python-shared-utils 2.4__tar.gz → 2.4.1__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.
Files changed (104) hide show
  1. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/.github/workflows/ci.yml +1 -1
  2. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/.github/workflows/publish.yml +1 -1
  3. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/.github/workflows/test.yml +1 -1
  4. nui_python_shared_utils-2.4.1/.markdownlint-base.yaml +66 -0
  5. nui_python_shared_utils-2.4.1/.markdownlint-cli2.yaml +17 -0
  6. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/CHANGELOG.md +15 -1
  7. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/CONTRIBUTING.md +1 -1
  8. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/PKG-INFO +3 -4
  9. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/README.md +1 -1
  10. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-008-stdlib-zoneinfo-over-pytz.md +4 -3
  11. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/getting-started/installation.md +3 -3
  12. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/shared-types.md +15 -0
  13. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/mypy.ini +1 -1
  14. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_python_shared_utils.egg-info/SOURCES.txt +1 -0
  15. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/logging/binding.py +0 -1
  16. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/timezone.py +4 -4
  17. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/pyproject.toml +3 -4
  18. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/redirect/pyproject.toml +1 -1
  19. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/setup.py +1 -2
  20. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_slack_client.py +66 -51
  21. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_timezone.py +37 -0
  22. nui_python_shared_utils-2.4/.markdownlint-cli2.yaml +0 -47
  23. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/.editorconfig +0 -0
  24. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/AGENTS.md +0 -0
  25. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/CLAUDE.md +0 -0
  26. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/LICENSE +0 -0
  27. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/MANIFEST.in +0 -0
  28. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/README.md +0 -0
  29. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +0 -0
  30. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-002-keep-shared-utilities-generic.md +0 -0
  31. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-003-lazy-load-integrations.md +0 -0
  32. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-004-credential-resolution-precedence.md +0 -0
  33. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-005-explicit-secrets-manager-region.md +0 -0
  34. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-006-snowflake-sql-api-client.md +0 -0
  35. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/ADR-007-elasticsearch-client-version-range.md +0 -0
  36. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/decisions/README.md +0 -0
  37. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/development/testing.md +0 -0
  38. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/getting-started/configuration.md +0 -0
  39. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/getting-started/quickstart.md +0 -0
  40. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/cli-tools.md +0 -0
  41. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/elasticsearch-integration.md +0 -0
  42. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/jwt-authentication.md +0 -0
  43. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/lambda-utilities.md +0 -0
  44. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/llm-integration.md +0 -0
  45. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/log-processing.md +0 -0
  46. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/powertools-integration.md +0 -0
  47. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/slack-integration.md +0 -0
  48. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/snowflake-integration.md +0 -0
  49. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/docs/guides/structured-logging-contract.md +0 -0
  50. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/__init__.py +0 -0
  51. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/base_client.py +0 -0
  52. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/cli.py +0 -0
  53. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/cloudwatch_metrics.py +0 -0
  54. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/config.py +0 -0
  55. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/db_client.py +0 -0
  56. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/error_handler.py +0 -0
  57. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/es_client.py +0 -0
  58. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/es_query_builder.py +0 -0
  59. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/jwt_auth.py +0 -0
  60. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/lambda_helpers.py +0 -0
  61. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/llm.py +0 -0
  62. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/log_processors.py +0 -0
  63. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/logging/__init__.py +0 -0
  64. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/logging/fields.py +0 -0
  65. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/logging/redaction.py +0 -0
  66. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/powertools_helpers.py +0 -0
  67. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/secrets_helper.py +0 -0
  68. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/slack_client.py +0 -0
  69. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/slack_formatter.py +0 -0
  70. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/slack_setup/__init__.py +0 -0
  71. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
  72. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
  73. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
  74. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/snowflake_client.py +0 -0
  75. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/nui_shared_utils/utils.py +0 -0
  76. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/pytest.ini +0 -0
  77. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/redirect/README.md +0 -0
  78. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/redirect/setup.py +0 -0
  79. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/requirements-test.txt +0 -0
  80. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/scripts/bench_imports.py +0 -0
  81. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/setup.cfg +0 -0
  82. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/__init__.py +0 -0
  83. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_aws_utils.py +0 -0
  84. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_base_client.py +0 -0
  85. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_cloudwatch_metrics.py +0 -0
  86. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_config.py +0 -0
  87. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_db_client.py +0 -0
  88. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_error_handler.py +0 -0
  89. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_es_client.py +0 -0
  90. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_es_client_compat.py +0 -0
  91. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_es_query_builder.py +0 -0
  92. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_jwt_auth.py +0 -0
  93. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_lambda_helpers.py +0 -0
  94. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_lazy_imports.py +0 -0
  95. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_llm.py +0 -0
  96. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_log_processors.py +0 -0
  97. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_logging_binding.py +0 -0
  98. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_logging_fields.py +0 -0
  99. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_powertools_helpers.py +0 -0
  100. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_redaction.py +0 -0
  101. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_secrets_helper.py +0 -0
  102. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_slack_formatter.py +0 -0
  103. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_snowflake_client.py +0 -0
  104. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.1}/tests/test_utils.py +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  strategy:
17
17
  matrix:
18
- python-version: ['3.9', '3.10', '3.11', '3.12']
18
+ python-version: ['3.10', '3.11', '3.12']
19
19
 
20
20
  steps:
21
21
  - uses: actions/checkout@v5
@@ -12,7 +12,7 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- python-version: ['3.9', '3.10', '3.11', '3.12']
15
+ python-version: ['3.10', '3.11', '3.12']
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v5
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- python-version: ['3.9', '3.10', '3.11']
14
+ python-version: ['3.10', '3.11']
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v5
@@ -0,0 +1,66 @@
1
+ # === managed by sync-markdownlint.sh — DO NOT EDIT ===
2
+ # Source of truth: shared tooling config/markdownlint-base.yaml
3
+ # Change rules in the source, then run: sync-markdownlint.sh --update
4
+ # stamp: e9f1a3abeab6
5
+ # === end managed header (sync-markdownlint.sh) ===
6
+ # Shared markdownlint ruleset (FLAT format).
7
+ # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
8
+ #
9
+ # SOURCE OF TRUTH. Repos adopt this by vendoring a stamped copy as
10
+ # `.markdownlint-base.yaml` (via sync-markdownlint.sh) and pointing their own
11
+ # `.markdownlint-cli2.yaml` at it with `config: { extends: .markdownlint-base.yaml }`,
12
+ # then adding repo-specific `globs`/`ignores` (and any rare repo-only rule tweaks).
13
+ #
14
+ # This file is FLAT (rules at top level, no `config:` wrapper) because that is
15
+ # the format markdownlint's `extends` requires. Keep it rules-only: file
16
+ # selection (globs) and exclusions (ignores) are per-repo.
17
+
18
+ # Enable all rules by default; disables/customisations follow.
19
+ default: true
20
+
21
+ # MD013: Line length - off. Code blocks, tables, and URLs routinely exceed any
22
+ # sane width; wrapping prose to a column is not a standard we enforce.
23
+ MD013: false
24
+
25
+ # MD033: Inline HTML - allowed. Needed for details/summary, anchors, and the
26
+ # HTML comment markers some docs use (companions metadata, stay markers).
27
+ MD033: false
28
+
29
+ # MD034: Bare URLs - off. Bare URLs in reference docs are intentional.
30
+ MD034: false
31
+
32
+ # MD041: First line in file should be a top-level heading - off. Many docs open
33
+ # with YAML frontmatter or a metadata block, not an H1.
34
+ MD041: false
35
+
36
+ # MD024: Multiple headings with the same content - allowed when not siblings
37
+ # (e.g. a repeated "Example" under different sections).
38
+ MD024:
39
+ siblings_only: true
40
+
41
+ # MD046: Code block style - require fenced (not indented) blocks.
42
+ MD046:
43
+ style: "fenced"
44
+
45
+ # MD026: Trailing punctuation in headings - off. "Note:" / "Warning!" are fine.
46
+ MD026: false
47
+
48
+ # MD029: Ordered list item prefix - off. Docs mix "1. 1. 1." and "1. 2. 3." and
49
+ # both render correctly; not worth churning.
50
+ MD029: false
51
+
52
+ # MD036: Emphasis used instead of a heading - off. Bold-as-label
53
+ # ("**Root cause:**", date stamps) is an accepted convention here.
54
+ MD036: false
55
+
56
+ # MD040: Fenced code language - off. Not every code block has (or needs) a
57
+ # language tag; forcing one produces noise on output samples and snippets.
58
+ MD040: false
59
+
60
+ # MD051: Link fragments should be valid - off. Cross-doc anchor validation is
61
+ # unreliable across the workspace's mixed heading styles and generated pages.
62
+ MD051: false
63
+
64
+ # MD060: Table column style - off. Minor pipe-alignment preference; the corpus
65
+ # has always mixed styles.
66
+ MD060: false
@@ -0,0 +1,17 @@
1
+ # Markdownlint configuration for nui-python-shared-utils
2
+ # Shared rules are vendored by sync-markdownlint.sh.
3
+
4
+ config:
5
+ extends: .markdownlint-base.yaml
6
+
7
+ # File patterns to check
8
+ globs:
9
+ - "**/*.md"
10
+
11
+ # Exclusions
12
+ ignores:
13
+ - "node_modules/**"
14
+ - "vendor/**"
15
+ - ".git/**"
16
+ - "venv/**"
17
+ - ".venv/**"
@@ -8,6 +8,19 @@ that contained them.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Changed
12
+
13
+ - **BREAKING**: require Python 3.10 or newer in both the main package and the legacy
14
+ redirect package. Python 3.9 is no longer supported or included in CI test matrices.
15
+
16
+ ### Fixed
17
+
18
+ - Use canonical IANA names `America/New_York` and `America/Los_Angeles` for US timezone
19
+ constants and option labels, so importing timezone utilities and Slack formatting works
20
+ on systems without legacy timezone links.
21
+
22
+ ## [2.4] - 2026-10-20
23
+
11
24
  ### Added
12
25
 
13
26
  - Add an opt-in `reraise_exceptions` parameter to `powertools_handler`. When enabled,
@@ -230,7 +243,8 @@ that contained them.
230
243
  - Basic project structure
231
244
  - Python package configuration
232
245
 
233
- [Unreleased]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.3...HEAD
246
+ [Unreleased]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.4...HEAD
247
+ [2.4]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.3...2.4
234
248
  [2.3]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.2...2.3
235
249
  [2.2]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.1...2.2
236
250
  [2.1]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.0...2.1
@@ -6,7 +6,7 @@ We welcome contributions to the NUI Python Shared Utilities project! This docume
6
6
 
7
7
  ### Prerequisites
8
8
 
9
- - Python 3.9 or higher
9
+ - Python 3.10 or higher
10
10
  - Git
11
11
  - Virtual environment tool (venv, virtualenv, etc.)
12
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nui-python-shared-utils
3
- Version: 2.4
3
+ Version: 2.4.1
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
@@ -36,7 +36,6 @@ Classifier: Intended Audience :: Developers
36
36
  Classifier: License :: OSI Approved :: MIT License
37
37
  Classifier: Operating System :: OS Independent
38
38
  Classifier: Programming Language :: Python :: 3
39
- Classifier: Programming Language :: Python :: 3.9
40
39
  Classifier: Programming Language :: Python :: 3.10
41
40
  Classifier: Programming Language :: Python :: 3.11
42
41
  Classifier: Programming Language :: Python :: 3.12
@@ -45,7 +44,7 @@ Classifier: Topic :: System :: Monitoring
45
44
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
46
45
  Classifier: Environment :: Console
47
46
  Classifier: Framework :: AWS CDK
48
- Requires-Python: >=3.9
47
+ Requires-Python: >=3.10
49
48
  Description-Content-Type: text/markdown
50
49
  License-File: LICENSE
51
50
  Requires-Dist: boto3>=1.20.0
@@ -102,7 +101,7 @@ Dynamic: requires-python
102
101
  # NUI Python Shared Utilities
103
102
 
104
103
  [![PyPI version](https://badge.fury.io/py/nui-python-shared-utils.svg)](https://badge.fury.io/py/nui-python-shared-utils)
105
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
104
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
106
105
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
107
106
 
108
107
  Production-ready shared Python utilities for AWS Lambda functions, CLI tools, and agents. Provides standardized integrations for Slack, Elasticsearch, database, and monitoring. Built and battle-tested on the NUI platform with sensible defaults configurable for any environment.
@@ -1,7 +1,7 @@
1
1
  # NUI Python Shared Utilities
2
2
 
3
3
  [![PyPI version](https://badge.fury.io/py/nui-python-shared-utils.svg)](https://badge.fury.io/py/nui-python-shared-utils)
4
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  Production-ready shared Python utilities for AWS Lambda functions, CLI tools, and agents. Provides standardized integrations for Slack, Elasticsearch, database, and monitoring. Built and battle-tested on the NUI platform with sensible defaults configurable for any environment.
@@ -11,7 +11,7 @@ that wanted only the logging contract or the metrics helper still shipped it.
11
11
 
12
12
  The cost is not the code. `pytz` bundles its own copy of the Olson/IANA database (0.86 MB of a
13
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
14
+ every interpreter this package supports: `requires-python = ">=3.10"`, and `zoneinfo` has been in
15
15
  the standard library since 3.9, reading the system database that Linux, macOS, and the AWS Lambda
16
16
  runtimes all ship.
17
17
 
@@ -82,8 +82,9 @@ a database to everyone:
82
82
 
83
83
  ## Load-bearing premises (supersede triggers)
84
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.
85
+ - **All supported interpreters include stdlib `zoneinfo`.** The package requires Python 3.10
86
+ or higher. Supporting an interpreter older than 3.9 would need `backports.zoneinfo`, at
87
+ which point the calculus changes.
87
88
  - **Target runtimes ship a system IANA database.** True of Linux, macOS, and the AWS Lambda
88
89
  runtimes today. If the primary deployment target became one that does not, `tzdata` belongs in
89
90
  the base dependencies unconditionally and this ADR's marker split is obsolete.
@@ -4,7 +4,7 @@ This guide covers installation, dependency management, and environment setup for
4
4
 
5
5
  ## Requirements
6
6
 
7
- - Python 3.9 or higher
7
+ - Python 3.10 or higher
8
8
  - AWS credentials configured for Secrets Manager access
9
9
  - Optional: AWS CLI for credential management
10
10
 
@@ -139,7 +139,7 @@ pip install nui-python-shared-utils[all]
139
139
 
140
140
  ```bash
141
141
  # Create environment
142
- conda create -n nui-utils python=3.9
142
+ conda create -n nui-utils python=3.10
143
143
 
144
144
  # Activate environment
145
145
  conda activate nui-utils
@@ -209,7 +209,7 @@ aws lambda publish-layer-version \
209
209
  --layer-name nui-python-shared-utils \
210
210
  --description "NUI Lambda Shared Utilities" \
211
211
  --zip-file fileb://nui-utils-layer.zip \
212
- --compatible-runtimes python3.9 python3.10 python3.11
212
+ --compatible-runtimes python3.10 python3.11
213
213
  ```
214
214
 
215
215
  ### Using in Lambda Function
@@ -103,6 +103,7 @@ client.get_connection_info() -> Dict
103
103
  ```
104
104
 
105
105
  **Connection Pool Entry Structure:**
106
+
106
107
  ```python
107
108
  {
108
109
  "connection": pymysql.Connection,
@@ -111,6 +112,7 @@ client.get_connection_info() -> Dict
111
112
  ```
112
113
 
113
114
  **Connection Info Response:**
115
+
114
116
  ```python
115
117
  {
116
118
  "host": str,
@@ -169,6 +171,7 @@ client.get_indices_info(pattern="*") -> List[Dict]
169
171
  ```
170
172
 
171
173
  **Service Stats Response:**
174
+
172
175
  ```python
173
176
  {
174
177
  "service": str,
@@ -183,6 +186,7 @@ client.get_indices_info(pattern="*") -> List[Dict]
183
186
  ```
184
187
 
185
188
  **Cluster Info Response:**
189
+
186
190
  ```python
187
191
  {
188
192
  "version": str,
@@ -222,6 +226,7 @@ client.get_bot_info() -> Dict
222
226
  ```
223
227
 
224
228
  **Lambda Context Structure:**
229
+
225
230
  ```python
226
231
  {
227
232
  "function_name": str,
@@ -240,6 +245,7 @@ client.get_bot_info() -> Dict
240
245
  ```
241
246
 
242
247
  **Bot Info Response:**
248
+
243
249
  ```python
244
250
  {
245
251
  "bot_id": str,
@@ -274,6 +280,7 @@ result = matcher.categorize_error(exception)
274
280
  ```
275
281
 
276
282
  **Categorized Error Response:**
283
+
277
284
  ```python
278
285
  {
279
286
  "error": str,
@@ -286,6 +293,7 @@ result = matcher.categorize_error(exception)
286
293
  ```
287
294
 
288
295
  **Built-in Error Categories:**
296
+
289
297
  - `data_format` - JSON parsing errors
290
298
  - `authentication` - Auth failures
291
299
  - `database` - DB timeouts
@@ -310,6 +318,7 @@ summary = aggregator.get_summary()
310
318
  ```
311
319
 
312
320
  **Error Summary Response:**
321
+
313
322
  ```python
314
323
  {
315
324
  "total_errors": int,
@@ -360,6 +369,7 @@ publisher.flush() -> bool
360
369
  ```
361
370
 
362
371
  **Metric Data Structure (internal buffer):**
372
+
363
373
  ```python
364
374
  {
365
375
  "MetricName": str,
@@ -429,6 +439,7 @@ with TimedMetric(publisher, "DatabaseQuery", unit="Milliseconds", dimensions=Non
429
439
  ### Credential Structures
430
440
 
431
441
  **Database Credentials:**
442
+
432
443
  ```python
433
444
  {
434
445
  "host": str,
@@ -440,6 +451,7 @@ with TimedMetric(publisher, "DatabaseQuery", unit="Milliseconds", dimensions=Non
440
451
  ```
441
452
 
442
453
  **Elasticsearch Credentials:**
454
+
443
455
  ```python
444
456
  {
445
457
  "host": str, # With port (e.g., "elastic:9200")
@@ -449,6 +461,7 @@ with TimedMetric(publisher, "DatabaseQuery", unit="Milliseconds", dimensions=Non
449
461
  ```
450
462
 
451
463
  **Slack Credentials:**
464
+
452
465
  ```python
453
466
  {
454
467
  "bot_token": str,
@@ -492,6 +505,7 @@ stats = get_pool_stats()
492
505
  ```
493
506
 
494
507
  **Pool Stats Response:**
508
+
495
509
  ```python
496
510
  {
497
511
  "total_pools": int,
@@ -514,6 +528,7 @@ response = handle_lambda_error(error, context)
514
528
  ```
515
529
 
516
530
  **Lambda Error Response:**
531
+
517
532
  ```python
518
533
  {
519
534
  "statusCode": 500,
@@ -1,5 +1,5 @@
1
1
  [mypy]
2
- python_version = 3.9
2
+ python_version = 3.10
3
3
  warn_return_any = False
4
4
  warn_unused_configs = True
5
5
  ignore_missing_imports = True
@@ -1,4 +1,5 @@
1
1
  .editorconfig
2
+ .markdownlint-base.yaml
2
3
  .markdownlint-cli2.yaml
3
4
  AGENTS.md
4
5
  CHANGELOG.md
@@ -306,7 +306,6 @@ def log_exception(binder: Any, exc: BaseException, message: str) -> None:
306
306
  extra={
307
307
  F.ERROR_TYPE: type(exc).__name__,
308
308
  F.ERROR_MESSAGE: str(exc),
309
- # 3-arg form for Python 3.9 compatibility (single-arg added in 3.10).
310
309
  F.ERROR_STACK: "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
311
310
  },
312
311
  )
@@ -13,8 +13,8 @@ from zoneinfo import ZoneInfo
13
13
  # Supported timezones
14
14
  NZ_TZ = ZoneInfo("Pacific/Auckland") # Primary timezone
15
15
  AUS_TZ = ZoneInfo("Australia/Sydney")
16
- US_EAST_TZ = ZoneInfo("US/Eastern")
17
- US_WEST_TZ = ZoneInfo("US/Pacific")
16
+ US_EAST_TZ = ZoneInfo("America/New_York")
17
+ US_WEST_TZ = ZoneInfo("America/Los_Angeles")
18
18
  EUR_TZ = ZoneInfo("Europe/London")
19
19
 
20
20
 
@@ -115,7 +115,7 @@ def get_timezone_options() -> List[Tuple[str, str]]:
115
115
  return [
116
116
  ("nz", "New Zealand (Pacific/Auckland)"),
117
117
  ("aus", "Australia (Australia/Sydney)"),
118
- ("us_east", "US Eastern (US/Eastern)"),
119
- ("us_west", "US Pacific (US/Pacific)"),
118
+ ("us_east", "US Eastern (America/New_York)"),
119
+ ("us_west", "US Pacific (America/Los_Angeles)"),
120
120
  ("eur", "Europe (Europe/London)"),
121
121
  ]
@@ -11,14 +11,13 @@ authors = [
11
11
  description = "Shared Python utilities for AWS Lambda, CLI tools, and agents with Slack, Elasticsearch, and monitoring integrations"
12
12
  readme = "README.md"
13
13
  license = {file = "LICENSE"}
14
- requires-python = ">=3.9"
14
+ requires-python = ">=3.10"
15
15
  classifiers = [
16
16
  "Development Status :: 4 - Beta",
17
17
  "Intended Audience :: Developers",
18
18
  "License :: OSI Approved :: MIT License",
19
19
  "Operating System :: OS Independent",
20
20
  "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3.9",
22
21
  "Programming Language :: Python :: 3.10",
23
22
  "Programming Language :: Python :: 3.11",
24
23
  "Programming Language :: Python :: 3.12",
@@ -110,7 +109,7 @@ include = ["nui_shared_utils*"]
110
109
 
111
110
  [tool.black]
112
111
  line-length = 120
113
- target-version = ['py39']
112
+ target-version = ['py310']
114
113
  include = '\.pyi?$'
115
114
  extend-exclude = '''
116
115
  /(
@@ -127,7 +126,7 @@ extend-exclude = '''
127
126
  '''
128
127
 
129
128
  [tool.mypy]
130
- python_version = "3.9"
129
+ python_version = "3.10"
131
130
  warn_return_any = false
132
131
  warn_unused_configs = true
133
132
  ignore_missing_imports = true
@@ -10,7 +10,7 @@ authors = [
10
10
  ]
11
11
  description = "Redirect package: use nui-python-shared-utils instead"
12
12
  readme = "README.md"
13
- requires-python = ">=3.9"
13
+ requires-python = ">=3.10"
14
14
  classifiers = [
15
15
  "Development Status :: 7 - Inactive",
16
16
  "Intended Audience :: Developers",
@@ -28,7 +28,6 @@ setup(
28
28
  "License :: OSI Approved :: MIT License",
29
29
  "Operating System :: OS Independent",
30
30
  "Programming Language :: Python :: 3",
31
- "Programming Language :: Python :: 3.9",
32
31
  "Programming Language :: Python :: 3.10",
33
32
  "Programming Language :: Python :: 3.11",
34
33
  "Programming Language :: Python :: 3.12",
@@ -103,6 +102,6 @@ setup(
103
102
  "anthropic[bedrock]>=0.45.0,<1.0.0",
104
103
  ],
105
104
  },
106
- python_requires=">=3.9",
105
+ python_requires=">=3.10",
107
106
  keywords="aws lambda utilities slack elasticsearch monitoring serverless python shared",
108
107
  )
@@ -51,12 +51,12 @@ class TestSlackClient:
51
51
  def test_init_with_default_secret_name(self, mock_webclient, mock_get_secret):
52
52
  """Test initialization with default secret name."""
53
53
  mock_get_secret.return_value = {"bot_token": "xoxb-default-token"}
54
-
54
+
55
55
  with patch("nui_shared_utils.base_client.resolve_config_value") as mock_resolve:
56
56
  mock_resolve.return_value = "slack-credentials"
57
-
57
+
58
58
  client = SlackClient()
59
-
59
+
60
60
  mock_get_secret.assert_called_once_with("slack-credentials")
61
61
  mock_webclient.assert_called_once_with(token="xoxb-default-token")
62
62
 
@@ -77,7 +77,9 @@ class TestSendMessage:
77
77
  result = slack.send_message("C123", "Test message", include_lambda_header=False)
78
78
 
79
79
  assert result is True
80
- mock_client.chat_postMessage.assert_called_once_with(channel="C123", text="Test message", blocks=None, attachments=None)
80
+ mock_client.chat_postMessage.assert_called_once_with(
81
+ channel="C123", text="Test message", blocks=None, attachments=None
82
+ )
81
83
 
82
84
  @patch("nui_shared_utils.base_client.get_secret")
83
85
  @patch("nui_shared_utils.slack_client.WebClient")
@@ -94,7 +96,9 @@ class TestSendMessage:
94
96
  result = slack.send_message("C123", "Fallback text", blocks=blocks, include_lambda_header=False)
95
97
 
96
98
  assert result is True
97
- mock_client.chat_postMessage.assert_called_once_with(channel="C123", text="Fallback text", blocks=blocks, attachments=None)
99
+ mock_client.chat_postMessage.assert_called_once_with(
100
+ channel="C123", text="Fallback text", blocks=blocks, attachments=None
101
+ )
98
102
 
99
103
  @patch("nui_shared_utils.base_client.get_secret")
100
104
  @patch("nui_shared_utils.slack_client.WebClient")
@@ -954,7 +958,7 @@ class TestLambdaContextHeader:
954
958
  "AWS_LAMBDA_LOG_STREAM_NAME": "2023/11/15/[$LATEST]abc123",
955
959
  "AWS_REGION": "eu-west-1",
956
960
  "STAGE": "prod",
957
- "AWS_EXECUTION_ENV": "AWS_Lambda_python3.9",
961
+ "AWS_EXECUTION_ENV": "AWS_Lambda_python3.10",
958
962
  },
959
963
  )
960
964
  def test_lambda_header_connect_production(self, mock_webclient, mock_get_secret, mock_boto3):
@@ -996,15 +1000,18 @@ class TestLambdaContextHeader:
996
1000
  header_text = header_block["elements"][0]["text"]
997
1001
 
998
1002
  # Use helper to check all expected parts
999
- self.assert_header_format(header_text, {
1000
- "robot emoji": "🤖",
1001
- "function name": "test-function",
1002
- "account name": "Production",
1003
- "account ID": "(123456789012)",
1004
- "region": "eu-west-1",
1005
- "log emoji": "📋",
1006
- "log group": "`/aws/lambda/test-function`"
1007
- })
1003
+ self.assert_header_format(
1004
+ header_text,
1005
+ {
1006
+ "robot emoji": "🤖",
1007
+ "function name": "test-function",
1008
+ "account name": "Production",
1009
+ "account ID": "(123456789012)",
1010
+ "region": "eu-west-1",
1011
+ "log emoji": "📋",
1012
+ "log group": "`/aws/lambda/test-function`",
1013
+ },
1014
+ )
1008
1015
 
1009
1016
  @patch("nui_shared_utils.slack_client.create_aws_client")
1010
1017
  @patch("nui_shared_utils.base_client.get_secret")
@@ -1055,14 +1062,17 @@ class TestLambdaContextHeader:
1055
1062
  header_text = header_block["elements"][0]["text"]
1056
1063
 
1057
1064
  # Use helper to check all expected parts
1058
- self.assert_header_format(header_text, {
1059
- "robot emoji": "🤖",
1060
- "function name": "nui-service",
1061
- "account name": "Development",
1062
- "account ID": "(234567890123)",
1063
- "region": "ap-southeast-2",
1064
- "log emoji": "📋"
1065
- })
1065
+ self.assert_header_format(
1066
+ header_text,
1067
+ {
1068
+ "robot emoji": "🤖",
1069
+ "function name": "nui-service",
1070
+ "account name": "Development",
1071
+ "account ID": "(234567890123)",
1072
+ "region": "ap-southeast-2",
1073
+ "log emoji": "📋",
1074
+ },
1075
+ )
1066
1076
 
1067
1077
  @patch("nui_shared_utils.slack_client.create_aws_client")
1068
1078
  @patch("nui_shared_utils.base_client.get_secret")
@@ -1096,14 +1106,14 @@ class TestLambdaContextHeader:
1096
1106
 
1097
1107
  # Create client with custom service name
1098
1108
  slack = SlackClient(
1099
- secret_name="test-secret",
1100
- service_name="my-service",
1101
- account_names={"123456789012": "Production"}
1109
+ secret_name="test-secret", service_name="my-service", account_names={"123456789012": "Production"}
1102
1110
  )
1103
1111
 
1104
1112
  # Simulate Lambda handler providing context with ARN
1105
1113
  mock_context = Mock()
1106
- mock_context.invoked_function_arn = "arn:aws:lambda:eu-west-1:123456789012:function:connect-prod-long-service-name-12345"
1114
+ mock_context.invoked_function_arn = (
1115
+ "arn:aws:lambda:eu-west-1:123456789012:function:connect-prod-long-service-name-12345"
1116
+ )
1107
1117
  slack.set_handler_context(mock_context)
1108
1118
 
1109
1119
  result = slack.send_message("C123", "Test message")
@@ -1122,14 +1132,17 @@ class TestLambdaContextHeader:
1122
1132
  assert "connect-prod-long-service-name-12345" not in header_text
1123
1133
 
1124
1134
  # Other header elements should still be present
1125
- self.assert_header_format(header_text, {
1126
- "robot emoji": "🤖",
1127
- "function name": "my-service", # Custom name
1128
- "account name": "Production",
1129
- "account ID": "(123456789012)",
1130
- "region": "eu-west-1",
1131
- "log emoji": "📋"
1132
- })
1135
+ self.assert_header_format(
1136
+ header_text,
1137
+ {
1138
+ "robot emoji": "🤖",
1139
+ "function name": "my-service", # Custom name
1140
+ "account name": "Production",
1141
+ "account ID": "(123456789012)",
1142
+ "region": "eu-west-1",
1143
+ "log emoji": "📋",
1144
+ },
1145
+ )
1133
1146
 
1134
1147
  @patch("nui_shared_utils.slack_client.create_aws_client")
1135
1148
  @patch("nui_shared_utils.base_client.get_secret")
@@ -1153,19 +1166,14 @@ class TestLambdaContextHeader:
1153
1166
  # Mock Lambda client
1154
1167
  mock_lambda = Mock()
1155
1168
  mock_boto3.return_value = mock_lambda
1156
- mock_lambda.get_function.return_value = {
1157
- "Configuration": {"FunctionName": "test-function"}
1158
- }
1169
+ mock_lambda.get_function.return_value = {"Configuration": {"FunctionName": "test-function"}}
1159
1170
 
1160
1171
  # Mock config file check
1161
1172
  with patch("os.path.exists") as mock_exists:
1162
1173
  mock_exists.return_value = False
1163
1174
 
1164
1175
  # Create client WITHOUT custom service name
1165
- slack = SlackClient(
1166
- secret_name="test-secret",
1167
- account_names={"123456789012": "Production"}
1168
- )
1176
+ slack = SlackClient(secret_name="test-secret", account_names={"123456789012": "Production"})
1169
1177
 
1170
1178
  # Simulate Lambda handler providing context with ARN
1171
1179
  mock_context = Mock()
@@ -1187,14 +1195,17 @@ class TestLambdaContextHeader:
1187
1195
  assert "test-function" in header_text
1188
1196
 
1189
1197
  # All header elements should be present
1190
- self.assert_header_format(header_text, {
1191
- "robot emoji": "🤖",
1192
- "function name": "test-function", # Original function name
1193
- "account name": "Production",
1194
- "account ID": "(123456789012)",
1195
- "region": "eu-west-1",
1196
- "log emoji": "📋"
1197
- })
1198
+ self.assert_header_format(
1199
+ header_text,
1200
+ {
1201
+ "robot emoji": "🤖",
1202
+ "function name": "test-function", # Original function name
1203
+ "account name": "Production",
1204
+ "account ID": "(123456789012)",
1205
+ "region": "eu-west-1",
1206
+ "log emoji": "📋",
1207
+ },
1208
+ )
1198
1209
 
1199
1210
  @patch("nui_shared_utils.slack_client.create_aws_client")
1200
1211
  @patch("nui_shared_utils.base_client.get_secret")
@@ -1632,7 +1643,11 @@ class TestSlackCredentialResolution:
1632
1643
 
1633
1644
  @patch("nui_shared_utils.base_client.get_secret")
1634
1645
  @patch("nui_shared_utils.slack_client.WebClient")
1635
- @patch.dict("os.environ", {"SLACK_BOT_TOKEN": "xoxb-env-token", "SLACK_WEBHOOK_URL": "https://hooks.slack.com/xxx"}, clear=False)
1646
+ @patch.dict(
1647
+ "os.environ",
1648
+ {"SLACK_BOT_TOKEN": "xoxb-env-token", "SLACK_WEBHOOK_URL": "https://hooks.slack.com/xxx"},
1649
+ clear=False,
1650
+ )
1636
1651
  def test_env_var_includes_optional_webhook(self, _mock_webclient, _mock_get_secret):
1637
1652
  """SLACK_WEBHOOK_URL is included when present."""
1638
1653
  client = SlackClient()
@@ -8,9 +8,11 @@ import sys
8
8
  import textwrap
9
9
  from datetime import datetime, timedelta, timezone
10
10
  from unittest.mock import patch
11
+ from zoneinfo import ZoneInfo
11
12
 
12
13
  import pytest
13
14
 
15
+ import nui_shared_utils.timezone as timezone_utils
14
16
  from nui_shared_utils.timezone import nz_time, format_nz_time, NZ_TZ
15
17
 
16
18
 
@@ -259,6 +261,41 @@ class TestAmbiguousWallTimes:
259
261
  class TestTimezoneConstants:
260
262
  """Tests for timezone constants."""
261
263
 
264
+ def test_all_zone_keys_are_canonical(self):
265
+ # Canonical names for the supported regions, from IANA's zone1970.tab:
266
+ # https://data.iana.org/time-zones/tzdb/zone1970.tab
267
+ # Keep this list independent of the host database: available_timezones()
268
+ # also includes optional backward links on many systems.
269
+ canonical_keys = {
270
+ "Pacific/Auckland",
271
+ "Australia/Sydney",
272
+ "America/New_York",
273
+ "America/Los_Angeles",
274
+ "Europe/London",
275
+ }
276
+ zone_keys = {value.key for value in vars(timezone_utils).values() if isinstance(value, ZoneInfo)}
277
+ assert zone_keys, "No timezone constants were checked"
278
+ assert zone_keys <= canonical_keys, f"Non-canonical or unreviewed zone keys: {zone_keys - canonical_keys}"
279
+
280
+ def test_option_labels_use_canonical_zone_keys(self):
281
+ labels = dict(timezone_utils.get_timezone_options())
282
+ assert labels["us_east"] == "US Eastern (America/New_York)"
283
+ assert labels["us_west"] == "US Pacific (America/Los_Angeles)"
284
+
285
+ @pytest.mark.parametrize(
286
+ "zone, month, offset, abbreviation",
287
+ [
288
+ (timezone_utils.US_EAST_TZ, 1, -5, "EST"),
289
+ (timezone_utils.US_EAST_TZ, 7, -4, "EDT"),
290
+ (timezone_utils.US_WEST_TZ, 1, -8, "PST"),
291
+ (timezone_utils.US_WEST_TZ, 7, -7, "PDT"),
292
+ ],
293
+ )
294
+ def test_us_offsets_and_dst(self, zone, month, offset, abbreviation):
295
+ local = datetime(2024, month, 15, 12, tzinfo=timezone.utc).astimezone(zone)
296
+ assert local.utcoffset() == timedelta(hours=offset)
297
+ assert local.tzname() == abbreviation
298
+
262
299
  def test_nz_tz_constant(self):
263
300
  """Test that NZ_TZ is properly configured."""
264
301
  assert NZ_TZ.key == "Pacific/Auckland"
@@ -1,47 +0,0 @@
1
- # Markdownlint configuration for nui-python-shared-utils
2
- # Extends workspace config with project-specific overrides
3
-
4
- config:
5
- # Enable all rules by default
6
- default: true
7
-
8
- # MD013: Line length - disabled (technical docs need long lines for URLs/commands)
9
- MD013: false
10
-
11
- # MD033: Inline HTML - allowed (needed for complex formatting)
12
- MD033: false
13
-
14
- # MD036: No emphasis as heading - disabled (acceptable in planning docs)
15
- MD036: false
16
-
17
- # MD040: Fenced code language - disabled (not all code blocks need language tags)
18
- MD040: false
19
-
20
- # MD041: First line heading - disabled (docs may start with metadata)
21
- MD041: false
22
-
23
- # MD024: Multiple headings with same content - allowed with different levels
24
- MD024:
25
- siblings_only: true
26
-
27
- # MD034: Bare URLs - allowed (sometimes intentional)
28
- MD034: false
29
-
30
- # MD046: Code block style - prefer fenced
31
- MD046:
32
- style: "fenced"
33
-
34
- # MD060: Table column style - disabled (minor formatting preference)
35
- MD060: false
36
-
37
- # File patterns to check
38
- globs:
39
- - "**/*.md"
40
-
41
- # Exclusions
42
- ignores:
43
- - "node_modules/**"
44
- - "vendor/**"
45
- - ".git/**"
46
- - "venv/**"
47
- - ".venv/**"