Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/ISSUE_TEMPLATE/bug-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ assignees: ''
#### Desktop (please complete the following information):
- **OS**: [e.g. MacOS 10.15.7, Windows 10]
- **Package Version** [e.g. 0.0.1 or commit ID]
- **Python Version** [e.g. python 3.9.16]
- **Python Version** [e.g. python 3.12.2]
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: Build and Test

on:
pull_request:
branches: [ main ]
branches: [ main, release/* ]
paths-ignore:
- '**/_version.py'
push:
branches: [ main ]
branches: [ main, release/* ]
tags-ignore:
- 'v*'
paths-ignore:
Expand Down
17 changes: 16 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,19 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@release/v1

create-github-release:
name: Create GitHub Release
needs: [bump, publish-to-pypi]
if: ${{ github.event.inputs.dry-run == 'false' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.bump.outputs.VERSION_TAG }}
name: ${{ needs.bump.outputs.VERSION_TAG }}
generate_release_notes: true
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ install-release: .uv ## Installs package dependencies
uv sync --frozen --group release


rebuild-lockfile: .uv ## Rebuilds the lockfile
upgrade-lockfile: .uv ## Upgrade lockfile to latest compatible versions (uv lock --upgrade)
uv lock --upgrade

link-packages: ## Link local packages to virtualenv
Expand Down Expand Up @@ -107,7 +107,7 @@ unlink-packages: ## Unlink local packages from virtualenv
make install; \
fi

.PHONY: .uv install install-release install rebuild-lockfile link-packages unlink-packages
.PHONY: .uv install install-release install upgrade-lockfile link-packages unlink-packages

#######################
##@ Formatting Commands
Expand Down
9 changes: 3 additions & 6 deletions docs/developer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,14 @@ class MyCustomHandler(LambdaHandler):
### Handler with Request/Response Models

```python
from dataclasses import dataclass
from aibs_informatics_aws_lambda.common.handler import LambdaHandler
from aibs_informatics_core.models import SchemaModel
from aibs_informatics_core.models.base import PydanticBaseModel

@dataclass
class MyRequest(SchemaModel):
class MyRequest(PydanticBaseModel):
input_path: str
output_path: str

@dataclass
class MyResponse(SchemaModel):
class MyResponse(PydanticBaseModel):
status: str
files_processed: int

Expand Down
9 changes: 3 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,13 @@ pip install aibs-informatics-aws-lambda
### Basic Usage

```python
from dataclasses import dataclass
from aibs_informatics_core.models.base import SchemaModel
from aibs_informatics_core.models.base import PydanticBaseModel
from aibs_informatics_aws_lambda.common.handler import LambdaHandler

@dataclass
class MyRequest(SchemaModel):
class MyRequest(PydanticBaseModel):
name: str

@dataclass
class MyResponse(SchemaModel):
class MyResponse(PydanticBaseModel):
message: str

class MyHandler(LambdaHandler[MyRequest, MyResponse]):
Expand Down
17 changes: 6 additions & 11 deletions docs/user-guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,13 @@ uv add aibs-informatics-aws-lambda
The `LambdaHandler` class provides a base class for creating strongly typed lambda functions with features like serialization/deserialization, logging, and metrics.

```python
from dataclasses import dataclass
from aibs_informatics_core.models.base import SchemaModel
from aibs_informatics_core.models.base import PydanticBaseModel
from aibs_informatics_aws_lambda.common.handler import LambdaHandler

@dataclass
class MyRequest(SchemaModel):
class MyRequest(PydanticBaseModel):
name: str

@dataclass
class MyResponse(SchemaModel):
class MyResponse(PydanticBaseModel):
message: str

class MyHandler(LambdaHandler[MyRequest, MyResponse]):
Expand All @@ -49,15 +46,13 @@ For API Gateway integrations, use `ApiLambdaHandler`:

```python
from dataclasses import dataclass
from aibs_informatics_core.models.base import SchemaModel
from aibs_informatics_core.models.base import PydanticBaseModel
from aibs_informatics_aws_lambda.common.api.handler import ApiLambdaHandler

@dataclass
class UserRequest(SchemaModel):
class UserRequest(PydanticBaseModel):
user_id: str

@dataclass
class UserResponse(SchemaModel):
class UserResponse(PydanticBaseModel):
name: str
email: str

Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ authors = [{ name = "AIBS Informatics Group", email = "marmot@alleninstitute.onm
maintainers = [{ name = "AIBS Informatics Group", email = "marmot@alleninstitute.onmicrosoft.com"}]
description = "Utility library for building validated and typed AWS Lambda functions"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.9"
requires-python = ">=3.11"
dynamic = [
"version",
]
dependencies = [
"aibs-informatics-aws-utils>=0.0.12,<1",
"aibs-informatics-core>=0.2.6,<1",
"aibs-informatics-aws-utils~=1.0",
"aibs-informatics-core>=1.0.3,<2",
"aws-lambda-powertools ~= 2.35",
"pydantic >= 2.0.3, < 3",
"aws-lambda-typing",
Expand Down
11 changes: 6 additions & 5 deletions src/aibs_informatics_aws_lambda/common/api/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
]

import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Callable, Dict, Generic, Optional, TypeVar, Union, cast
from typing import Any, Generic, Optional, TypeVar, Union, cast

from aibs_informatics_core.models.api.http_parameters import HTTPParameters
from aibs_informatics_core.models.api.route import ApiRoute
Expand Down Expand Up @@ -78,7 +79,7 @@ def handle(self, request: MyRequest) -> MyResponse:
```
"""

_current_event: Optional[BaseProxyEvent] = field(default=None, repr=False)
_current_event: BaseProxyEvent | None = field(default=None, repr=False)

def __post_init__(self):
super().__post_init__()
Expand Down Expand Up @@ -153,8 +154,8 @@ def add_to_router(
cls,
router: BaseRouter,
*args,
logger: Optional[Logger] = None,
metrics: Optional[Union[EphemeralMetrics, Metrics]] = None,
logger: Logger | None = None,
metrics: EphemeralMetrics | Metrics | None = None,
**kwargs,
) -> Callable:
"""Register this handler with an API Gateway router.
Expand Down Expand Up @@ -216,7 +217,7 @@ def gateway_handler(logger=logger, metrics=metrics, **route_parameters) -> Any:

@classmethod
def _parse_event(
cls, event: BaseProxyEvent, route_parameters: Dict[str, Any], logger: logging.Logger
cls, event: BaseProxyEvent, route_parameters: dict[str, Any], logger: logging.Logger
) -> API_REQUEST:
"""Parse an API Gateway event into a request object.

Expand Down
15 changes: 8 additions & 7 deletions src/aibs_informatics_aws_lambda/common/api/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
"ApiResolverBuilder",
]
import json
from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime
from traceback import format_exc
from types import ModuleType
from typing import Callable, ClassVar, List, Optional, Union
from typing import ClassVar, Union

from aibs_informatics_core.collections import PostInitMixin
from aibs_informatics_core.utils.json import JSON, JSONObject
Expand Down Expand Up @@ -205,8 +206,8 @@ def get_lambda_handler(self, *args, **kwargs) -> LambdaHandlerType:
def add_handlers(
self,
target_module: ModuleType,
router: Optional[BaseRouter] = None,
prefix: Optional[str] = None,
router: BaseRouter | None = None,
prefix: str | None = None,
):
"""Dynamically add all API Lambda handlers from a module.

Expand Down Expand Up @@ -239,8 +240,8 @@ def add_handlers(
def add_handlers_to_router(
router: BaseRouter,
target_module: ModuleType,
metrics: Optional[Union[EphemeralMetrics, Metrics]] = None,
logger: Optional[Logger] = None,
metrics: EphemeralMetrics | Metrics | None = None,
logger: Logger | None = None,
):
"""Add all API handlers from a module to a router.

Expand All @@ -261,7 +262,7 @@ def add_handlers_to_router(
api_handler_class.add_to_router(router, logger=logger, metrics=metrics)


def get_target_handler_classes(target_module: ModuleType) -> List[ApiLambdaHandler]:
def get_target_handler_classes(target_module: ModuleType) -> list[ApiLambdaHandler]:
"""Get all ApiLambdaHandler subclasses in a module.

Recursively loads all modules from the target package and returns
Expand All @@ -287,7 +288,7 @@ def get_target_handler_classes(target_module: ModuleType) -> List[ApiLambdaHandl
*list(loaded_modules.keys()),
]

target_api_handler_classes: List[ApiLambdaHandler] = [
target_api_handler_classes: list[ApiLambdaHandler] = [
api_handler_class
for api_handler_class in get_all_subclasses(ApiLambdaHandler, True) # type: ignore[type-abstract]
if (getattr(api_handler_class, "__module__") in target_module_paths)
Expand Down
29 changes: 15 additions & 14 deletions src/aibs_informatics_aws_lambda/common/handler.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import json
import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import Callable, Generic, Literal, Optional, TypeVar, Union, cast
from typing import Generic, Literal, Optional, TypeAlias, TypeVar, cast

from aibs_informatics_aws_utils.s3 import download_to_json_object, upload_json
from aibs_informatics_core.executors.base import BaseExecutor
from aibs_informatics_core.models.aws.s3 import S3URI
from aibs_informatics_core.models.aws.s3 import S3Path
from aibs_informatics_core.models.base import ModelProtocol
from aibs_informatics_core.utils.json import JSON
from aws_lambda_powertools.utilities.batch import (
Expand All @@ -24,8 +25,9 @@
from aibs_informatics_aws_lambda.common.logging import LoggingMixins
from aibs_informatics_aws_lambda.common.metrics import MetricsMixins

LambdaEvent = Union[JSON] # type: ignore # https://github.com/python/mypy/issues/7866
LambdaEvent: TypeAlias = JSON
LambdaHandlerType = Callable[[LambdaEvent, LambdaContext], Optional[JSON]]

logger = logging.getLogger(__name__)

REQUEST = TypeVar("REQUEST", bound=ModelProtocol)
Expand Down Expand Up @@ -58,12 +60,10 @@ class LambdaHandler(

Example:
```python
@dataclass
class MyRequest(SchemaModel):
class MyRequest(PydanticBaseModel):
name: str

@dataclass
class MyResponse(SchemaModel):
class MyResponse(PydanticBaseModel):
message: str

class MyHandler(LambdaHandler[MyRequest, MyResponse]):
Expand All @@ -79,24 +79,24 @@ def __post_init__(self):
super().__post_init__()

@classmethod
def load_input__remote(cls, remote_path: S3URI) -> JSON:
def load_input__remote(cls, remote_path: S3Path) -> JSON:
"""Load input data from a remote S3 location.

Args:
remote_path (S3URI): The S3 URI to download the input from.
remote_path (S3Path): The S3 URI to download the input from.

Returns:
The JSON content from the S3 object.
"""
return download_to_json_object(remote_path)

@classmethod
def write_output__remote(cls, output: JSON, remote_path: S3URI) -> None:
def write_output__remote(cls, output: JSON, remote_path: S3Path) -> None:
"""Write output data to a remote S3 location.

Args:
output (JSON): The JSON content to upload.
remote_path (S3URI): The S3 URI to upload the output to.
remote_path (S3Path): The S3 URI to upload the output to.
"""
return upload_json(output, remote_path)

Expand Down Expand Up @@ -132,7 +132,7 @@ def get_handler(cls, *args, **kwargs) -> LambdaHandlerType:
logger = cls.get_logger(service=cls.service_name(), add_to_root=False)

@logger.inject_lambda_context(log_event=True)
def handler(event: LambdaEvent, context: LambdaContext) -> Optional[JSON]:
def handler(event: LambdaEvent, context: LambdaContext) -> JSON | None:
lambda_handler = cls(*args, **kwargs) # type: ignore[call-arg]
logger.info(f"Instantiated {lambda_handler}.")
lambda_handler.log = logger
Expand All @@ -155,6 +155,7 @@ def handler(event: LambdaEvent, context: LambdaContext) -> Optional[JSON]:

return None

handler._handler_class = cls # type: ignore[attr-defined]
return handler

@classmethod
Expand Down Expand Up @@ -229,7 +230,7 @@ def get_sqs_batch_handler(
logger = cls.get_logger(cls.service_name())

# Create a record handler for each record in batch.
def record_handler(record: SQSRecord) -> Optional[JSON]:
def record_handler(record: SQSRecord) -> JSON | None:
if not cls.should_process_sqs_record(record):
logger.info(f"SQS record {record} elected not to be processed.")
return None
Expand Down Expand Up @@ -315,7 +316,7 @@ def get_dynamodb_stream_handler(cls, *args, **kwargs) -> LambdaHandlerType:
logger = cls.get_logger(cls.service_name())

# Create a record handler for each record in batch.
def record_handler(record: DynamoDBRecord) -> Optional[JSON]:
def record_handler(record: DynamoDBRecord) -> JSON | None:
if not cls.should_process_dynamodb_record(record):
logger.info(f"DynamoDB record {record} will not be processed.")
return None
Expand Down
7 changes: 3 additions & 4 deletions src/aibs_informatics_aws_lambda/common/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"""

import logging
from typing import Optional, Union

from aibs_informatics_core.utils.logging import get_all_handlers
from aws_lambda_powertools.logging import Logger
Expand Down Expand Up @@ -73,7 +72,7 @@ def logger(self, value: Logger):
self._logger = value

@classmethod
def get_logger(cls, service: Optional[str] = None, add_to_root: bool = False) -> Logger:
def get_logger(cls, service: str | None = None, add_to_root: bool = False) -> Logger:
"""Create a new Logger instance.

Args:
Expand All @@ -95,7 +94,7 @@ def add_logger_to_root(self):


def get_service_logger(
service: Optional[str] = None, child: bool = False, add_to_root: bool = False
service: str | None = None, child: bool = False, add_to_root: bool = False
) -> Logger:
"""Create a service logger with optional root logger integration.

Expand All @@ -114,7 +113,7 @@ def get_service_logger(


def add_handler_to_logger(
source_logger: Logger, target_logger: Union[str, logging.Logger, None] = None
source_logger: Logger, target_logger: str | logging.Logger | None = None
):
"""Add a source logger's handler to a target logger.

Expand Down
Loading
Loading