nui-python-shared-utils 2.0__py3-none-any.whl → 2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nui-python-shared-utils
3
- Version: 2.0
3
+ Version: 2.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
@@ -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<8.0.0,>=7.17.0; extra == "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<8.0.0,>=7.17.0; extra == "all"
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
@@ -1,4 +1,4 @@
1
- nui_python_shared_utils-2.0.dist-info/licenses/LICENSE,sha256=vGe2mC5yLUb8toYlY3T36ZwCB5zQUW5hlCtEMiqokhM,1071
1
+ nui_python_shared_utils-2.1.dist-info/licenses/LICENSE,sha256=vGe2mC5yLUb8toYlY3T36ZwCB5zQUW5hlCtEMiqokhM,1071
2
2
  nui_shared_utils/__init__.py,sha256=vFS5oaw7_iHgHnLRifDYYyb7au6G2nYJ6ztqMs-hrQs,13289
3
3
  nui_shared_utils/base_client.py,sha256=I1lKQGhrKyvujV2zps0TrtEdPDqMCwRQaIh6ceGONXQ,11016
4
4
  nui_shared_utils/cli.py,sha256=JJpSoQWKvAz4b8cO30yFNi5vY9jmqrCHzbFpvrVTkbU,8747
@@ -6,7 +6,7 @@ nui_shared_utils/cloudwatch_metrics.py,sha256=p0J-s63UJDGK14TRGt0k8yoj7aWSSM55UK
6
6
  nui_shared_utils/config.py,sha256=ZbkUKDxnHMJiHNv-TlWKjotmf2mH1Vz3GaIXgGfWg7E,4524
7
7
  nui_shared_utils/db_client.py,sha256=HyVKJ1Su0dVPV1QI2MU5UtT8myDRCjKgV60xRL4SEdE,21726
8
8
  nui_shared_utils/error_handler.py,sha256=pJ6b_mI0Ait8QRbs9UmLzYUN-Ft0l_dnpudVO_6LRSE,12171
9
- nui_shared_utils/es_client.py,sha256=rLyexuL6_RXQpuMC4A2SeJDk30medx2qxOgWIG3A_7o,15765
9
+ nui_shared_utils/es_client.py,sha256=3MlrbddTrRN2mEH1b3TNzcmwYycLdujoIq_5_j-Cyfc,16714
10
10
  nui_shared_utils/es_query_builder.py,sha256=UuheQf8b2UXaOiJlYCKYIfKfVERDZ3ag1P0OOOWm9do,12158
11
11
  nui_shared_utils/jwt_auth.py,sha256=2Ag1zZKxd2R8QBz3aQA4h8OtKHQvUVKrjUp6lXpJxJU,9143
12
12
  nui_shared_utils/lambda_helpers.py,sha256=psHVotpmOfnmyQCoOt4MSIEj7VwWcy6gZM3vYSZwjOk,3041
@@ -15,9 +15,9 @@ nui_shared_utils/log_processors.py,sha256=x5gz1LEkbmoMCZ3ZB6q-mbn26dSzwh1trz4wSk
15
15
  nui_shared_utils/powertools_helpers.py,sha256=qc-lYLfY-QX20gALfPOJcihrtJhiCU-fJW8rsPT7LJE,11037
16
16
  nui_shared_utils/secrets_helper.py,sha256=_jQkMWzjayZ8vvyHG2yuuxur-SQYUUONfcsZ0MF8qOw,7025
17
17
  nui_shared_utils/slack_client.py,sha256=wXE0D4T4FWt1giz9Xmp71RustpaJYeLAZo6JxulDVko,25550
18
- nui_shared_utils/slack_formatter.py,sha256=95g6XfAJst7RVhd0M0ahiF3gWWW5j96WYkCg5tLS6Zg,10894
18
+ nui_shared_utils/slack_formatter.py,sha256=QT9VXApE0q09n_PmEVvaLCSfLW24vmowlCXF3Dzimfk,10850
19
19
  nui_shared_utils/snowflake_client.py,sha256=e3jH6_2mMqbtP1TE8-mUFFYrfGW9eqUPT5tlcgeCqx8,16181
20
- nui_shared_utils/timezone.py,sha256=TvtSZV7w3Vvz3NbMTcJSNbUf0ZxPwpFS-xfgye1h-4Q,3583
20
+ nui_shared_utils/timezone.py,sha256=gTiSK5NUG0Z8UY_AsB4ktIzLrZ0wwTXtdTUH-Npdvik,3837
21
21
  nui_shared_utils/utils.py,sha256=gOpw80tZGC24QsyesR8EySRODY-TFXG8cXUvggPBsRI,8184
22
22
  nui_shared_utils/logging/__init__.py,sha256=gVC853q3ja4RhcBVbf5jKO10lFX4upfsTvkKZx4wu54,1805
23
23
  nui_shared_utils/logging/binding.py,sha256=I5VNmtf4U2yt54aS4F0Djw4u22mfjwCpWvORUnJNkPU,13577
