Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a0d4ab4
build(deps-dev): bump werkzeug from 3.0.4 to 3.0.6
dependabot[bot] Oct 26, 2024
cbbf327
build(deps): bump boto3-stubs from 1.35.29 to 1.35.53
dependabot[bot] Nov 1, 2024
21e3915
Merge dependabot/pip/boto3-stubs-1.35.53 into mesh-2092-dependabot-co…
github-actions[bot] Nov 25, 2024
1e32d5d
Merge dependabot/pip/werkzeug-3.0.6 into mesh-2092-dependabot-combined
github-actions[bot] Nov 25, 2024
9f0748a
Merge branch 'develop' into mesh-2092-dependabot-combined
github-actions[bot] Nov 25, 2024
271d2ea
MESH-2092 update packages
james-bradley-nhs Nov 27, 2024
0de16b9
build(deps-dev): bump pytest-httpserver from 1.1.0 to 1.1.1
dependabot[bot] Mar 1, 2025
3c21f37
build(deps-dev): bump tox from 4.23.2 to 4.25.0
dependabot[bot] Apr 1, 2025
bb592d3
build(deps): bump boto3-stubs from 1.35.93 to 1.38.6
dependabot[bot] May 1, 2025
869a806
build(deps-dev): bump ruff from 0.8.6 to 0.11.7
dependabot[bot] May 1, 2025
9dd7aa7
build(deps): bump boto3 from 1.35.95 to 1.37.38
dependabot[bot] May 1, 2025
6b32d04
Merge branch 'develop' of github.com:NHSDigital/nhs-aws-helpers into …
james-bradley-nhs May 8, 2025
bf57bba
Merge branch 'dependabot/pip/pytest-httpserver-1.1.1' of github.com:N…
james-bradley-nhs May 8, 2025
8b6360c
Merge branch 'dependabot/pip/tox-4.25.0' of github.com:NHSDigital/nhs…
james-bradley-nhs May 8, 2025
ea09df2
Merge branch 'dependabot/pip/boto3-stubs-1.38.6' of github.com:NHSDig…
james-bradley-nhs May 8, 2025
b798b82
Merge branch 'dependabot/pip/ruff-0.11.7' of github.com:NHSDigital/nh…
james-bradley-nhs May 8, 2025
3dd8052
Merge branch 'dependabot/pip/boto3-1.37.38' of github.com:NHSDigital/…
james-bradley-nhs May 8, 2025
515584c
mesh-2092 update packages
james-bradley-nhs May 8, 2025
da1ffae
Mesh-2092 bump actions/upload-arifact to v4
james-bradley-nhs May 8, 2025
eeb81ec
mesh-2092 fix test by adding NumberOfDecreasesToday
james-bradley-nhs May 9, 2025
a8b368b
mesh-2092: remove python 3.8
james-bradley-nhs May 14, 2025
07833fe
mesh-2092: remove python 3.8
james-bradley-nhs May 14, 2025
f7659c6
mesh-2092: update packages
james-bradley-nhs May 14, 2025
0cfa517
mesh-2092 remove 3.8 from github workflows
james-bradley-nhs May 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/merge-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
with:
path: |
.venv
key: ${{ runner.os }}-poetry-v2-py3.8-${{ hashFiles('./poetry.lock') }}
key: ${{ runner.os }}-poetry-v2-py3.9-${{ hashFiles('./poetry.lock') }}

- name: git reset
run: git reset --hard
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
tox:
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]

runs-on: ubuntu-latest
if: github.repository == 'NHSDigital/nhs-aws-helpers'
Expand Down Expand Up @@ -114,7 +114,7 @@ jobs:
with:
path: |
.venv
key: ${{ runner.os }}-poetry-v2-py3.8-${{ hashFiles('./poetry.lock') }}
key: ${{ runner.os }}-poetry-v2-py3.9-${{ hashFiles('./poetry.lock') }}

- name: git reset
run: git reset --hard
Expand Down Expand Up @@ -171,7 +171,7 @@ jobs:

- name: archive reports
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: reports
path: reports/**/*
Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
with:
path: |
.venv
key: ${{ runner.os }}-poetry-v2-py3.8-${{ hashFiles('./poetry.lock') }}
key: ${{ runner.os }}-poetry-v2-py3.9-${{ hashFiles('./poetry.lock') }}

