diff --git a/.github/workflows/test-on-push-and-pr.yml b/.github/workflows/test-on-push-and-pr.yml index 5b80d23..d1a282c 100644 --- a/.github/workflows/test-on-push-and-pr.yml +++ b/.github/workflows/test-on-push-and-pr.yml @@ -12,8 +12,19 @@ jobs: steps: - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Run 'pr' target - run: make pr + run: poetry run poe pr alpine: runs-on: ubuntu-latest @@ -21,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run alpine integration tests - run: DISTRO=alpine make test-integ + run: DISTRO=alpine poetry run poe test-integ amazonlinux: runs-on: ubuntu-latest @@ -29,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run amazonlinux integration tests - run: DISTRO=amazonlinux make test-integ + run: DISTRO=amazonlinux poetry run poe test-integ debian: runs-on: ubuntu-latest @@ -37,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run debian integration tests - run: DISTRO=debian make test-integ + run: DISTRO=debian poetry run poe test-integ ubuntu: runs-on: ubuntu-latest @@ -45,4 +56,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run ubuntu integration tests - run: DISTRO=ubuntu make test-integ \ No newline at end of file + run: DISTRO=ubuntu poetry run poe test-integ diff --git a/.gitignore b/.gitignore index 9d46e4c..d0a548e 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,12 @@ tmp*.py deps/artifacts/ deps/aws-lambda-cpp-*/ deps/curl-*/ + +# local caches +.DS_Store + +# local build artifacts +build-artifacts + +# native wheels used for testing the separation of the native build +native-wheels/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index bb66b03..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -repos: - - repo: https://github.com/python/black - rev: 19.3b0 - hooks: - - id: black - language_version: python3.9 - exclude_types: ['markdown', 'ini', 'toml', 'rst'] diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..1036be4 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,21 @@ +FROM python:3.9-alpine AS build-image + + +RUN apk add --no-cache curl make + +# Install native wheel dependency +COPY native-wheels/*.whl /tmp/native-wheels/ +RUN pip install /tmp/native-wheels/*.whl + +# Build awslambdaric +ARG RIC_BUILD_DIR="/home/build/" +RUN mkdir -p ${RIC_BUILD_DIR} +WORKDIR ${RIC_BUILD_DIR} +COPY . . +RUN pip install setuptools +RUN curl -sSL https://install.python-poetry.org | python3 - && \ + ln -s /root/.local/bin/poetry /usr/local/bin/poetry +RUN poetry install && poetry run poe build + +# Keep the built wheel accessible +CMD ["sh", "-c", "echo 'Build complete. Wheel available in /home/build/dist/'"] \ No newline at end of file diff --git a/Dockerfile.rie b/Dockerfile.rie new file mode 100644 index 0000000..60a7d0e --- /dev/null +++ b/Dockerfile.rie @@ -0,0 +1,30 @@ +FROM python:3.9-alpine + +RUN apk add --no-cache libstdc++ curl + +# Install RIE +RUN curl -Lo /usr/local/bin/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \ + chmod +x /usr/local/bin/aws-lambda-rie + +# Install native wheel dependency first +ADD native-wheels/*.whl /tmp/native/ +RUN pip install /tmp/native/*.whl + +# Add the pre-built wheel +ADD build-artifacts/*.whl /tmp/ + +# Install the wheel +RUN pip install /tmp/*.whl + +# Copy test handler +COPY tests/integration/test-handlers/echo/app.py /var/task/app.py + +# Set environment for local testing +ENV AWS_LAMBDA_RUNTIME_API="127.0.0.1:8080" +ENV LAMBDA_TASK_ROOT="/var/task" +ENV _HANDLER="app.handler" + +WORKDIR /var/task + +ENTRYPOINT ["/usr/local/bin/aws-lambda-rie"] +CMD ["python", "-m", "awslambdaric", "app.handler"] \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 521b61c..0000000 --- a/Makefile +++ /dev/null @@ -1,72 +0,0 @@ -.PHONY: target -target: - $(info ${HELP_MESSAGE}) - @exit 0 - -.PHONY: init -init: - pip3 install -r requirements/base.txt -r requirements/dev.txt - -.PHONY: test -test: check-format - pytest --cov awslambdaric --cov-report term-missing --cov-fail-under 90 tests - -.PHONY: setup-codebuild-agent -setup-codebuild-agent: - docker build -t codebuild-agent - < tests/integration/codebuild-local/Dockerfile.agent - -.PHONY: test-smoke -test-smoke: setup-codebuild-agent - CODEBUILD_IMAGE_TAG=codebuild-agent tests/integration/codebuild-local/test_one.sh tests/integration/codebuild/buildspec.os.alpine.yml alpine 3.15 3.9 - -.PHONY: test-integ -test-integ: setup-codebuild-agent - CODEBUILD_IMAGE_TAG=codebuild-agent DISTRO="$(DISTRO)" tests/integration/codebuild-local/test_all.sh tests/integration/codebuild/. - -.PHONY: check-security -check-security: - bandit -r awslambdaric - -.PHONY: format -format: - black setup.py awslambdaric/ tests/ - -.PHONY: check-format -check-format: - black --check setup.py awslambdaric/ tests/ - -# Command to run everytime you make changes to verify everything works -.PHONY: dev -dev: init test - -# Verifications to run before sending a pull request -.PHONY: pr -pr: init check-format check-security dev - -codebuild: setup-codebuild-agent - CODEBUILD_IMAGE_TAG=codebuild-agent DISTRO="$(DISTRO)" tests/integration/codebuild-local/test_all.sh tests/integration/codebuild - -.PHONY: clean -clean: - rm -rf dist - rm -rf awslambdaric.egg-info - -.PHONY: build -build: clean - BUILD=true python3 setup.py sdist - -define HELP_MESSAGE - -Usage: $ make [TARGETS] - -TARGETS - check-security Run bandit to find security issues. - format Run black to automatically update your code to match our formatting. - build Builds the package. - clean Cleans the working directory by removing built artifacts. - dev Run all development tests after a change. - init Initialize and install the requirements and dev-requirements for this project. - pr Perform all checks before submitting a Pull Request. - test Run the Unit tests. - -endef diff --git a/README.md b/README.md index 4a96a3f..f0aa532 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,26 @@ The Python Runtime Interface Client package currently supports Python versions: ## Usage +### Container-Based Builds + +For development or when you need to build awslambdaric from source, you can use container-based builds to ensure consistent compilation across different platforms, and native dependencies linking. + +```shell script +# Build awslambdaric wheel in a Linux container +make build-container +# Or with poetry (run 'poetry install' first): +poetry run build-container + +# Test with RIE using the built wheel +make test-rie +# Or with poetry: +poetry run test-rie +``` + +This approach builds the C++ extensions in a Linux environment, ensuring compatibility with Lambda's runtime environment regardless of your host OS. + +**Note**: Running `make build` (or `poetry run build`) on non-Linux machines will not properly link the native C++ dependencies, resulting in a non-functional runtime client. Always use container-based builds for development. + ### Creating a Docker Image for Lambda with the Runtime Interface Client First step is to choose the base image to be used. The supported Linux OS distributions are: @@ -103,38 +123,23 @@ def handler(event, context): ### Local Testing -To make it easy to locally test Lambda functions packaged as container images we open-sourced a lightweight web-server, Lambda Runtime Interface Emulator (RIE), which allows your function packaged as a container image to accept HTTP requests. You can install the [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) on your local machine to test your function. Then when you run the image function, you set the entrypoint to be the emulator. - -*To install the emulator and test your Lambda function* - -1) From your project directory, run the following command to download the RIE from GitHub and install it on your local machine. +To test Lambda functions with the Runtime Interface Client, use the [AWS Lambda Runtime Interface Emulator (RIE)](https://github.com/aws/aws-lambda-runtime-interface-emulator). To test your local changes with RIE (Runtime Interface Emulator): ```shell script -mkdir -p ~/.aws-lambda-rie && \ - curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \ - chmod +x ~/.aws-lambda-rie/aws-lambda-rie +# Build your current code (do this when you make changes) +# We build on a linux machine to ensure native build dependencies are met +make build-container +# Or with poetry: +poetry run build-container + +# Test with RIE (fast, repeatable) +make test-rie +# Or with poetry: +poetry run test-rie + +# Test the function +curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"message":"test"}' ``` -2) Run your Lambda image function using the docker run command. - -```shell script -docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ - --entrypoint /aws-lambda/aws-lambda-rie \ - myfunction:latest \ - /usr/local/bin/python -m awslambdaric app.handler -``` - -This runs the image as a container and starts up an endpoint locally at `http://localhost:9000/2015-03-31/functions/function/invocations`. - -3) Post an event to the following endpoint using a curl command: - -```shell script -curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' -``` - -This command invokes the function running in the container image and returns a response. - -*Alternately, you can also include RIE as a part of your base image. See the AWS documentation on how to [Build RIE into your base image](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-alternative).* - ## Development @@ -145,7 +150,6 @@ Clone this repository and run: make init make build ``` - ### Running tests Make sure the project is built: diff --git a/awslambdaric/__init__.py b/awslambdaric/__init__.py index 5605903..e1e5905 100644 --- a/awslambdaric/__init__.py +++ b/awslambdaric/__init__.py @@ -1,5 +1,3 @@ -""" -Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. -""" +"""Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.""" -__version__ = "3.1.1" +__version__ = "3.0.2" diff --git a/awslambdaric/__main__.py b/awslambdaric/__main__.py index 5cbbaab..3cd1ad4 100644 --- a/awslambdaric/__main__.py +++ b/awslambdaric/__main__.py @@ -1,6 +1,4 @@ -""" -Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -""" +"""Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.""" import os import sys @@ -9,6 +7,7 @@ def main(args): + """Run the Lambda runtime main entry point.""" app_root = os.getcwd() try: diff --git a/awslambdaric/bootstrap.py b/awslambdaric/bootstrap.py index cb8d5c3..1847103 100644 --- a/awslambdaric/bootstrap.py +++ b/awslambdaric/bootstrap.py @@ -1,6 +1,4 @@ -""" -Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -""" +"""Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.""" import importlib import json @@ -9,10 +7,12 @@ import sys import time import traceback +from typing import Any, Optional, List from .lambda_context import LambdaContext from .lambda_runtime_client import LambdaRuntimeClient from .lambda_runtime_exception import FaultException +from .interfaces import RuntimeClientProtocol, LogSinkProtocol from .lambda_runtime_log_utils import ( _DATETIME_FORMAT, _DEFAULT_FRAME_TYPE, @@ -36,6 +36,7 @@ def _get_handler(handler): + """Get handler function from module.""" try: (modname, fname) = handler.rsplit(".", 1) except ValueError as e: @@ -82,6 +83,7 @@ def make_error( stack_trace, invoke_id=None, ): + """Create error response.""" result = { "errorMessage": error_message if error_message else "", "errorType": error_type if error_type else "", @@ -92,6 +94,7 @@ def make_error( def replace_line_indentation(line, indent_char, new_indent_char): + """Replace line indentation characters.""" ident_chars_count = 0 for c in line: if c != indent_char: @@ -104,6 +107,7 @@ def replace_line_indentation(line, indent_char, new_indent_char): _ERROR_FRAME_TYPE = _JSON_FRAME_TYPES[logging.ERROR] def log_error(error_result, log_sink): + """Log error in JSON format.""" error_result = { "timestamp": time.strftime( _DATETIME_FORMAT, logging.Formatter.converter(time.time()) @@ -119,6 +123,7 @@ def log_error(error_result, log_sink): _ERROR_FRAME_TYPE = _TEXT_FRAME_TYPES[logging.ERROR] def log_error(error_result, log_sink): + """Log error in text format.""" error_description = "[ERROR]" error_result_type = error_result.get("errorType") @@ -149,18 +154,19 @@ def log_error(error_result, log_sink): def handle_event_request( - lambda_runtime_client, - request_handler, - invoke_id, - event_body, - content_type, - client_context_json, - cognito_identity_json, - invoked_function_arn, - epoch_deadline_time_in_ms, - tenant_id, - log_sink, -): + lambda_runtime_client: RuntimeClientProtocol, + request_handler: Any, + invoke_id: Optional[str], + event_body: Any, + content_type: Optional[str], + client_context_json: Optional[str], + cognito_identity_json: Optional[str], + invoked_function_arn: Optional[str], + epoch_deadline_time_in_ms: Optional[str], + tenant_id: Optional[str], + log_sink: LogSinkProtocol, +) -> None: + """Handle Lambda event request.""" error_result = None try: lambda_context = create_lambda_context( @@ -201,7 +207,6 @@ def handle_event_request( ) if error_result is not None: - log_error(error_result, log_sink) lambda_runtime_client.post_invocation_error( invoke_id, to_json(error_result), to_json(xray_fault) @@ -213,6 +218,7 @@ def handle_event_request( def parse_json_header(header, name): + """Parse JSON header.""" try: return json.loads(header) except Exception as e: @@ -224,13 +230,14 @@ def parse_json_header(header, name): def create_lambda_context( - client_context_json, - cognito_identity_json, - epoch_deadline_time_in_ms, - invoke_id, - invoked_function_arn, - tenant_id, + client_context_json: Optional[str], + cognito_identity_json: Optional[str], + epoch_deadline_time_in_ms: Optional[str], + invoke_id: Optional[str], + invoked_function_arn: Optional[str], + tenant_id: Optional[str], ): + """Create Lambda context object.""" client_context = None if client_context_json: client_context = parse_json_header(client_context_json, "Client Context") @@ -249,6 +256,7 @@ def create_lambda_context( def build_fault_result(exc_info, msg): + """Build fault result from exception info.""" etype, value, tb = exc_info tb_tuples = extract_traceback(tb) for i in range(len(tb_tuples)): @@ -264,6 +272,7 @@ def build_fault_result(exc_info, msg): def make_xray_fault(ex_type, ex_msg, working_dir, tb_tuples): + """Create X-Ray fault object.""" stack = [] files = set() for t in tb_tuples: @@ -282,6 +291,7 @@ def make_xray_fault(ex_type, ex_msg, working_dir, tb_tuples): def extract_traceback(tb): + """Extract traceback information.""" return [ (frame.filename, frame.lineno, frame.name, frame.line) for frame in traceback.extract_tb(tb) @@ -289,6 +299,7 @@ def extract_traceback(tb): def on_init_complete(lambda_runtime_client, log_sink): + """Handle initialization completion.""" from . import lambda_runtime_hooks_runner try: @@ -312,21 +323,29 @@ def on_init_complete(lambda_runtime_client, log_sink): class LambdaLoggerHandler(logging.Handler): + """Lambda logger handler.""" + def __init__(self, log_sink): + """Initialize logger handler.""" logging.Handler.__init__(self) self.log_sink = log_sink def emit(self, record): + """Emit log record.""" msg = self.format(record) self.log_sink.log(msg) class LambdaLoggerHandlerWithFrameType(logging.Handler): + """Lambda logger handler with frame type.""" + def __init__(self, log_sink): + """Initialize logger handler.""" super().__init__() self.log_sink = log_sink def emit(self, record): + """Emit log record with frame type.""" self.log_sink.log( self.format(record), frame_type=( @@ -337,14 +356,20 @@ def emit(self, record): class LambdaLoggerFilter(logging.Filter): + """Lambda logger filter.""" + def filter(self, record): + """Filter log record.""" record.aws_request_id = _GLOBAL_AWS_REQUEST_ID or "" record.tenant_id = _GLOBAL_TENANT_ID return True class Unbuffered(object): + """Unbuffered stream wrapper.""" + def __init__(self, stream): + """Initialize unbuffered stream.""" self.stream = stream def __enter__(self): @@ -357,16 +382,21 @@ def __getattr__(self, attr): return getattr(self.stream, attr) def write(self, msg): + """Write message to stream.""" self.stream.write(msg) self.stream.flush() def writelines(self, msgs): + """Write multiple lines to stream.""" self.stream.writelines(msgs) self.stream.flush() -class StandardLogSink(object): +class StandardLogSink: + """Standard log sink.""" + def __init__(self): + """Initialize standard log sink.""" pass def __enter__(self): @@ -375,18 +405,20 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_tb): pass - def log(self, msg, frame_type=None): + def log(self, msg: str, frame_type: Optional[bytes] = None) -> None: + """Log message to stdout.""" sys.stdout.write(msg) - def log_error(self, message_lines): + def log_error(self, message_lines: List[str]) -> None: + """Log error message to stdout.""" error_message = ERROR_LOG_LINE_TERMINATE.join(message_lines) + "\n" sys.stdout.write(error_message) class FramedTelemetryLogSink(object): - """ - FramedTelemetryLogSink implements the logging contract between runtimes and the platform. It implements a simple - framing protocol so message boundaries can be determined. Each frame can be visualized as follows: + """FramedTelemetryLogSink implements the logging contract between runtimes and the platform. + + It implements a simple framing protocol so message boundaries can be determined. Each frame can be visualized as follows:
     {@code
     +----------------------+------------------------+---------------------+-----------------------+
@@ -400,7 +432,9 @@ class FramedTelemetryLogSink(object):
     """
 
     def __init__(self, fd):
+        """Initialize framed telemetry log sink."""
         self.fd = int(fd)
+        self.file: Any = None
 
     def __enter__(self):
         self.file = os.fdopen(self.fd, "wb", 0)
@@ -409,7 +443,8 @@ def __enter__(self):
     def __exit__(self, exc_type, exc_value, exc_tb):
         self.file.close()
 
-    def log(self, msg, frame_type=None):
+    def log(self, msg: str, frame_type: Optional[bytes] = None) -> None:
+        """Log message with frame type."""
         encoded_msg = msg.encode("utf8")
 
         timestamp = int(time.time_ns() / 1000)  # UNIX timestamp in microseconds
@@ -421,7 +456,8 @@ def log(self, msg, frame_type=None):
         )
         self.file.write(log_msg)
 
-    def log_error(self, message_lines):
+    def log_error(self, message_lines: List[str]) -> None:
+        """Log error message."""
         error_message = "\n".join(message_lines)
         self.log(
             error_message,
@@ -430,6 +466,7 @@ def log_error(self, message_lines):
 
 
 def update_xray_env_variable(xray_trace_id):
+    """Update X-Ray trace ID environment variable."""
     if xray_trace_id is not None:
         os.environ["_X_AMZN_TRACE_ID"] = xray_trace_id
     else:
@@ -438,6 +475,7 @@ def update_xray_env_variable(xray_trace_id):
 
 
 def create_log_sink():
+    """Create appropriate log sink."""
     if "_LAMBDA_TELEMETRY_LOG_FD" in os.environ:
         fd = os.environ["_LAMBDA_TELEMETRY_LOG_FD"]
         del os.environ["_LAMBDA_TELEMETRY_LOG_FD"]
@@ -452,6 +490,7 @@ def create_log_sink():
 
 
 def _setup_logging(log_format, log_level, log_sink):
+    """Set up logging configuration."""
     logging.Formatter.converter = time.gmtime
     logger = logging.getLogger()
 
@@ -478,6 +517,7 @@ def _setup_logging(log_format, log_level, log_sink):
 
 
 def run(app_root, handler, lambda_runtime_api_addr):
+    """Run Lambda runtime."""
     sys.stdout = Unbuffered(sys.stdout)
     sys.stderr = Unbuffered(sys.stderr)
 
diff --git a/awslambdaric/interfaces.py b/awslambdaric/interfaces.py
new file mode 100644
index 0000000..aaecdc6
--- /dev/null
+++ b/awslambdaric/interfaces.py
@@ -0,0 +1,33 @@
+"""Protocol interfaces for AWS Lambda Runtime Interface Client."""
+
+from typing import Protocol, Any, Dict, Tuple, Optional, List
+
+
+class RuntimeClientProtocol(Protocol):
+    """Protocol for Lambda runtime client operations."""
+    
+    marshaller: "MarshallerProtocol"
+    
+    def wait_next_invocation(self) -> Any: ...
+    
+    def post_invocation_result(self, invoke_id: Optional[str], result_data: Any, content_type: str = "application/json") -> None: ...
+    
+    def post_invocation_error(self, invoke_id: Optional[str], error_response_data: str, xray_fault: str) -> None: ...
+    
+    def post_init_error(self, error_response_data: Dict[str, Any], error_type_override: Optional[str] = None) -> None: ...
+
+
+class MarshallerProtocol(Protocol):
+    """Protocol for request/response marshalling."""
+    
+    def unmarshal_request(self, request: Any, content_type: Optional[str] = "application/json") -> Any: ...
+    
+    def marshal_response(self, response: Any) -> Tuple[Any, str]: ...
+
+
+class LogSinkProtocol(Protocol):
+    """Protocol for logging operations."""
+    
+    def log(self, msg: str, frame_type: Optional[bytes] = None) -> None: ...
+    
+    def log_error(self, message_lines: List[str]) -> None: ...
\ No newline at end of file
diff --git a/awslambdaric/lambda_context.py b/awslambdaric/lambda_context.py
index e0a3363..ac701df 100644
--- a/awslambdaric/lambda_context.py
+++ b/awslambdaric/lambda_context.py
@@ -1,14 +1,15 @@
-"""
-Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-"""
+"""Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved."""
 
 import logging
 import os
 import sys
 import time
+from typing import Optional, Any
 
 
 class LambdaContext(object):
+    """Lambda context object."""
+
     def __init__(
         self,
         invoke_id,
@@ -18,6 +19,7 @@ def __init__(
         invoked_function_arn=None,
         tenant_id=None,
     ):
+        """Initialize Lambda context."""
         self.aws_request_id = invoke_id
         self.log_group_name = os.environ.get("AWS_LAMBDA_LOG_GROUP_NAME")
         self.log_stream_name = os.environ.get("AWS_LAMBDA_LOG_STREAM_NAME")
@@ -34,7 +36,7 @@ def __init__(
             )
 
         self.identity = make_obj_from_dict(CognitoIdentity, {})
-        if cognito_identity is not None:
+        if cognito_identity is not None and self.identity is not None:
             self.identity.cognito_identity_id = cognito_identity.get(
                 "cognitoIdentityId"
             )
@@ -45,11 +47,13 @@ def __init__(
         self._epoch_deadline_time_in_ms = epoch_deadline_time_in_ms
 
     def get_remaining_time_in_millis(self):
+        """Get remaining time in milliseconds."""
         epoch_now_in_ms = int(time.time() * 1000)
         delta_ms = self._epoch_deadline_time_in_ms - epoch_now_in_ms
         return delta_ms if delta_ms > 0 else 0
 
     def log(self, msg):
+        """Log a message."""
         for handler in logging.getLogger().handlers:
             if hasattr(handler, "log_sink"):
                 handler.log_sink.log(str(msg))
@@ -74,7 +78,13 @@ def __repr__(self):
 
 
 class CognitoIdentity(object):
+    """Cognito identity information."""
+
     __slots__ = ["cognito_identity_id", "cognito_identity_pool_id"]
+    
+    def __init__(self) -> None:
+        self.cognito_identity_id: Optional[str] = None
+        self.cognito_identity_pool_id: Optional[str] = None
 
     def __repr__(self):
         return (
@@ -86,6 +96,8 @@ def __repr__(self):
 
 
 class Client(object):
+    """Client information."""
+
     __slots__ = [
         "installation_id",
         "app_title",
@@ -93,6 +105,13 @@ class Client(object):
         "app_version_code",
         "app_package_name",
     ]
+    
+    def __init__(self) -> None:
+        self.installation_id: Optional[str] = None
+        self.app_title: Optional[str] = None
+        self.app_version_name: Optional[str] = None
+        self.app_version_code: Optional[str] = None
+        self.app_package_name: Optional[str] = None
 
     def __repr__(self):
         return (
@@ -107,7 +126,14 @@ def __repr__(self):
 
 
 class ClientContext(object):
+    """Client context information."""
+
     __slots__ = ["custom", "env", "client"]
+    
+    def __init__(self) -> None:
+        self.custom: Optional[Any] = None
+        self.env: Optional[Any] = None
+        self.client: Optional[Client] = None
 
     def __repr__(self):
         return (
@@ -120,6 +146,7 @@ def __repr__(self):
 
 
 def make_obj_from_dict(_class, _dict, fields=None):
+    """Create object from dictionary."""
     if _dict is None:
         return None
     obj = _class()
@@ -128,6 +155,7 @@ def make_obj_from_dict(_class, _dict, fields=None):
 
 
 def set_obj_from_dict(obj, _dict, fields=None):
+    """Set object attributes from dictionary."""
     if fields is None:
         fields = obj.__class__.__slots__
     for field in fields:
diff --git a/awslambdaric/lambda_literals.py b/awslambdaric/lambda_literals.py
index 2585b89..a2a0746 100644
--- a/awslambdaric/lambda_literals.py
+++ b/awslambdaric/lambda_literals.py
@@ -1,6 +1,4 @@
-"""
-Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-"""
+"""Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved."""
 
 lambda_warning = "LAMBDA_WARNING"
 
diff --git a/awslambdaric/lambda_runtime_client.py b/awslambdaric/lambda_runtime_client.py
index ba4ad92..3d5cf09 100644
--- a/awslambdaric/lambda_runtime_client.py
+++ b/awslambdaric/lambda_runtime_client.py
@@ -1,11 +1,11 @@
-"""
-Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-"""
+"""Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved."""
 
 import sys
+from typing import Any, Dict, Optional
 from awslambdaric import __version__
 from .lambda_runtime_exception import FaultException
 from .lambda_runtime_marshaller import to_json
+from .interfaces import MarshallerProtocol, RuntimeClientProtocol
 
 ERROR_TYPE_HEADER = "Lambda-Runtime-Function-Error-Type"
 
@@ -18,26 +18,41 @@ def _user_agent():
     return f"aws-lambda-python/{py_version}-{pkg_version}"
 
 
-try:
-    import runtime_client
-
-    runtime_client.initialize_client(_user_agent())
-except ImportError:
-    runtime_client = None
+# Import native extension
+import awslambdaric_native as runtime_client
+runtime_client.initialize_client(_user_agent())
 
 from .lambda_runtime_marshaller import LambdaMarshaller
 
 
-class InvocationRequest(object):
-    def __init__(self, **kwds):
+class InvocationRequest:
+    """Lambda invocation request."""
+
+    def __init__(self, **kwds: Any) -> None:
+        """Initialize invocation request."""
+        self.invoke_id: Optional[str] = kwds.get('invoke_id')
+        self.x_amzn_trace_id: Optional[str] = kwds.get('x_amzn_trace_id')
+        self.invoked_function_arn: Optional[str] = kwds.get('invoked_function_arn')
+        self.deadline_time_in_ms: Optional[str] = kwds.get('deadline_time_in_ms')
+        self.client_context: Optional[str] = kwds.get('client_context')
+        self.cognito_identity: Optional[str] = kwds.get('cognito_identity')
+        self.tenant_id: Optional[str] = kwds.get('tenant_id')
+        self.content_type: Optional[str] = kwds.get('content_type')
+        self.event_body: Any = kwds.get('event_body')
         self.__dict__.update(kwds)
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
+        """Check equality."""
+        if not isinstance(other, InvocationRequest):
+            return False
         return self.__dict__ == other.__dict__
 
 
 class LambdaRuntimeClientError(Exception):
+    """Lambda runtime client error."""
+
     def __init__(self, endpoint, response_code, response_body):
+        """Initialize runtime client error."""
         self.endpoint = endpoint
         self.response_code = response_code
         self.response_body = response_body
@@ -46,13 +61,16 @@ def __init__(self, endpoint, response_code, response_body):
         )
 
 
-class LambdaRuntimeClient(object):
-    marshaller = LambdaMarshaller()
+class LambdaRuntimeClient(RuntimeClientProtocol):
+    """Lambda runtime client."""
+
+    marshaller: MarshallerProtocol = LambdaMarshaller()
     """marshaller is a class attribute that determines the unmarshalling and marshalling logic of a function's event
     and response. It allows for function authors to override the the default implementation, LambdaMarshaller which
     unmarshals and marshals JSON, to an instance of a class that implements the same interface."""
 
     def __init__(self, lambda_runtime_address, use_thread_for_polling_next=False):
+        """Initialize runtime client."""
         self.lambda_runtime_address = lambda_runtime_address
         self.use_thread_for_polling_next = use_thread_for_polling_next
         if self.use_thread_for_polling_next:
@@ -65,6 +83,7 @@ def __init__(self, lambda_runtime_address, use_thread_for_polling_next=False):
     def call_rapid(
         self, http_method, endpoint, expected_http_code, payload=None, headers=None
     ):
+        """Call RAPID endpoint."""
         # These imports are heavy-weight. They implicitly trigger `import ssl, hashlib`.
         # Importing them lazily to speed up critical path of a common case.
         import http.client
@@ -83,7 +102,8 @@ def call_rapid(
         if response.code != expected_http_code:
             raise LambdaRuntimeClientError(endpoint, response.code, response_body)
 
-    def post_init_error(self, error_response_data, error_type_override=None):
+    def post_init_error(self, error_response_data: Dict[str, Any], error_type_override: Optional[str] = None) -> None:
+        """Post initialization error."""
         import http
 
         endpoint = "/2018-06-01/runtime/init/error"
@@ -99,12 +119,14 @@ def post_init_error(self, error_response_data, error_type_override=None):
         )
 
     def restore_next(self):
+        """Restore next invocation."""
         import http
 
         endpoint = "/2018-06-01/runtime/restore/next"
         self.call_rapid("GET", endpoint, http.HTTPStatus.OK)
 
     def report_restore_error(self, restore_error_data):
+        """Report restore error."""
         import http
 
         endpoint = "/2018-06-01/runtime/restore/error"
@@ -114,6 +136,7 @@ def report_restore_error(self, restore_error_data):
         )
 
     def wait_next_invocation(self):
+        """Wait for next invocation."""
         # Calling runtime_client.next() from a separate thread unblocks the main thread,
         # which can then process signals.
         if self.use_thread_for_polling_next:
@@ -130,6 +153,7 @@ def wait_next_invocation(self):
                 )
         else:
             response_body, headers = runtime_client.next()
+            
         return InvocationRequest(
             invoke_id=headers.get("Lambda-Runtime-Aws-Request-Id"),
             x_amzn_trace_id=headers.get("Lambda-Runtime-Trace-Id"),
@@ -143,8 +167,9 @@ def wait_next_invocation(self):
         )
 
     def post_invocation_result(
-        self, invoke_id, result_data, content_type="application/json"
-    ):
+        self, invoke_id: Optional[str], result_data: Any, content_type: str = "application/json"
+    ) -> None:
+        """Post invocation result."""
         runtime_client.post_invocation_result(
             invoke_id,
             (
@@ -155,7 +180,8 @@ def post_invocation_result(
             content_type,
         )
 
-    def post_invocation_error(self, invoke_id, error_response_data, xray_fault):
+    def post_invocation_error(self, invoke_id: Optional[str], error_response_data: str, xray_fault: str) -> None:
+        """Post invocation error."""
         max_header_size = 1024 * 1024  # 1MiB
         xray_fault = xray_fault if len(xray_fault.encode()) < max_header_size else ""
         runtime_client.post_error(invoke_id, error_response_data, xray_fault)
diff --git a/awslambdaric/lambda_runtime_exception.py b/awslambdaric/lambda_runtime_exception.py
index 3ea5b29..3c2e41a 100644
--- a/awslambdaric/lambda_runtime_exception.py
+++ b/awslambdaric/lambda_runtime_exception.py
@@ -1,9 +1,9 @@
-"""
-Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-"""
+"""Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved."""
 
 
 class FaultException(Exception):
+    """Exception class for Lambda runtime faults."""
+
     MARSHAL_ERROR = "Runtime.MarshalError"
     UNMARSHAL_ERROR = "Runtime.UnmarshalError"
     USER_CODE_SYNTAX_ERROR = "Runtime.UserCodeSyntaxError"
@@ -17,6 +17,7 @@ class FaultException(Exception):
     LAMBDA_RUNTIME_CLIENT_ERROR = "Runtime.LambdaRuntimeClientError"
 
     def __init__(self, exception_type, msg, trace=None):
+        """Initialize FaultException."""
         self.msg = msg
         self.exception_type = exception_type
         self.trace = trace
diff --git a/awslambdaric/lambda_runtime_hooks_runner.py b/awslambdaric/lambda_runtime_hooks_runner.py
index 8aee181..ab67e63 100644
--- a/awslambdaric/lambda_runtime_hooks_runner.py
+++ b/awslambdaric/lambda_runtime_hooks_runner.py
@@ -1,10 +1,14 @@
-# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-# SPDX-License-Identifier: Apache-2.0
+"""Lambda runtime hooks runner.
+
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+"""
 
 from snapshot_restore_py import get_before_snapshot, get_after_restore
 
 
 def run_before_snapshot():
+    """Run before snapshot hooks."""
     before_snapshot_callables = get_before_snapshot()
     while before_snapshot_callables:
         # Using pop as before checkpoint callables are executed in the reverse order of their registration
@@ -13,6 +17,7 @@ def run_before_snapshot():
 
 
 def run_after_restore():
+    """Run after restore hooks."""
     after_restore_callables = get_after_restore()
     for func, args, kwargs in after_restore_callables:
         func(*args, **kwargs)
diff --git a/awslambdaric/lambda_runtime_log_utils.py b/awslambdaric/lambda_runtime_log_utils.py
index 9ddbcfb..93a1d63 100644
--- a/awslambdaric/lambda_runtime_log_utils.py
+++ b/awslambdaric/lambda_runtime_log_utils.py
@@ -1,6 +1,4 @@
-"""
-Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-"""
+"""Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved."""
 
 import json
 import logging
@@ -36,11 +34,14 @@
 
 
 class LogFormat(IntEnum):
+    """Log format enumeration."""
+
     JSON = 0b0
     TEXT = 0b1
 
     @classmethod
     def from_str(cls, value: str):
+        """Convert string to LogFormat."""
         if value and value.upper() == "JSON":
             return cls.JSON.value
         return cls.TEXT.value
@@ -77,7 +78,10 @@ def _format_log_level(record: logging.LogRecord) -> int:
 
 
 class JsonFormatter(logging.Formatter):
+    """JSON formatter for Lambda logs."""
+
     def __init__(self):
+        """Initialize the JSON formatter."""
         super().__init__(datefmt=_DATETIME_FORMAT)
 
     @staticmethod
@@ -108,6 +112,7 @@ def __format_location(record: logging.LogRecord):
         return f"{record.pathname}:{record.funcName}:{record.lineno}"
 
     def format(self, record: logging.LogRecord) -> str:
+        """Format log record as JSON."""
         record.levelno = _format_log_level(record)
         record.levelname = logging.getLevelName(record.levelno)
         record._frame_type = _JSON_FRAME_TYPES.get(
diff --git a/awslambdaric/lambda_runtime_marshaller.py b/awslambdaric/lambda_runtime_marshaller.py
index 4256066..1a0cbdd 100644
--- a/awslambdaric/lambda_runtime_marshaller.py
+++ b/awslambdaric/lambda_runtime_marshaller.py
@@ -1,20 +1,23 @@
-"""
-Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-"""
+"""Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved."""
 
 import decimal
 import math
 import os
+from typing import Any, Tuple, Optional
 import simplejson as json
 
 from .lambda_runtime_exception import FaultException
+from .interfaces import MarshallerProtocol
 
 
 # simplejson's Decimal encoding allows '-NaN' as an output, which is a parse error for json.loads
 # to get the good parts of Decimal support, we'll special-case NaN decimals and otherwise duplicate the encoding for decimals the same way simplejson does
 # We also set 'ensure_ascii=False' so that the encoded json contains unicode characters instead of unicode escape sequences
 class Encoder(json.JSONEncoder):
+    """Custom JSON encoder for Lambda responses."""
+
     def __init__(self):
+        """Initialize the encoder."""
         if os.environ.get("AWS_EXECUTION_ENV") in {
             "AWS_Lambda_python3.12",
             "AWS_Lambda_python3.13",
@@ -23,7 +26,8 @@ def __init__(self):
         else:
             super().__init__(use_decimal=False, allow_nan=True)
 
-    def default(self, obj):
+    def default(self, obj: Any) -> Any:
+        """Handle special object types during encoding."""
         if isinstance(obj, decimal.Decimal):
             if obj.is_nan():
                 return math.nan
@@ -32,14 +36,19 @@ def default(self, obj):
 
 
 def to_json(obj):
+    """Convert object to JSON string."""
     return Encoder().encode(obj)
 
 
-class LambdaMarshaller:
+class LambdaMarshaller(MarshallerProtocol):
+    """Marshaller for Lambda requests and responses."""
+
     def __init__(self):
+        """Initialize the marshaller."""
         self.jsonEncoder = Encoder()
 
-    def unmarshal_request(self, request, content_type="application/json"):
+    def unmarshal_request(self, request: Any, content_type: Optional[str] = "application/json") -> Any:
+        """Unmarshal incoming request."""
         if content_type != "application/json":
             return request
         try:
@@ -51,7 +60,8 @@ def unmarshal_request(self, request, content_type="application/json"):
                 None,
             )
 
-    def marshal_response(self, response):
+    def marshal_response(self, response: Any) -> Tuple[Any, str]:
+        """Marshal response for Lambda."""
         if isinstance(response, bytes):
             return response, "application/unknown"
 
diff --git a/awslambdaric/runtime_client.cpp b/awslambdaric/runtime_client.cpp
deleted file mode 100644
index 7fb2e95..0000000
--- a/awslambdaric/runtime_client.cpp
+++ /dev/null
@@ -1,150 +0,0 @@
-/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
-
-#include 
-#include 
-#include 
-#include 
-
-#define NULL_IF_EMPTY(v) (((v) == NULL || (v)[0] == 0) ? NULL : (v))
-
-static const std::string ENDPOINT(getenv("AWS_LAMBDA_RUNTIME_API") ? getenv("AWS_LAMBDA_RUNTIME_API") : "127.0.0.1:9001");
-static aws::lambda_runtime::runtime *CLIENT;
-
-static PyObject *method_initialize_client(PyObject *self, PyObject *args) {
-    char *user_agent_arg;
-    if (!PyArg_ParseTuple(args, "s", &user_agent_arg)) {
-        PyErr_SetString(PyExc_RuntimeError, "Wrong arguments");
-        return NULL;
-    }
-
-    const std::string user_agent = std::string(user_agent_arg);
-
-    CLIENT = new aws::lambda_runtime::runtime(ENDPOINT, user_agent);
-    Py_INCREF(Py_None);
-    return Py_None;
-}
-
-static PyObject *method_next(PyObject *self) {
-    aws::lambda_runtime::invocation_request response;
-
-    // Release GIL and save thread state
-    // ref: https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock
-    PyThreadState *_save;
-    _save = PyEval_SaveThread();
-
-    auto outcome = CLIENT->get_next();
-    if (!outcome.is_success()) {
-        // Reacquire GIL before exiting
-        PyEval_RestoreThread(_save);
-        PyErr_SetString(PyExc_RuntimeError, "Failed to get next");
-        return NULL;
-    }
-
-    response = outcome.get_result();
-    // Reacquire GIL before constructing return object
-    PyEval_RestoreThread(_save);
-
-    auto payload = response.payload;
-    auto request_id = response.request_id.c_str();
-    auto trace_id = response.xray_trace_id.c_str();
-    auto function_arn = response.function_arn.c_str();
-    auto deadline = std::chrono::duration_cast(response.deadline.time_since_epoch()).count();
-    auto client_context = response.client_context.c_str();
-    auto content_type = response.content_type.c_str();
-    auto cognito_id = response.cognito_identity.c_str();
-    auto tenant_id = response.tenant_id.c_str();
-
-    PyObject *payload_bytes = PyBytes_FromStringAndSize(payload.c_str(), payload.length());
-    PyObject *result = Py_BuildValue("(O,{s:s,s:s,s:s,s:l,s:s,s:s,s:s,s:s})",
-                         payload_bytes,  //Py_BuildValue() increments reference counter
-                         "Lambda-Runtime-Aws-Request-Id", request_id,
-                         "Lambda-Runtime-Trace-Id", NULL_IF_EMPTY(trace_id),
-                         "Lambda-Runtime-Invoked-Function-Arn", function_arn,
-                         "Lambda-Runtime-Deadline-Ms", deadline,
-                         "Lambda-Runtime-Client-Context", NULL_IF_EMPTY(client_context),
-                         "Content-Type", NULL_IF_EMPTY(content_type),
-                         "Lambda-Runtime-Cognito-Identity", NULL_IF_EMPTY(cognito_id),
-                         "Lambda-Runtime-Aws-Tenant-Id", NULL_IF_EMPTY(tenant_id)
-    );
-
-    Py_XDECREF(payload_bytes);
-    return result;
-}
-
-static PyObject *method_post_invocation_result(PyObject *self, PyObject *args) {
-    if (CLIENT == nullptr) {
-        PyErr_SetString(PyExc_RuntimeError, "Client not yet initalized");
-        return NULL;
-    }
-
-    PyObject *invocation_response;
-    Py_ssize_t length;
-    char *request_id, *content_type, *response_as_c_string;
-
-    if (!PyArg_ParseTuple(args, "sSs", &request_id, &invocation_response, &content_type)) {
-        PyErr_SetString(PyExc_RuntimeError, "Wrong arguments");
-        return NULL;
-    }
-
-    length = PyBytes_Size(invocation_response);
-    response_as_c_string = PyBytes_AsString(invocation_response);
-    std::string response_string(response_as_c_string, response_as_c_string + length);
-
-    auto response = aws::lambda_runtime::invocation_response::success(response_string, content_type);
-    auto outcome = CLIENT->post_success(request_id, response);
-    if (!outcome.is_success()) {
-        PyErr_SetString(PyExc_RuntimeError, "Failed to post invocation response");
-        return NULL;
-    }
-
-    Py_INCREF(Py_None);
-    return Py_None;
-}
-
-static PyObject *method_post_error(PyObject *self, PyObject *args) {
-    if (CLIENT == nullptr) {
-        PyErr_SetString(PyExc_RuntimeError, "Client not yet initalized");
-        return NULL;
-    }
-
-    char *request_id, *response_string, *xray_fault;
-
-    if (!PyArg_ParseTuple(args, "sss", &request_id, &response_string, &xray_fault)) {
-        PyErr_SetString(PyExc_RuntimeError, "Wrong arguments");
-        return NULL;
-    }
-
-    auto response = aws::lambda_runtime::invocation_response(response_string, "application/json", false, xray_fault);
-    auto outcome = CLIENT->post_failure(request_id, response);
-    if (!outcome.is_success()) {
-        PyErr_SetString(PyExc_RuntimeError, "Failed to post invocation error");
-        return NULL;
-    }
-
-    Py_INCREF(Py_None);
-    return Py_None;
-}
-
-static PyMethodDef Runtime_Methods[] = {
-        {"initialize_client",      method_initialize_client,      METH_VARARGS, NULL},
-        {"next",                   (PyCFunction) method_next,     METH_NOARGS,  NULL},
-        {"post_invocation_result", method_post_invocation_result, METH_VARARGS, NULL},
-        {"post_error",             method_post_error,             METH_VARARGS, NULL},
-        {NULL,                     NULL,                          0,            NULL}
-};
-
-static struct PyModuleDef runtime_client = {
-        PyModuleDef_HEAD_INIT,
-        "runtime",
-        NULL,
-        -1,
-        Runtime_Methods,
-        NULL,
-        NULL,
-        NULL,
-        NULL
-};
-
-PyMODINIT_FUNC PyInit_runtime_client(void) {
-    return PyModule_Create(&runtime_client);
-}
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..0758dfe
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,514 @@
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
+markers = "sys_platform == \"win32\""
+files = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.10.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+    {file = "coverage-7.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1c86eb388bbd609d15560e7cc0eb936c102b6f43f31cf3e58b4fd9afe28e1372"},
+    {file = "coverage-7.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b4ba0f488c1bdb6bd9ba81da50715a372119785458831c73428a8566253b86b"},
+    {file = "coverage-7.10.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083442ecf97d434f0cb3b3e3676584443182653da08b42e965326ba12d6b5f2a"},
+    {file = "coverage-7.10.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c1a40c486041006b135759f59189385da7c66d239bad897c994e18fd1d0c128f"},
+    {file = "coverage-7.10.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3beb76e20b28046989300c4ea81bf690df84ee98ade4dc0bbbf774a28eb98440"},
+    {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc265a7945e8d08da28999ad02b544963f813a00f3ed0a7a0ce4165fd77629f8"},
+    {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:47c91f32ba4ac46f1e224a7ebf3f98b4b24335bad16137737fe71a5961a0665c"},
+    {file = "coverage-7.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a108dd78ed185020f66f131c60078f3fae3f61646c28c8bb4edd3fa121fc7fc"},
+    {file = "coverage-7.10.1-cp310-cp310-win32.whl", hash = "sha256:7092cc82382e634075cc0255b0b69cb7cada7c1f249070ace6a95cb0f13548ef"},
+    {file = "coverage-7.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac0c5bba938879c2fc0bc6c1b47311b5ad1212a9dcb8b40fe2c8110239b7faed"},
+    {file = "coverage-7.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45e2f9d5b0b5c1977cb4feb5f594be60eb121106f8900348e29331f553a726f"},
+    {file = "coverage-7.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a7a4d74cb0f5e3334f9aa26af7016ddb94fb4bfa11b4a573d8e98ecba8c34f1"},
+    {file = "coverage-7.10.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4b0aab55ad60ead26159ff12b538c85fbab731a5e3411c642b46c3525863437"},
+    {file = "coverage-7.10.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dcc93488c9ebd229be6ee1f0d9aad90da97b33ad7e2912f5495804d78a3cd6b7"},
+    {file = "coverage-7.10.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa309df995d020f3438407081b51ff527171cca6772b33cf8f85344b8b4b8770"},
+    {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfb8b9d8855c8608f9747602a48ab525b1d320ecf0113994f6df23160af68262"},
+    {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:320d86da829b012982b414c7cdda65f5d358d63f764e0e4e54b33097646f39a3"},
+    {file = "coverage-7.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc60ddd483c556590da1d9482a4518292eec36dd0e1e8496966759a1f282bcd0"},
+    {file = "coverage-7.10.1-cp311-cp311-win32.whl", hash = "sha256:4fcfe294f95b44e4754da5b58be750396f2b1caca8f9a0e78588e3ef85f8b8be"},
+    {file = "coverage-7.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:efa23166da3fe2915f8ab452dde40319ac84dc357f635737174a08dbd912980c"},
+    {file = "coverage-7.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:d12b15a8c3759e2bb580ffa423ae54be4f184cf23beffcbd641f4fe6e1584293"},
+    {file = "coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4"},
+    {file = "coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e"},
+    {file = "coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4"},
+    {file = "coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a"},
+    {file = "coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe"},
+    {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386"},
+    {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6"},
+    {file = "coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f"},
+    {file = "coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca"},
+    {file = "coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3"},
+    {file = "coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4"},
+    {file = "coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39"},
+    {file = "coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7"},
+    {file = "coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892"},
+    {file = "coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7"},
+    {file = "coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994"},
+    {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0"},
+    {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7"},
+    {file = "coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7"},
+    {file = "coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7"},
+    {file = "coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e"},
+    {file = "coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4"},
+    {file = "coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72"},
+    {file = "coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af"},
+    {file = "coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7"},
+    {file = "coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759"},
+    {file = "coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324"},
+    {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53"},
+    {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f"},
+    {file = "coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd"},
+    {file = "coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c"},
+    {file = "coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18"},
+    {file = "coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4"},
+    {file = "coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c"},
+    {file = "coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e"},
+    {file = "coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b"},
+    {file = "coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41"},
+    {file = "coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f"},
+    {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1"},
+    {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2"},
+    {file = "coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4"},
+    {file = "coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613"},
+    {file = "coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e"},
+    {file = "coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652"},
+    {file = "coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894"},
+    {file = "coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5"},
+    {file = "coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2"},
+    {file = "coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb"},
+    {file = "coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b"},
+    {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea"},
+    {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd"},
+    {file = "coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d"},
+    {file = "coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47"},
+    {file = "coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651"},
+    {file = "coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab"},
+    {file = "coverage-7.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:57b6e8789cbefdef0667e4a94f8ffa40f9402cee5fc3b8e4274c894737890145"},
+    {file = "coverage-7.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85b22a9cce00cb03156334da67eb86e29f22b5e93876d0dd6a98646bb8a74e53"},
+    {file = "coverage-7.10.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97b6983a2f9c76d345ca395e843a049390b39652984e4a3b45b2442fa733992d"},
+    {file = "coverage-7.10.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ddf2a63b91399a1c2f88f40bc1705d5a7777e31c7e9eb27c602280f477b582ba"},
+    {file = "coverage-7.10.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47ab6dbbc31a14c5486420c2c1077fcae692097f673cf5be9ddbec8cdaa4cdbc"},
+    {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:21eb7d8b45d3700e7c2936a736f732794c47615a20f739f4133d5230a6512a88"},
+    {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:283005bb4d98ae33e45f2861cd2cde6a21878661c9ad49697f6951b358a0379b"},
+    {file = "coverage-7.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fefe31d61d02a8b2c419700b1fade9784a43d726de26495f243b663cd9fe1513"},
+    {file = "coverage-7.10.1-cp39-cp39-win32.whl", hash = "sha256:e8ab8e4c7ec7f8a55ac05b5b715a051d74eac62511c6d96d5bb79aaafa3b04cf"},
+    {file = "coverage-7.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:c36baa0ecde742784aa76c2b816466d3ea888d5297fda0edbac1bf48fa94688a"},
+    {file = "coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7"},
+    {file = "coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57"},
+]
+
+[package.extras]
+toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
+files = [
+    {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
+    {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+    {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
+    {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
+]
+
+[[package]]
+name = "mock"
+version = "5.2.0"
+description = "Rolling backport of unittest.mock for all Pythons"
+optional = false
+python-versions = ">=3.6"
+groups = ["dev"]
+files = [
+    {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"},
+    {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"},
+]
+
+[package.extras]
+build = ["blurb", "twine", "wheel"]
+docs = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+    {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
+    {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
+]
+
+[[package]]
+name = "parameterized"
+version = "0.9.0"
+description = "Parameterized testing with any Python test framework"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+    {file = "parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b"},
+    {file = "parameterized-0.9.0.tar.gz", hash = "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1"},
+]
+
+[package.extras]
+dev = ["jinja2"]
+
+[[package]]
+name = "pastel"
+version = "0.2.1"
+description = "Bring colors to your terminal."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["dev"]
+files = [
+    {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
+    {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+    {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+    {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["coverage", "pytest", "pytest-benchmark"]
+
+[[package]]
+name = "poethepoet"
+version = "0.24.4"
+description = "A task runner that works well with poetry."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+    {file = "poethepoet-0.24.4-py3-none-any.whl", hash = "sha256:fb4ea35d7f40fe2081ea917d2e4102e2310fda2cde78974050ca83896e229075"},
+    {file = "poethepoet-0.24.4.tar.gz", hash = "sha256:ff4220843a87c888cbcb5312c8905214701d0af60ac7271795baa8369b428fef"},
+]
+
+[package.dependencies]
+pastel = ">=0.2.1,<0.3.0"
+tomli = ">=1.2.2"
+
+[package.extras]
+poetry-plugin = ["poetry (>=1.0,<2.0)"]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+    {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
+    {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pyrefly"
+version = "0.26.1"
+description = "A fast Python type checker written in Rust"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+    {file = "pyrefly-0.26.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d81e737125d7a5f64bc378dc699f28b33182e803d25ec74f1b59007baacb6b70"},
+    {file = "pyrefly-0.26.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c0e0956065e4b45d026175adcb992472368a864ece889752b9397906d4d3ec11"},
+    {file = "pyrefly-0.26.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be24b8550a4890beea7e8843c9e4b469f70d204a73c0f38652f61d0f4edc6f23"},
+    {file = "pyrefly-0.26.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab3ed0cadd2de0f986925f72c7ca422b5a418d9416d614012565feb56e74d3a"},
+    {file = "pyrefly-0.26.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4885ec86b7d500a808adbd9f8541971e13fa3d3babe8bb48d9d20a5ece71a3f9"},
+    {file = "pyrefly-0.26.1-py3-none-win32.whl", hash = "sha256:1b997e4c201c2104008fc214256a3fe0de741fbf13d54c41fe5de6dd4f62f5bd"},
+    {file = "pyrefly-0.26.1-py3-none-win_amd64.whl", hash = "sha256:3856d5f2a37a2c0f59ddab2f0ab175d338f361ee1003c357cf8c2e409cd59b22"},
+    {file = "pyrefly-0.26.1-py3-none-win_arm64.whl", hash = "sha256:8a1a85e6d04649d1f0ade12e4424a0f08b27be963aba99958ee377e565b80714"},
+    {file = "pyrefly-0.26.1.tar.gz", hash = "sha256:f598b8fa29200e9cfa2c6d53d66bc4c7278b254192429d36bd0b49b32a553d82"},
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.1"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+    {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
+    {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
+iniconfig = ">=1"
+packaging = ">=20"
+pluggy = ">=1.5,<2"
+pygments = ">=2.7.2"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "ruff"
+version = "0.1.15"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+    {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
+    {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
+    {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
+    {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
+    {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
+    {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
+    {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
+    {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
+    {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
+    {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
+    {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
+    {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
+    {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
+    {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
+    {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
+    {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
+    {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
+]
+
+[[package]]
+name = "simplejson"
+version = "3.20.1"
+description = "Simple, fast, extensible JSON encoder/decoder for Python"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5"
+groups = ["main"]
+files = [
+    {file = "simplejson-3.20.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f5272b5866b259fe6c33c4a8c5073bf8b359c3c97b70c298a2f09a69b52c7c41"},
+    {file = "simplejson-3.20.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5c0de368f3052a59a1acf21f8b2dd28686a9e4eba2da7efae7ed9554cb31e7bc"},
+    {file = "simplejson-3.20.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0821871404a537fd0e22eba240c74c0467c28af6cc435903eca394cfc74a0497"},
+    {file = "simplejson-3.20.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c939a1e576bded47d7d03aa2afc2ae90b928b2cf1d9dc2070ceec51fd463f430"},
+    {file = "simplejson-3.20.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3c4f0a61cdc05550782ca4a2cdb311ea196c2e6be6b24a09bf71360ca8c3ca9b"},
+    {file = "simplejson-3.20.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6c21f5c026ca633cfffcb6bc1fac2e99f65cb2b24657d3bef21aed9916cc3bbf"},
+    {file = "simplejson-3.20.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:8d23b7f8d6b72319d6d55a0261089ff621ce87e54731c2d3de6a9bf7be5c028c"},
+    {file = "simplejson-3.20.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:cda5c32a98f392909088111ecec23f2b0d39346ceae1a0fea23ab2d1f84ec21d"},
+    {file = "simplejson-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e580aa65d5f6c3bf41b9b4afe74be5d5ddba9576701c107c772d936ea2b5043a"},
+    {file = "simplejson-3.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a586ce4f78cec11f22fe55c5bee0f067e803aab9bad3441afe2181693b5ebb5"},
+    {file = "simplejson-3.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74a1608f9e6e8c27a4008d70a54270868306d80ed48c9df7872f9f4b8ac87808"},
+    {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03db8cb64154189a92a7786209f24e391644f3a3fa335658be2df2af1960b8d8"},
+    {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea7e2b7d858f6fdfbf0fe3cb846d6bd8a45446865bc09960e51f3d473c2271b"},
+    {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e66712b17d8425bb7ff8968d4c7c7fd5a2dd7bd63728b28356223c000dd2f91f"},
+    {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2cc4f6486f9f515b62f5831ff1888886619b84fc837de68f26d919ba7bbdcbc"},
+    {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3c2df555ee4016148fa192e2b9cd9e60bc1d40769366134882685e90aee2a1e"},
+    {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78520f04b7548a5e476b5396c0847e066f1e0a4c0c5e920da1ad65e95f410b11"},
+    {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f4bd49ecde87b0fe9f55cc971449a32832bca9910821f7072bbfae1155eaa007"},
+    {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7eaae2b88eb5da53caaffdfa50e2e12022553949b88c0df4f9a9663609373f72"},
+    {file = "simplejson-3.20.1-cp310-cp310-win32.whl", hash = "sha256:e836fb88902799eac8debc2b642300748f4860a197fa3d9ea502112b6bb8e142"},
+    {file = "simplejson-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a19b552b212fc3b5b96fc5ce92333d4a9ac0a800803e1f17ebb16dac4be5"},
+    {file = "simplejson-3.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:325b8c107253d3217e89d7b50c71015b5b31e2433e6c5bf38967b2f80630a8ca"},
+    {file = "simplejson-3.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88a7baa8211089b9e58d78fbc1b0b322103f3f3d459ff16f03a36cece0d0fcf0"},
+    {file = "simplejson-3.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:299b1007b8101d50d95bc0db1bf5c38dc372e85b504cf77f596462083ee77e3f"},
+    {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ec618ed65caab48e81e3ed29586236a8e57daef792f1f3bb59504a7e98cd10"},
+    {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2cdead1d3197f0ff43373cf4730213420523ba48697743e135e26f3d179f38"},
+    {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3466d2839fdc83e1af42e07b90bc8ff361c4e8796cd66722a40ba14e458faddd"},
+    {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d492ed8e92f3a9f9be829205f44b1d0a89af6582f0cf43e0d129fa477b93fe0c"},
+    {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f924b485537b640dc69434565463fd6fc0c68c65a8c6e01a823dd26c9983cf79"},
+    {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e8eacf6a3491bf76ea91a8d46726368a6be0eb94993f60b8583550baae9439e"},
+    {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d04bf90b4cea7c22d8b19091633908f14a096caa301b24c2f3d85b5068fb8"},
+    {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:69dd28d4ce38390ea4aaf212902712c0fd1093dc4c1ff67e09687c3c3e15a749"},
+    {file = "simplejson-3.20.1-cp311-cp311-win32.whl", hash = "sha256:dfe7a9da5fd2a3499436cd350f31539e0a6ded5da6b5b3d422df016444d65e43"},
+    {file = "simplejson-3.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:896a6c04d7861d507d800da7642479c3547060bf97419d9ef73d98ced8258766"},
+    {file = "simplejson-3.20.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f31c4a3a7ab18467ee73a27f3e59158255d1520f3aad74315edde7a940f1be23"},
+    {file = "simplejson-3.20.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:884e6183d16b725e113b83a6fc0230152ab6627d4d36cb05c89c2c5bccfa7bc6"},
+    {file = "simplejson-3.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03d7a426e416fe0d3337115f04164cd9427eb4256e843a6b8751cacf70abc832"},
+    {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:000602141d0bddfcff60ea6a6e97d5e10c9db6b17fd2d6c66199fa481b6214bb"},
+    {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af8377a8af78226e82e3a4349efdde59ffa421ae88be67e18cef915e4023a595"},
+    {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15c7de4c88ab2fbcb8781a3b982ef883696736134e20b1210bca43fb42ff1acf"},
+    {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455a882ff3f97d810709f7b620007d4e0aca8da71d06fc5c18ba11daf1c4df49"},
+    {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc0f523ce923e7f38eb67804bc80e0a028c76d7868500aa3f59225574b5d0453"},
+    {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76461ec929282dde4a08061071a47281ad939d0202dc4e63cdd135844e162fbc"},
+    {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19c2da8c043607bde4d4ef3a6b633e668a7d2e3d56f40a476a74c5ea71949f"},
+    {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2578bedaedf6294415197b267d4ef678fea336dd78ee2a6d2f4b028e9d07be3"},
+    {file = "simplejson-3.20.1-cp312-cp312-win32.whl", hash = "sha256:339f407373325a36b7fd744b688ba5bae0666b5d340ec6d98aebc3014bf3d8ea"},
+    {file = "simplejson-3.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:627d4486a1ea7edf1f66bb044ace1ce6b4c1698acd1b05353c97ba4864ea2e17"},
+    {file = "simplejson-3.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:71e849e7ceb2178344998cbe5ade101f1b329460243c79c27fbfc51c0447a7c3"},
+    {file = "simplejson-3.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b63fdbab29dc3868d6f009a59797cefaba315fd43cd32ddd998ee1da28e50e29"},
+    {file = "simplejson-3.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1190f9a3ce644fd50ec277ac4a98c0517f532cfebdcc4bd975c0979a9f05e1fb"},
+    {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1336ba7bcb722ad487cd265701ff0583c0bb6de638364ca947bb84ecc0015d1"},
+    {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e975aac6a5acd8b510eba58d5591e10a03e3d16c1cf8a8624ca177491f7230f0"},
+    {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a6dd11ee282937ad749da6f3b8d87952ad585b26e5edfa10da3ae2536c73078"},
+    {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab980fcc446ab87ea0879edad41a5c28f2d86020014eb035cf5161e8de4474c6"},
+    {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f5aee2a4cb6b146bd17333ac623610f069f34e8f31d2f4f0c1a2186e50c594f0"},
+    {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:652d8eecbb9a3b6461b21ec7cf11fd0acbab144e45e600c817ecf18e4580b99e"},
+    {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c09948f1a486a89251ee3a67c9f8c969b379f6ffff1a6064b41fea3bce0a112"},
+    {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cbbd7b215ad4fc6f058b5dd4c26ee5c59f72e031dfda3ac183d7968a99e4ca3a"},
+    {file = "simplejson-3.20.1-cp313-cp313-win32.whl", hash = "sha256:ae81e482476eaa088ef9d0120ae5345de924f23962c0c1e20abbdff597631f87"},
+    {file = "simplejson-3.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:1b9fd15853b90aec3b1739f4471efbf1ac05066a2c7041bf8db821bb73cd2ddc"},
+    {file = "simplejson-3.20.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c7edf279c1376f28bf41e916c015a2a08896597869d57d621f55b6a30c7e1e6d"},
+    {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9202b9de38f12e99a40addd1a8d508a13c77f46d87ab1f9095f154667f4fe81"},
+    {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:391345b4157cc4e120027e013bd35c45e2c191e2bf48b8913af488cdc3b9243c"},
+    {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6fdcc9debb711ddd2ad6d69f9386a3d9e8e253234bbb30513e0a7caa9510c51"},
+    {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9daf8cdc7ee8a9e9f7a3b313ba0a003391857e90d0e82fbcd4d614aa05cb7c3b"},
+    {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:c02f4868a3a46ffe284a51a88d134dc96feff6079a7115164885331a1ba8ed9f"},
+    {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:3d7310172d5340febd258cb147f46aae30ad57c445f4d7e1ae8461c10aaf43b0"},
+    {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4762e05577955312a4c6802f58dd02e040cc79ae59cda510aa1564d84449c102"},
+    {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:8bb98fdf318c05aefd08a92583bd6ee148e93c6756fb1befb7b2d5f27824be78"},
+    {file = "simplejson-3.20.1-cp36-cp36m-win32.whl", hash = "sha256:9a74e70818818981294b8e6956ce3496c5e1bd4726ac864fae473197671f7b85"},
+    {file = "simplejson-3.20.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e041add470e8f8535cc05509485eb7205729a84441f03b25cde80ad48823792e"},
+    {file = "simplejson-3.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e9d73f46119240e4f4f07868241749d67d09873f40cb968d639aa9ccc488b86"},
+    {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae6e637dc24f8fee332ed23dd070e81394138e42cd4fd9d0923e5045ba122e27"},
+    {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efd3bc6c6b17e3d4620eb6be5196f0d1c08b6ce7c3101fa8e292b79e0908944b"},
+    {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87fc623d457173a0213bc9ca4e346b83c9d443f63ed5cca847fb0cacea3cfc95"},
+    {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec6a1e0a7aff76f0e008bebfa950188b9c50b58c1885d898145f48fc8e189a56"},
+    {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:9c079606f461a6e950099167e21e13985147c8a24be8eea66c9ad68f73fad744"},
+    {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:9faceb68fba27ef17eda306e4cd97a7b4b14fdadca5fbb15790ba8b26ebeec0c"},
+    {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:7ceed598e4bacbf5133fe7a418f7991bb2df0683f3ac11fbf9e36a2bc7aa4b85"},
+    {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ede69c765e9901861ad7c6139023b7b7d5807c48a2539d817b4ab40018002d5f"},
+    {file = "simplejson-3.20.1-cp37-cp37m-win32.whl", hash = "sha256:d8853c269a4c5146ddca4aa7c70e631795e9d11239d5fedb1c6bbc91ffdebcac"},
+    {file = "simplejson-3.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ed6a17fd397f0e2b3ad668fc9e19253ed2e3875ad9086bd7f795c29a3223f4a1"},
+    {file = "simplejson-3.20.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7551682b60bba3a9e2780742e101cf0a64250e76de7d09b1c4b0c8a7c7cc6834"},
+    {file = "simplejson-3.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd9577ec1c8c3a43040e3787711e4c257c70035b7551a21854b5dec88dad09e1"},
+    {file = "simplejson-3.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8e197e4cf6d42c2c57e7c52cd7c1e7b3e37c5911df1314fb393320131e2101"},
+    {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bd09c8c75666e7f62a33d2f1fb57f81da1fcbb19a9fe7d7910b5756e1dd6048"},
+    {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bd6bfe5678d73fbd5328eea6a35216503796428fc47f1237432522febaf3a0c"},
+    {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b75d448fd0ceb2e7c90e72bb82c41f8462550d48529980bc0bab1d2495bfbb"},
+    {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7e15b716d09f318c8cda3e20f82fae81684ce3d3acd1d7770fa3007df1769de"},
+    {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3e7963197d958fcf9e98b212b80977d56c022384621ff463d98afc3b6b1ce7e8"},
+    {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2e671dd62051129185d3a9a92c60101f56cbc174854a1a3dfb69114ebd9e1699"},
+    {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e25b2a0c396f3b84fb89573d07b0e1846ed563eb364f2ea8230ca92b8a8cb786"},
+    {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:489c3a43116082bad56795215786313832ba3991cca1f55838e52a553f451ab6"},
+    {file = "simplejson-3.20.1-cp38-cp38-win32.whl", hash = "sha256:4a92e948bad8df7fa900ba2ba0667a98303f3db206cbaac574935c332838208e"},
+    {file = "simplejson-3.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:49d059b8363327eee3c94799dd96782314b2dbd7bcc293b4ad48db69d6f4d362"},
+    {file = "simplejson-3.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a8011f1dd1d676befcd4d675ebdbfdbbefd3bf350052b956ba8c699fca7d8cef"},
+    {file = "simplejson-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e91703a4c5fec53e36875ae426ad785f4120bd1d93b65bed4752eeccd1789e0c"},
+    {file = "simplejson-3.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e39eaa57c7757daa25bcd21f976c46be443b73dd6c3da47fe5ce7b7048ccefe2"},
+    {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceab2ce2acdc7fbaa433a93006758db6ba9a659e80c4faa13b80b9d2318e9b17"},
+    {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d4f320c33277a5b715db5bf5b10dae10c19076bd6d66c2843e04bd12d1f1ea5"},
+    {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b6436c48e64378fa844d8c9e58a5ed0352bbcfd4028369a9b46679b7ab79d2d"},
+    {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e18345c8dda5d699be8166b61f9d80aaee4545b709f1363f60813dc032dac53"},
+    {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:90b573693d1526bed576f6817e2a492eaaef68f088b57d7a9e83d122bbb49e51"},
+    {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:272cc767826e924a6bd369ea3dbf18e166ded29059c7a4d64d21a9a22424b5b5"},
+    {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:51b41f284d603c4380732d7d619f8b34bd04bc4aa0ed0ed5f4ffd0539b14da44"},
+    {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e6697a3067d281f01de0fe96fc7cba4ea870d96d7deb7bfcf85186d74456503"},
+    {file = "simplejson-3.20.1-cp39-cp39-win32.whl", hash = "sha256:6dd3a1d5aca87bf947f3339b0f8e8e329f1badf548bdbff37fac63c17936da8e"},
+    {file = "simplejson-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:463f1fca8fbf23d088e5850fdd0dd4d5faea8900a9f9680270bd98fd649814ca"},
+    {file = "simplejson-3.20.1-py3-none-any.whl", hash = "sha256:8a6c1bbac39fa4a79f83cbf1df6ccd8ff7069582a9fd8db1e52cea073bc2c697"},
+    {file = "simplejson-3.20.1.tar.gz", hash = "sha256:e64139b4ec4f1f24c142ff7dcafe55a22b811a74d86d66560c8815687143037d"},
+]
+
+[[package]]
+name = "snapshot-restore-py"
+version = "1.0.0"
+description = "Runtime Hooks for AWS Lambda SnapStart - Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+    {file = "snapshot-restore-py-1.0.0.tar.gz", hash = "sha256:4d27f82fb6f09968f422501e9c3c2dea48a46cd19dc798eb7d6cbc57523c8004"},
+    {file = "snapshot_restore_py-1.0.0-py3-none-any.whl", hash = "sha256:38f99e696793790f54658e71c68c7a8a40cea877c81232b5052383b1301aceba"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+    {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+    {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+    {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+    {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+    {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+    {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+    {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+    {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+    {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+    {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+    {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+    {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+    {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+    {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+    {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+    {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+    {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+    {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+    {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+    {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.1"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
+files = [
+    {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"},
+    {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"},
+]
+
+[metadata]
+lock-version = "2.1"
+python-versions = ">=3.9"
+content-hash = "fb2951082007675c7b94eaf035984e23a6651bf6028874b515bdd8baa0ef98c1"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..531f16d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,70 @@
+[tool.poetry]
+name = "awslambdaric"
+version = "3.0.2"
+description = "AWS Lambda Runtime Interface Client for Python"
+authors = ["Amazon Web Services"]
+license = "Apache-2.0"
+readme = "README.md"
+packages = [{ include = "awslambdaric" }]
+
+[tool.poetry.dependencies]
+python = ">=3.9"
+simplejson = ">=3.20.1"
+snapshot-restore-py = ">=1.0.0"
+
+[tool.poetry.group.dev.dependencies]
+coverage = ">=4.4.0"
+pytest = ">=3.0.7"
+mock = ">=2.0.0"
+parameterized = ">=0.9.0"
+ruff = "^0.1.0"
+poethepoet = "^0.24.0"
+pyrefly = "^0.26.0"
+
+[tool.poe.tasks]
+init = "poetry install"
+test = "pytest tests"
+lint = "ruff check awslambdaric/ tests/"
+format = "ruff format awslambdaric/ tests/"
+clean = { shell = "rm -rf build dist *.egg-info" }
+build = ["clean", { cmd = "python setup.py sdist bdist_wheel" }]
+build-container = { shell = "./scripts/build-container.sh" }
+test-rie = { shell = "./scripts/test-rie.sh" }
+check-security = "ruff check --select S awslambdaric/"
+check-format = "ruff format --check awslambdaric/ tests/"
+check-docstr = "ruff check --select D --ignore D105 awslambdaric/"
+type-check = "pyrefly check"
+test-smoke = { shell = "CODEBUILD_IMAGE_TAG=codebuild-agent tests/integration/codebuild-local/test_one.sh tests/integration/codebuild/buildspec.os.alpine.yml alpine 3.15 3.9" }
+test-integ = { shell = "CODEBUILD_IMAGE_TAG=codebuild-agent DISTRO=\"$DISTRO\" tests/integration/codebuild-local/test_all.sh tests/integration/codebuild/." }
+setup-codebuild-agent = { shell = "docker build -t codebuild-agent - < tests/integration/codebuild-local/Dockerfile.agent" }
+
+# Composite tasks
+dev = ["init", "test"]
+pr = ["init", "check-format", "check-security", "dev"]
+
+[build-system]
+requires = ["poetry-core>=2.0.0,<3.0.0", "setuptools>=68", "wheel"]
+build-backend = "poetry.core.masonry.api"
+
+# Ruff configuration
+[tool.ruff]
+target-version = "py39"
+line-length = 88
+
+[tool.ruff.lint]
+select = [
+    "E",   # pycodestyle errors
+    "W",   # pycodestyle warnings  
+    "F",   # pyflakes
+    "I",   # isort
+    "UP",  # pyupgrade
+    "C90", # mccabe complexity
+    "S",   # flake8-bandit security checks
+]
+
+[tool.pyrefly]
+project-includes = ["**/*"]
+project-excludes = ["**/*venv/**/*"]
+
+[tool.ruff.format]
+quote-style = "double"
diff --git a/requirements/base.txt b/requirements/base.txt
deleted file mode 100644
index 4bb251e..0000000
--- a/requirements/base.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-simplejson>=3.20.1
-snapshot-restore-py>=1.0.0
diff --git a/requirements/dev.txt b/requirements/dev.txt
deleted file mode 100644
index 68377ce..0000000
--- a/requirements/dev.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-coverage>=4.4.0
-flake8>=3.3.0
-tox>=2.2.1
-pytest-cov>=2.4.0
-pylint>=1.7.2
-black>=20.8b0
-bandit>=1.6.2
-
-# Test requirements
-pytest>=3.0.7
-mock>=2.0.0
-parameterized>=0.9.0
\ No newline at end of file
diff --git a/scripts/build-container.sh b/scripts/build-container.sh
new file mode 100755
index 0000000..2b7c9c6
--- /dev/null
+++ b/scripts/build-container.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -euo pipefail
+
+TAG=${TAG:-latest}
+PYTHON_VERSION=${PYTHON_VERSION:-3.9}
+
+echo "Building awslambdaric wheel for Python ${PYTHON_VERSION}..."
+
+echo "Building wheel in container..."
+docker build \
+    --build-arg PYTHON_VERSION="${PYTHON_VERSION}" \
+    -f Dockerfile.build \
+    -t "awslambdaric-builder:${TAG}" \
+    .
+
+echo "Extracting built wheel..."
+mkdir -p build-artifacts
+
+docker run --rm -v $(pwd)/build-artifacts:/output awslambdaric-builder:${TAG} /bin/sh -c "
+    cp /home/build/dist/*.whl /output/
+    echo 'Wheel copied to build-artifacts/'
+    ls -la /output/
+"
+
+echo "Build complete! Wheel is available in build-artifacts/"
\ No newline at end of file
diff --git a/scripts/test-rie.sh b/scripts/test-rie.sh
new file mode 100755
index 0000000..4544870
--- /dev/null
+++ b/scripts/test-rie.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -euo pipefail
+
+DOCKERFILE="Dockerfile.rie"
+
+echo "Starting RIE test setup..."
+
+# Check if build artifacts exist
+if [ ! -d "build-artifacts" ] || [ -z "$(ls -A build-artifacts/*.whl 2>/dev/null)" ]; then
+    echo "No build artifacts found. Please run 'poetry run poe build-container' first."
+    exit 1
+fi
+
+echo "Building test Docker image..."
+docker build \
+    -f "${DOCKERFILE}" \
+    -t awslambdaric-rie-test .
+
+echo "Starting test container on port 9000..."
+echo ""
+echo "Test with:"
+echo "curl -XPOST \"http://localhost:9000/2015-03-31/functions/function/invocations\" -d '{\"message\":\"test\"}'"
+echo ""
+
+docker run -p 9000:8080 \
+    --rm awslambdaric-rie-test
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 2bf28ef..3ce5c62 100644
--- a/setup.py
+++ b/setup.py
@@ -1,98 +1,44 @@
-"""
-Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-"""
-
-import io
-import os
-import platform
-from subprocess import check_call, check_output
-from setuptools import Extension, find_packages, setup
-from awslambdaric import __version__
-
-
-def get_curl_extra_linker_flags():
-    # We do not want to build the dependencies during packaging
-    if platform.system() != "Linux" or os.getenv("BUILD") == "true":
-        return []
-
-    # Build the dependencies
-    check_call(["./scripts/preinstall.sh"])
-
-    # call curl-config to get the required linker flags
-    cmd = ["./deps/artifacts/bin/curl-config", "--static-libs"]
-    curl_config = check_output(cmd).decode("utf-8").replace("\n", "")
-
-    # It is expected that the result of the curl-config call is similar to
-    # "/tmp/pip-req-build-g9dlug7g/deps/artifacts/lib/libcurl.a -lidn2"
-    # we want to return just the extra flags
-    flags = curl_config.split(" ")[1:]
-
-    return flags
-
-
-def get_runtime_client_extension():
-    if platform.system() != "Linux" and os.getenv("BUILD") != "true":
-        print(
-            "The native runtime_client only builds on Linux. Skipping its compilation."
-        )
-        return []
-
-    runtime_client = Extension(
-        "runtime_client",
-        ["awslambdaric/runtime_client.cpp"],
-        extra_compile_args=["--std=c++11"],
-        library_dirs=["deps/artifacts/lib", "deps/artifacts/lib64"],
-        libraries=["aws-lambda-runtime", "curl"],
-        extra_link_args=get_curl_extra_linker_flags(),
-        include_dirs=["deps/artifacts/include"],
-    )
-
-    return [runtime_client]
-
-
-def read(*filenames, **kwargs):
-    encoding = kwargs.get("encoding", "utf-8")
-    sep = kwargs.get("sep", os.linesep)
-    buf = []
-    for filename in filenames:
-        with io.open(filename, encoding=encoding) as f:
-            buf.append(f.read())
-    return sep.join(buf)
-
-
-def read_requirements(req="base.txt"):
-    content = read(os.path.join("requirements", req))
-    return [
-        line for line in content.split(os.linesep) if not line.strip().startswith("#")
-    ]
-
+import sys
+from setuptools import setup, find_packages
+
+if sys.version_info >= (3, 11):
+    import tomllib
+else:
+    try:
+        import tomli as tomllib
+    except ImportError:
+        import subprocess
+        subprocess.check_call([sys.executable, "-m", "pip", "install", "tomli"])
+        import tomli as tomllib
+
+def get_metadata():
+    with open("pyproject.toml", "rb") as f:
+        pyproject = tomllib.load(f)
+    
+    poetry_config = pyproject["tool"]["poetry"]
+    return {
+        "name": poetry_config["name"],
+        "version": poetry_config["version"],
+        "description": poetry_config["description"],
+        "author": poetry_config["authors"][0] if poetry_config["authors"] else "",
+        "license": poetry_config["license"],
+        "python_requires": poetry_config["dependencies"]["python"],
+        "install_requires": [
+            f"{pkg}{version}" if isinstance(version, str) and not version.startswith("^") and not version.startswith("~") else f"{pkg}>={version[1:] if isinstance(version, str) else version}"
+            for pkg, version in poetry_config["dependencies"].items()
+            if pkg != "python" and not isinstance(version, dict)
+        ]
+    }
+
+metadata = get_metadata()
 
 setup(
-    name="awslambdaric",
-    version=__version__,
-    author="Amazon Web Services",
-    description="AWS Lambda Runtime Interface Client for Python",
-    long_description=read("README.md"),
-    long_description_content_type="text/markdown",
-    url="https://github.com/aws/aws-lambda-python-runtime-interface-client",
-    packages=find_packages(
-        exclude=("tests", "tests.*", "docs", "examples", "versions")
-    ),
-    install_requires=read_requirements("base.txt"),
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Programming Language :: Python :: 3.13",
-        "License :: OSI Approved :: Apache Software License",
-        "Operating System :: OS Independent",
-    ],
-    python_requires=">=3.9",
-    ext_modules=get_runtime_client_extension(),
-    test_suite="tests",
+    name=metadata["name"],
+    version=metadata["version"],
+    description=metadata["description"],
+    author=metadata["author"],
+    license=metadata["license"],
+    packages=find_packages(),
+    python_requires=metadata["python_requires"],
+    install_requires=metadata["install_requires"],
 )
diff --git a/tests/integration/docker/Dockerfile.echo.alpine b/tests/integration/docker/Dockerfile.echo.alpine
index f6790fa..e4d6010 100644
--- a/tests/integration/docker/Dockerfile.echo.alpine
+++ b/tests/integration/docker/Dockerfile.echo.alpine
@@ -22,7 +22,8 @@ RUN apk add --no-cache \
     elfutils-dev \
     make \
     cmake \
-    libcurl
+    libcurl \
+    curl
 
 # Include global args in this stage of the build
 ARG RIC_BUILD_DIR="/home/build/"
@@ -32,8 +33,13 @@ RUN mkdir -p ${RIC_BUILD_DIR}
 WORKDIR ${RIC_BUILD_DIR}
 COPY . .
 RUN pip3 install setuptools
-RUN make init build test && \
-    mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz
+# Install Poetry
+RUN curl -sSL https://install.python-poetry.org | python3 - && \
+    ln -s /root/.local/bin/poetry /usr/local/bin/poetry
+
+RUN poetry install && poetry run poe build
+
+RUN ls -la ./dist/
 
 # Include global args in this stage of the build
 ARG FUNCTION_DIR="/home/app/"
@@ -45,7 +51,7 @@ COPY tests/integration/test-handlers/echo/* ${FUNCTION_DIR}
 # Install the function's dependencies
 WORKDIR ${FUNCTION_DIR}
 RUN python${RUNTIME_VERSION} -m pip install \
-        ${RIC_BUILD_DIR}/dist/awslambdaric-test.tar.gz \
+        ${RIC_BUILD_DIR}/dist/*.whl \
         --target ${FUNCTION_DIR}
 
 
diff --git a/tests/integration/docker/Dockerfile.echo.amazonlinux2 b/tests/integration/docker/Dockerfile.echo.amazonlinux2
index be05aa1..1c80890 100644
--- a/tests/integration/docker/Dockerfile.echo.amazonlinux2
+++ b/tests/integration/docker/Dockerfile.echo.amazonlinux2
@@ -22,7 +22,8 @@ RUN yum install -y \
   openssl11-devel \
   bzip2-devel \
   libffi-devel \
-  sqlite-devel
+  sqlite-devel \
+  curl
 
 RUN RUNTIME_LATEST_VERSION=${RUNTIME_VERSION}.$(curl -s https://www.python.org/ftp/python/ | \
                                                 grep -oE "href=\"$(echo ${RUNTIME_VERSION} | sed "s/\\./\\\./g")\.[0-9]+" | \
@@ -72,6 +73,9 @@ RUN wget --quiet -O cmake-install https://github.com/Kitware/CMake/releases/down
 ENV PATH=/usr/local/bin:$PATH
 ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
 
+# Create symlinks for consistent python usage
+RUN ln -sf /usr/local/bin/python${RUNTIME_VERSION} /usr/local/bin/python3 && \
+    ln -sf /usr/local/bin/python${RUNTIME_VERSION} /usr/local/bin/python
 
 # Include global args in this stage of the build
 ARG RIC_BUILD_DIR="/home/build/"
@@ -81,8 +85,15 @@ RUN mkdir -p ${RIC_BUILD_DIR}
 WORKDIR ${RIC_BUILD_DIR}
 COPY . .
 
-RUN make init build test && \
-    mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz
+# Install wheel and poetry using the correct python version
+RUN /usr/local/bin/python${RUNTIME_VERSION} -m pip install wheel poetry
+
+# Configure poetry to use the correct python version
+RUN poetry config virtualenvs.create false
+
+RUN poetry install && poetry run poe build
+
+RUN ls -la ./dist/
 
 # Include global args in this stage of the build
 ARG FUNCTION_DIR="/home/app/"
@@ -90,15 +101,12 @@ ARG FUNCTION_DIR="/home/app/"
 RUN mkdir -p ${FUNCTION_DIR}
 # Copy function code
 COPY tests/integration/test-handlers/echo/* ${FUNCTION_DIR}
-# Copy Runtime Interface Client .tgz
-RUN cp ./dist/awslambdaric-test.tar.gz ${FUNCTION_DIR}/awslambdaric-test.tar.gz
 
 # Install the function's dependencies
 WORKDIR ${FUNCTION_DIR}
 RUN python${RUNTIME_VERSION} -m pip install \
-        awslambdaric-test.tar.gz \
-        --target ${FUNCTION_DIR} && \
-    rm awslambdaric-test.tar.gz
+        ${RIC_BUILD_DIR}/dist/*.whl \
+        --target ${FUNCTION_DIR}
 
 
 # Stage 4 - final runtime interface client image
diff --git a/tests/integration/docker/Dockerfile.echo.amazonlinux2023 b/tests/integration/docker/Dockerfile.echo.amazonlinux2023
index 16bbc79..283f8fa 100644
--- a/tests/integration/docker/Dockerfile.echo.amazonlinux2023
+++ b/tests/integration/docker/Dockerfile.echo.amazonlinux2023
@@ -6,7 +6,7 @@ FROM public.ecr.aws/amazonlinux/amazonlinux:${DISTRO_VERSION} AS python-amazonli
 ARG RUNTIME_VERSION
 
 # Install apt dependencies
-RUN dnf install -y \
+RUN dnf install -y --allowerasing \
   gcc \
   gcc-c++ \
   tar \
@@ -22,7 +22,8 @@ RUN dnf install -y \
   openssl-devel \
   bzip2-devel \
   libffi-devel \
-  sqlite-devel
+  sqlite-devel \
+  curl
 
 RUN RUNTIME_LATEST_VERSION=${RUNTIME_VERSION}.$(curl -s https://www.python.org/ftp/python/ | \
                                                 grep -oE "href=\"$(echo ${RUNTIME_VERSION} | sed "s/\\./\\\./g")\.[0-9]+" | \
@@ -73,6 +74,9 @@ RUN wget --quiet -O cmake-install https://github.com/Kitware/CMake/releases/down
 ENV PATH=/usr/local/bin:$PATH
 ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
 
+# Create symlinks for consistent python usage
+RUN ln -sf /usr/local/bin/python${RUNTIME_VERSION} /usr/local/bin/python3 && \
+    ln -sf /usr/local/bin/python${RUNTIME_VERSION} /usr/local/bin/python
 
 # Include global args in this stage of the build
 ARG RIC_BUILD_DIR="/home/build/"
@@ -85,15 +89,14 @@ COPY . .
 # distutils no longer available in python3.12 and later
 #   https://docs.python.org/3/whatsnew/3.12.html
 #   https://peps.python.org/pep-0632/
-RUN pip3 install setuptools
-RUN make init build 
+RUN /usr/local/bin/python${RUNTIME_VERSION} -m pip install setuptools wheel poetry
 
-RUN mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz
-RUN python${RUNTIME_VERSION} -m pip install \
-      ./dist/awslambdaric-test.tar.gz \
-      --target ${RIC_BUILD_DIR}
+# Configure poetry to use the correct python version
+RUN poetry config virtualenvs.create false
+
+RUN poetry install && poetry run poe build
 
-RUN make test
+RUN ls -la ./dist/
 
 # Include global args in this stage of the build
 ARG FUNCTION_DIR="/home/app/"
@@ -101,15 +104,12 @@ ARG FUNCTION_DIR="/home/app/"
 RUN mkdir -p ${FUNCTION_DIR}
 # Copy function code
 COPY tests/integration/test-handlers/echo/* ${FUNCTION_DIR}
-# Copy Runtime Interface Client .tgz
-RUN cp ./dist/awslambdaric-test.tar.gz ${FUNCTION_DIR}/awslambdaric-test.tar.gz
 
 # Install the function's dependencies
 WORKDIR ${FUNCTION_DIR}
 RUN python${RUNTIME_VERSION} -m pip install \
-        awslambdaric-test.tar.gz \
-        --target ${FUNCTION_DIR} && \
-    rm awslambdaric-test.tar.gz
+        ${RIC_BUILD_DIR}/dist/*.whl \
+        --target ${FUNCTION_DIR}
 
 
 # Stage 4 - final runtime interface client image
diff --git a/tests/integration/docker/Dockerfile.echo.debian b/tests/integration/docker/Dockerfile.echo.debian
index bf0f4fa..2749d9b 100644
--- a/tests/integration/docker/Dockerfile.echo.debian
+++ b/tests/integration/docker/Dockerfile.echo.debian
@@ -10,7 +10,8 @@ RUN apt-get update && \
         g++ \
         make \
         cmake \
-        libcurl4-openssl-dev
+        libcurl4-openssl-dev \
+        curl
 
 # Include global args in this stage of the build
 ARG RIC_BUILD_DIR="/home/build/"
@@ -20,8 +21,13 @@ RUN mkdir -p ${RIC_BUILD_DIR}
 WORKDIR ${RIC_BUILD_DIR}
 COPY . .
 RUN pip3 install setuptools
-RUN make init build test && \
-    mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz
+# Install Poetry
+RUN curl -sSL https://install.python-poetry.org | python3 - && \
+    ln -s /root/.local/bin/poetry /usr/local/bin/poetry
+
+RUN poetry install && poetry run poe build
+
+RUN ls -la ./dist/
 
 # Include global args in this stage of the build
 ARG FUNCTION_DIR="/home/app/"
@@ -29,15 +35,12 @@ ARG FUNCTION_DIR="/home/app/"
 RUN mkdir -p ${FUNCTION_DIR}
 # Copy function code
 COPY tests/integration/test-handlers/echo/* ${FUNCTION_DIR}
-# Copy Runtime Interface Client .tgz
-RUN cp ./dist/awslambdaric-test.tar.gz ${FUNCTION_DIR}/awslambdaric-test.tar.gz
 
 # Install the function's dependencies
 WORKDIR ${FUNCTION_DIR}
 RUN pip install \
-        awslambdaric-test.tar.gz \
-        --target ${FUNCTION_DIR} && \
-    rm awslambdaric-test.tar.gz
+        ${RIC_BUILD_DIR}/dist/*.whl \
+        --target ${FUNCTION_DIR}
 
 
 # Stage 2 - final runtime interface client image
diff --git a/tests/integration/docker/Dockerfile.echo.ubuntu b/tests/integration/docker/Dockerfile.echo.ubuntu
index 0ce3000..c9d2848 100644
--- a/tests/integration/docker/Dockerfile.echo.ubuntu
+++ b/tests/integration/docker/Dockerfile.echo.ubuntu
@@ -54,8 +54,11 @@ WORKDIR ${RIC_BUILD_DIR}
 COPY . .
 RUN . /home/venv/bin/activate && \
     pip install setuptools && \
-    make init build test && \
-    mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz
+    curl -sSL https://install.python-poetry.org | python3 - && \
+    ln -s /root/.local/bin/poetry /usr/local/bin/poetry && \
+    poetry install && poetry run poe build
+
+RUN ls -la ./dist/
 
 
 
@@ -68,7 +71,7 @@ COPY tests/integration/test-handlers/echo/* ${FUNCTION_DIR}
 # Install the function's dependencies
 WORKDIR ${FUNCTION_DIR}
 RUN . /home/venv/bin/activate && \
-    pip install ${RIC_BUILD_DIR}/dist/awslambdaric-test.tar.gz --target ${FUNCTION_DIR}
+    pip install ${RIC_BUILD_DIR}/dist/*.whl --target ${FUNCTION_DIR}
 
 
 
diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py
index 33afb1c..d08d900 100644
--- a/tests/test_bootstrap.py
+++ b/tests/test_bootstrap.py
@@ -924,10 +924,13 @@ def test_binary_with_application_json(self):
         self.lambda_runtime.post_invocation_error.assert_called_once()
 
         (
-            invoke_id,
-            error_result,
-            xray_fault,
-        ), _ = self.lambda_runtime.post_invocation_error.call_args
+            (
+                invoke_id,
+                error_result,
+                xray_fault,
+            ),
+            _,
+        ) = self.lambda_runtime.post_invocation_error.call_args
         error_dict = json.loads(error_result)
 
         self.assertEqual("invoke-id", invoke_id)