@@ -27,8 +27,8 @@ nui_shared_utils/slack_setup/__init__.py,sha256=OElyS3xk4F_YKH5uUUTDpN0ah1dOO3e5
27
27
  nui_shared_utils/slack_setup/channel_creator.py,sha256=0gyCBIS0EC96SVn1Z2dD4Fpzk0xNdHSWyiCxoUQIUe8,10667
28
28
  nui_shared_utils/slack_setup/channel_definitions.py,sha256=atfz5ZhpqefOeLh1gShbWd-TLzgjPmhv95bfuTdmZog,5676
29
29
  nui_shared_utils/slack_setup/setup_helpers.py,sha256=pzzXMs12GI9sdZttWeYzgLPgC0xqPz7ZLHM1GUPNNXc,7219
30
- nui_python_shared_utils-2.0.dist-info/METADATA,sha256=bK7P8bMGhOLpSyVQsZ3gwrWRZ6KZksIoAJIlj6V-qHM,22584
31
- nui_python_shared_utils-2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
32
- nui_python_shared_utils-2.0.dist-info/entry_points.txt,sha256=TrJ3Z4kz3oXfy6InXn20jHu7rnTwo-4EA0KRFX3fkL0,66
33
- nui_python_shared_utils-2.0.dist-info/top_level.txt,sha256=YMRu5cnn3hxHoYq9wNvaI9atVHnTNVdswO5_cQSfnJI,17
34
- nui_python_shared_utils-2.0.dist-info/RECORD,,
30
+ nui_python_shared_utils-2.1.dist-info/METADATA,sha256=fWK7ffhd2g2e4rKPJeAhStSKUECYAxImYWsvGZyTbvM,22688
31
+ nui_python_shared_utils-2.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
32
+ nui_python_shared_utils-2.1.dist-info/entry_points.txt,sha256=TrJ3Z4kz3oXfy6InXn20jHu7rnTwo-4EA0KRFX3fkL0,66
33
+ nui_python_shared_utils-2.1.dist-info/top_level.txt,sha256=YMRu5cnn3hxHoYq9wNvaI9atVHnTNVdswO5_cQSfnJI,17
34
+ nui_python_shared_utils-2.1.dist-info/RECORD,,
@@ -97,24 +97,37 @@ class ElasticsearchClient(BaseClient, ServiceHealthMixin):
97
97
  retry_on_timeout=self.client_config.get("retry_on_timeout", True),
98
98
  )
99
99
 
100
+ @staticmethod
101
+ def _body_with_size(body: Dict, size: int) -> Dict:
102
+ """Return a copy of ``body`` carrying ``size``.
103
+
104
+ ``size`` travels inside the request body rather than as a sibling keyword
105
+ argument, which is the one call shape every supported client major accepts.
106
+ On elasticsearch-py 8.x/9.x, passing ``body=`` alongside a body field as a
107
+ keyword folds that keyword into the caller's dict (mutating it) and raises
108
+ ``ValueError`` if the key is already there, so reusing a body dict for two
109
+ searches breaks. A body that carries its own ``size`` is accepted unchanged
110
+ by 7.17, 8.x and 9.x, and leaves the caller's dict alone.
111
+ """
112
+ return {**body, "size": size}
113
+
100
114
  @handle_client_errors(default_return=[])
101
115
  def search(self, index: str, body: Dict, size: int = 100) -> List[Dict]:
102
116
  """
103
117
  Execute search query with error handling.
104
-
118
+
105
119
  Args:
106
120
  index: Index pattern to search
107
121
  body: Elasticsearch query body
108
- size: Maximum results to return
109
-
122
+ size: Maximum results to return (overrides any ``size`` in ``body``)
123
+
110
124
  Returns:
111
125
  List of hit documents
112
126
  """
113
127
  def _search_operation():
114
128
  response = self._service_client.search(
115
129
  index=index,
116
- body=body,
117
- size=size,
130
+ body=self._body_with_size(body, size),
118
131
  ignore_unavailable=True
119
132
  )
120
133
  return [hit["_source"] for hit in response["hits"]["hits"]]
@@ -141,8 +154,7 @@ class ElasticsearchClient(BaseClient, ServiceHealthMixin):
141
154
  def _aggregate_operation():
142
155
  response = self._service_client.search(
143
156
  index=index,
144
- body=body,
145
- size=0, # Only need aggregations
157
+ body=self._body_with_size(body, 0), # Only need aggregations
146
158
  ignore_unavailable=True
147
159
  )
148
160
  return response.get("aggregations", {})
@@ -374,7 +386,9 @@ class ElasticsearchClient(BaseClient, ServiceHealthMixin):
374
386
  format="json",
375
387
  h="index,health,status,docs.count,store.size"
376
388
  )
377
- return response or []
389
+ # 8.x/9.x return a response wrapper around the JSON array; normalise to
390
+ # a plain list so callers get the same type on every client major.
391
+ return list(response) if response else []
378
392
 