- name: git reset
run: git reset --hard
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
poetry 1.8.5
python 3.10.12 3.8.12 3.9.12 3.11.5
python 3.10.12 3.9.12 3.11.5
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"editor.formatOnSave": true,
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
}
},
"isort.args": [
Expand Down
50 changes: 22 additions & 28 deletions nhs_aws_helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,15 @@
import random
import re
import time
from collections.abc import AsyncGenerator, Generator, Iterable, Sequence
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial, reduce, wraps
from typing import (
IO,
Any,
AsyncGenerator,
Callable,
Dict,
Generator,
Iterable,
List,
Literal,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
Expand Down Expand Up @@ -86,15 +79,15 @@ def s3_build_uri(bucket: str, key: str) -> str:
return f"s3://{bucket}/{key}"


def s3_split_path(s3uri: str) -> Tuple[str, str, str]:
def s3_split_path(s3uri: str) -> tuple[str, str, str]:
match = s3re.match(s3uri)
if not match:
raise ValueError(f"Not a s3 uri: {s3uri}")
scheme, bucket, key = match.groups()
return scheme, bucket, key


_default_configs: Dict[str, Config] = {}
_default_configs: dict[str, Config] = {}

_pre_configure: Optional[Callable[[str, str, Optional[Config]], Optional[Config]]] = None

Expand Down Expand Up @@ -322,9 +315,9 @@ def secret_binary_value(name: str, session: Optional[Session] = None, config: Op

def ssm_parameter(
name: str, decrypt=False, session: Optional[Session] = None, config: Optional[Config] = None
) -> Union[str, List[str]]:
) -> Union[str, list[str]]:
ssm = ssm_client(session=session, config=config)
value = cast(Union[str, List[str]], ssm.get_parameter(Name=name, WithDecryption=decrypt)["Parameter"]["Value"])
value = cast(Union[str, list[str]], ssm.get_parameter(Name=name, WithDecryption=decrypt)["Parameter"]["Value"])
return value


Expand Down Expand Up @@ -359,7 +352,7 @@ def s3_get_tags(
session: Optional[Session] = None,
config: Optional[Config] = None,
client: Optional[S3Client] = None,
) -> Dict[str, str]:
) -> dict[str, str]:
client = client or s3_resource(session=session, config=config).meta.client
result = client.get_object_tagging(Bucket=bucket, Key=key)
tags = {pair["Key"]: pair["Value"] for pair in result["TagSet"]}
Expand All @@ -369,7 +362,7 @@ def s3_get_tags(
def s3_replace_tags(
bucket: str,
key: str,
tags: Dict[str, str],
tags: dict[str, str],
session: Optional[Session] = None,
config: Optional[Config] = None,
client: Optional[S3Client] = None,
Expand All @@ -384,11 +377,11 @@ def s3_replace_tags(
def s3_update_tags(
bucket: str,
key: str,
tags: Dict[str, str],
tags: dict[str, str],
session: Optional[Session] = None,
config: Optional[Config] = None,
client: Optional[S3Client] = None,
) -> Dict[str, str]:
) -> dict[str, str]:
client = client or s3_resource(session=session, config=config).meta.client

result = client.get_object_tagging(Bucket=bucket, Key=key)
Expand All @@ -407,7 +400,7 @@ def s3_get_all_keys(
session: Optional[Session] = None,
config: Optional[Config] = None,
client: Optional[S3Client] = None,
) -> List[str]:
) -> list[str]:
client = client or s3_resource(session=session, config=config).meta.client
paginator = client.get_paginator("list_objects_v2")
page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix)
Expand Down Expand Up @@ -442,7 +435,7 @@ def s3_delete_keys(


def s3_delete_versioned_keys(
keys: Iterable[Tuple[str, str]], bucket: str, session: Optional[Session] = None, config: Optional[Config] = None
keys: Iterable[tuple[str, str]], bucket: str, session: Optional[Session] = None, config: Optional[Config] = None
):
# delete specific versions, rather than deleting "objects" and adding delete_marker
buck = s3_bucket(bucket, session=session, config=config)
Expand Down Expand Up @@ -512,7 +505,8 @@ def filter_items(items: Sequence[Union[DeleteMarkerEntryTypeDef, ObjectVersionTy

for i in range(0, len(version_list), 1000):
response = client.delete_objects(
Bucket=bucket, Delete={"Objects": version_list[i : i + 1000], "Quiet": True} # type: ignore[typeddict-item]
Bucket=bucket,
Delete={"Objects": version_list[i : i + 1000], "Quiet": True}, # type: ignore[typeddict-item]
)
print(response)

Expand Down Expand Up @@ -608,7 +602,7 @@ def s3_put(bucket: Bucket, key: str, body: Union[bytes, str], encoding: str = "u
return obj


def s3_list_prefixes(s3_path: str, session: Optional[Session] = None, config: Optional[Config] = None) -> List[str]:
def s3_list_prefixes(s3_path: str, session: Optional[Session] = None, config: Optional[Config] = None) -> list[str]:
_na, bucket, prefix = s3_split_path(s3_path)

if not prefix.endswith("/"):
Expand All @@ -619,7 +613,7 @@ def s3_list_prefixes(s3_path: str, session: Optional[Session] = None, config: Op
return [o["Prefix"].replace(prefix, "").strip("/") for o in result.get("CommonPrefixes", [])]


def s3_list_folders(bucket_name: str, bucket_prefix: str, page_size: int = 100) -> List[str]:
def s3_list_folders(bucket_name: str, bucket_prefix: str, page_size: int = 100) -> list[str]:
paginator = s3_client().get_paginator("list_objects")
folders = []
iterator = paginator.paginate(
Expand Down Expand Up @@ -656,7 +650,7 @@ def s3_get_size(


def s3_upload_multipart_from_copy(
obj: Object, parts_keys: Sequence[str], executor_type: Type[ThreadPoolExecutor] = ThreadPoolExecutor, **kwargs
obj: Object, parts_keys: Sequence[str], executor_type: type[ThreadPoolExecutor] = ThreadPoolExecutor, **kwargs
):
multipart_upload = obj.initiate_multipart_upload(**kwargs)

Expand Down Expand Up @@ -889,7 +883,7 @@ async def _async_wrapper(*args, **kwargs):
@dynamodb_retry_backoff()
def get_items_batched(
ddb_table_name: str,
keys: List[Dict[str, Any]],
keys: list[dict[str, Any]],
client: Optional[DynamoDBClient] = None,
session: Optional[Session] = None,
config: Optional[Config] = None,
Expand All @@ -905,12 +899,12 @@ def get_items_batched(

def ddb_get_items(
ddb_table_name: str,
keys: List[Dict[str, Any]],
keys: list[dict[str, Any]],
client: Optional[DynamoDBClient] = None,
session: Optional[Session] = None,
config: Optional[Config] = None,
**kwargs,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
client = client or cast(DynamoDBClient, dynamodb(session=session, config=config).meta.client)
result = []
remaining = keys
Expand Down Expand Up @@ -1106,7 +1100,7 @@ async def async_stream_from_s3(
_RE_CANCELLATION_REASONS = re.compile(r"^.+\[(\w+[^\]]*?)\]$")


def transaction_cancellation_reasons(err: ClientError) -> List[str]:
def transaction_cancellation_reasons(err: ClientError) -> list[str]:
"""
get cancellation reasons as strings .. e.g. ['None', 'ConditionalCheckFailed', 'None']
"""
Expand All @@ -1121,7 +1115,7 @@ def transaction_cancellation_reasons(err: ClientError) -> List[str]:
return [reason.strip() for reason in match.group(1).split(",")]


def transaction_cancelled_for_only_reasons(err: ClientError, *match_reason: str) -> List[str]:
def transaction_cancelled_for_only_reasons(err: ClientError, *match_reason: str) -> list[str]:
"""
returns all reasons ... if all reasons either match match_reason or 'None'
"""
Expand All @@ -1130,5 +1124,5 @@ def transaction_cancelled_for_only_reasons(err: ClientError, *match_reason: str)
return reasons if all(reason in match_reasons for reason in reasons) else []


def cancellation_reasons_if_conditional_check(err: ClientError) -> List[str]:
def cancellation_reasons_if_conditional_check(err: ClientError) -> list[str]:
return transaction_cancelled_for_only_reasons(err, "ConditionalCheckFailed")
11 changes: 6 additions & 5 deletions nhs_aws_helpers/async_s3_object_writer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
from asyncio import Task
from typing import Any, Final, Iterator, List, Mapping, Optional, Union
from collections.abc import Iterator, Mapping
from typing import Any, Final, Optional, Union

from mypy_boto3_s3.service_resource import MultipartUpload, Object

Expand Down Expand Up @@ -28,8 +29,8 @@ def __init__(
self._encoding = encoding
self._buffer = b""
self._bytes_written, self._position = (0, 0)
self._upload_tasks: List[Task] = []
self._upload_errors: List[Exception] = []
self._upload_tasks: list[Task] = []
self._upload_errors: list[Exception] = []
self._buffer_size = buffer_size
self._multipart_upload: Optional[MultipartUpload] = None
self._name = name
Expand Down Expand Up @@ -95,7 +96,7 @@ def read(self, num_bytes: int = ...) -> Union[str, bytes]:
def readline(self, limit: int = ...) -> Union[str, bytes]:
raise NotImplementedError

def readlines(self, hint: int = ...) -> List[Union[str, bytes]]:
def readlines(self, hint: int = ...) -> list[Union[str, bytes]]:
raise NotImplementedError

def seek(self, offset: int, whence: int = ...) -> int:
Expand All @@ -107,7 +108,7 @@ def __next__(self) -> Union[str, bytes]:
def __iter__(self) -> Iterator[Union[str, bytes]]:
raise NotImplementedError

async def writelines(self, lines: List[Union[str, bytes]]) -> None:
async def writelines(self, lines: list[Union[str, bytes]]) -> None:
for arg in lines:
await self.write(arg)

Expand Down
15 changes: 8 additions & 7 deletions nhs_aws_helpers/dynamodb_model_store/base_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import inspect
from collections.abc import Mapping
from dataclasses import fields, is_dataclass
from functools import lru_cache
from typing import Any, Dict, Generic, List, Mapping, Tuple, Type, TypeVar, cast
from typing import Any, Generic, TypeVar, cast

TModelKey = TypeVar("TModelKey", bound=Mapping[str, Any])

Expand All @@ -11,7 +12,7 @@ class serialised_property(property):


class BaseModel(Generic[TModelKey]):
_model_key_type: Type[TModelKey]
_model_key_type: type[TModelKey]

@serialised_property
def model_type(self) -> str:
Expand All @@ -21,7 +22,7 @@ def model_key(self) -> TModelKey:
return cast(TModelKey, {k: getattr(self, k) for k in self.model_key_fields()})

@classmethod
def model_key_fields(cls) -> List[str]:
def model_key_fields(cls) -> list[str]:
model_key_fields = _MODEL_KEY_FIELDS.get(cls._model_key_type)
if not model_key_fields:
model_key_fields = list(cls._model_key_type.__annotations__.keys())
Expand All @@ -31,16 +32,16 @@ def model_key_fields(cls) -> List[str]:
return model_key_fields

@classmethod
def model_key_from_item(cls, item: Dict[str, Any]) -> TModelKey:
def model_key_from_item(cls, item: dict[str, Any]) -> TModelKey:
return cast(TModelKey, {k: item.get(k) for k in cls.model_key_fields()})


_MODEL_KEY_FIELDS: Dict[type, List[str]] = {}
_MODEL_KEY_FIELDS: dict[type, list[str]] = {}


@lru_cache
def model_properties_cache(model_type: Type[BaseModel]) -> List[Tuple[str, type, Mapping[str, Any]]]:
model_fields: List[Tuple[str, type, Mapping[str, Any]]] = []
def model_properties_cache(model_type: type[BaseModel]) -> list[tuple[str, type, Mapping[str, Any]]]:
model_fields: list[tuple[str, type, Mapping[str, Any]]] = []

if is_dataclass(model_type):
model_fields.extend([(field.name, field.type, field.metadata) for field in fields(model_type)])
Expand Down
Loading
Loading