379
393
  return self._execute_with_error_handling(
380
394
  "get_indices_info",
@@ -4,8 +4,9 @@ Provides a builder pattern for creating Slack Block Kit messages.
4
4
  """
5
5
 
6
6
  from typing import Dict, List, Any, Optional, Union
7
- from datetime import datetime, timedelta
8
- import pytz
7
+ from datetime import datetime, timedelta, timezone
8
+
9
+ from .timezone import NZ_TZ
9
10
 
10
11
  # Severity emoji mapping
11
12
  SEVERITY_EMOJI = {"critical": "🚨", "warning": "⚠️", "info": "ℹ️", "success": "✅", "error": "❌"}
@@ -42,25 +43,23 @@ def format_number(value: Union[int, float], decimals: int = 0) -> str:
42
43
  def format_nz_time(dt: Optional[datetime] = None) -> str:
43
44
  """Format datetime in NZ timezone."""
44
45
  if dt is None:
45
- dt = datetime.utcnow()
46
+ dt = datetime.now(timezone.utc)
46
47
  elif not dt.tzinfo:
47
- dt = dt.replace(tzinfo=pytz.UTC)
48
+ dt = dt.replace(tzinfo=timezone.utc)
48
49
 
49
- nz_tz = pytz.timezone("Pacific/Auckland")
50
- nz_time = dt.astimezone(nz_tz)
50
+ nz_time = dt.astimezone(NZ_TZ)
51
51
  return nz_time.strftime("%I:%M %p %Z")
52
52
 
53
53
 
54
54
  def format_date_range(start: datetime, end: datetime) -> str:
55
55
  """Format a date range in NZ timezone."""
56
- nz_tz = pytz.timezone("Pacific/Auckland")
57
56
  if not start.tzinfo:
58
- start = start.replace(tzinfo=pytz.UTC)
57
+ start = start.replace(tzinfo=timezone.utc)
59
58
  if not end.tzinfo:
60
- end = end.replace(tzinfo=pytz.UTC)
59
+ end = end.replace(tzinfo=timezone.utc)
61
60
 
62
- start_nz = start.astimezone(nz_tz)
63
- end_nz = end.astimezone(nz_tz)
61
+ start_nz = start.astimezone(NZ_TZ)
62
+ end_nz = end.astimezone(NZ_TZ)
64
63
 
65
64
  if start_nz.date() == end_nz.date():
66
65
  return f"{start_nz.strftime('%b %d')} {start_nz.strftime('%I:%M %p')} - {end_nz.strftime('%I:%M %p %Z')}"
@@ -1,17 +1,21 @@
1
1
  """
2
2
  Timezone utilities for multi-timezone handling (NZ primary, AUS/US/EUR secondary).
3
+
4
+ Zone data comes from the standard library's ``zoneinfo``, which reads the system
5
+ IANA database. Environments without one (Windows, minimal images such as Alpine)
6
+ need the ``tzdata`` package: `pip install nui-python-shared-utils[timezone]`.
3
7
  """
4
8
 
5
9
  from datetime import datetime, timezone
6
- import pytz
7
- from typing import List, Tuple, Optional
10
+ from typing import List, Optional, Tuple
11
+ from zoneinfo import ZoneInfo
8
12
 
9
13
  # Supported timezones
10
- NZ_TZ = pytz.timezone("Pacific/Auckland") # Primary timezone
11
- AUS_TZ = pytz.timezone("Australia/Sydney")
12
- US_EAST_TZ = pytz.timezone("US/Eastern")
13
- US_WEST_TZ = pytz.timezone("US/Pacific")
14
- EUR_TZ = pytz.timezone("Europe/London")
14
+ NZ_TZ = ZoneInfo("Pacific/Auckland") # Primary timezone
15
+ AUS_TZ = ZoneInfo("Australia/Sydney")
16
+ US_EAST_TZ = ZoneInfo("US/Eastern")
17
+ US_WEST_TZ = ZoneInfo("US/Pacific")
18
+ EUR_TZ = ZoneInfo("Europe/London")
15
19
 
16
20
 
17
21
  def nz_time(utc_dt: Optional[datetime] = None) -> datetime:
@@ -28,7 +32,7 @@ def nz_time(utc_dt: Optional[datetime] = None) -> datetime:
28
32
  utc_dt = datetime.now(timezone.utc)
29
33
 
30
34
  if utc_dt.tzinfo is None:
31
- utc_dt = pytz.UTC.localize(utc_dt)
35
+ utc_dt = utc_dt.replace(tzinfo=timezone.utc)
32
36
 
33
37
  return utc_dt.astimezone(NZ_TZ)
34
38
 
@@ -66,7 +70,7 @@ def format_multi_timezone(dt: Optional[datetime] = None, primary_tz: str = "nz",
66
70
  dt = datetime.now(timezone.utc)
67
71
 
68
72
  if dt.tzinfo is None:
69
- dt = pytz.UTC.localize(dt)
73
+ dt = dt.replace(tzinfo=timezone.utc)
70
74
 
71
75
  # Timezone mapping
72
76
  tz_map = {"nz": NZ_TZ, "aus": AUS_TZ, "us_east": US_EAST_TZ, "us_west": US_WEST_TZ, "eur": EUR_TZ}