Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/code-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
name: Code Standard Checks
runs-on: ubuntu-latest
permissions:
contents: read
contents: read
defaults:
run:
working-directory: aws-lambda-calculator
Expand Down Expand Up @@ -48,13 +48,14 @@ jobs:
python -m poetry run ruff format --check
python -m poetry run ruff check

# - name: Types cheking (mypy)
# run: python -m poetry run mypy --check-untyped-defs .
- name: Types checking (mypy)
run: python -m poetry run mypy . --show-error-codes --ignore-missing-imports

- name: Notify failure if checks failed
if: ${{ failure() }}
env:
msg: Some of the code standard checks have failed. Please run `just checks`
msg:
Some of the code standard checks have failed. Please run `just checks`
locally before pushing your changes.
run: |-
echo "::error::${msg}"
15 changes: 15 additions & 0 deletions aws-lambda-calculator/mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[mypy]
# Type checking configuration
python_version = 3.13
show_error_codes = True
ignore_missing_imports = True

# Exclude test files
exclude = tests/

# Exclude utility files that have complex typing issues
[mypy-aws_lambda_calculator.pricing_scraper]
ignore_errors = True

[mypy-aws_lambda_calculator.screenshotter]
ignore_errors = True
303 changes: 258 additions & 45 deletions aws-lambda-calculator/poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion aws-lambda-calculator/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ types-colorama = "^0.4.15.20240311"
python-dotenv = "^1.1.0"
requests = "^2.32.4"
boto3 = "^1.39.14"
pydantic = "^2.12.3"

[project.urls]
homepage = "https://github.com/zMynx/aws-lambda-calculator"
Expand All @@ -48,9 +49,10 @@ relative_files = true
[tool.poetry.group.dev.dependencies]
ruff = "^0.9.7"
pytest = "^8.3.4"
mypy = "^1.15.0"
pytest-cov = "^6.1.0"
playwright = "^1.52.0"
mypy = "^1.18.2"
types-requests = "^2.32.4.20250913"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
Expand Down
51 changes: 38 additions & 13 deletions aws-lambda-calculator/src/aws_lambda_calculator/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from dotenv import load_dotenv
import logging
import json
from .models import CalculationRequest, CalculationResult
from typing import Literal, Any

# Load environment variables from .env file
load_dotenv()
Expand All @@ -10,7 +12,7 @@
steps = []


def open_json_file(region: str) -> dict:
def open_json_file(region: str) -> dict[str, Any]:
"""Open a JSON file containing cost factors for a specific region."""
base_dir = os.path.dirname(__file__)
file_path = os.path.join(base_dir, "jsons", f"{region}.json")
Expand Down Expand Up @@ -80,7 +82,7 @@
raise ValueError(f"Unknown request unit: {request_unit}")


def unit_conversion_memory(memory: int, memory_unit: str) -> float:
def unit_conversion_memory(memory: float, memory_unit: str) -> float:

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.

Copilot Autofix

AI 24 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

"""
@brief Convert memory based on the unit provided.
@param memory: amount of memory.
Expand Down Expand Up @@ -124,7 +126,6 @@
return ephemeral_storage_mb
case _:
raise ValueError(f"Unknown storage unit: {storage_unit}")
return None


def calculate_tiered_cost(
Expand Down Expand Up @@ -181,8 +182,8 @@
requests_per_month: int,
duration_of_each_request_in_ms: int,
memory_in_gb: float,
tier_cost_factor: dict,
):
tier_cost_factor: dict[str, float],
) -> tuple[float, float]:
"""
@brief Calculate the monthly compute charges based on requests per month, duration of each request in ms, and memory in GB.
@param requests_per_month: The number of requests per month.
Expand Down Expand Up @@ -290,17 +291,40 @@
# 6. Calculate the total monthly cost by summing up the monthly compute charges, monthly request charges, and monthly ephemeral storage charges.
def calculate(
region: str = "us-east-1",
architecture: str = "x86",
architecture: Literal["x86", "arm64"] = "x86",
number_of_requests: int = 1000000,
request_unit: str = "per day",
request_unit: Literal[
"per second",
"per minute",
"per hour",
"per day",
"per month",
"million per month",
] = "per day",
duration_of_each_request_in_ms: int = 1500,
memory: int = 128,
memory_unit: str = "MB",
ephemeral_storage: int = 128,
storage_unit: str = "MB",
) -> tuple[float, list[str]]:
memory: float = 128,
memory_unit: Literal["MB", "GB"] = "MB",
ephemeral_storage: float = 512,
storage_unit: Literal["MB", "GB"] = "MB",
) -> CalculationResult:
"""Calculate the total cost of execution."""

# Validate inputs using pydantic
CalculationRequest(
region=region,
architecture=architecture,
number_of_requests=number_of_requests,
request_unit=request_unit,
duration_of_each_request_in_ms=duration_of_each_request_in_ms,
memory=memory,
memory_unit=memory_unit,
ephemeral_storage=ephemeral_storage,
storage_unit=storage_unit,
)

global steps
steps = []

logger.info("Starting cost calculation...")

# Step 2
Expand Down Expand Up @@ -364,4 +388,5 @@
)
logger.debug(f"Lambda cost (monthly): {total} USD")
steps.append(f"Lambda cost (monthly): {total} USD")
return total, steps

return CalculationResult(total_cost=total, calculation_steps=steps)
80 changes: 80 additions & 0 deletions aws-lambda-calculator/src/aws_lambda_calculator/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from pydantic import BaseModel, Field, model_validator
from typing import Literal


class CalculationRequest(BaseModel):
"""Pydantic model for AWS Lambda cost calculation request parameters."""

region: str = Field(default="us-east-1", description="AWS region for pricing")

architecture: Literal["x86", "arm64"] = Field(
default="x86", description="Lambda function architecture"
)

number_of_requests: int = Field(
default=1000000, gt=0, description="Number of Lambda requests"
)

request_unit: Literal[
"per second",
"per minute",
"per hour",
"per day",
"per month",
"million per month",
] = Field(default="per day", description="Unit for number of requests")

duration_of_each_request_in_ms: int = Field(
default=1500, gt=0, description="Duration of each request in milliseconds"
)

memory: float = Field(
default=128, gt=0, description="Memory allocated to Lambda function"
)

memory_unit: Literal["MB", "GB"] = Field(
default="MB", description="Unit for memory allocation"
)

ephemeral_storage: float = Field(
default=512, gt=0, description="Ephemeral storage allocated to Lambda function"
)

storage_unit: Literal["MB", "GB"] = Field(
default="MB", description="Unit for ephemeral storage"
)

@model_validator(mode="after")
def validate_aws_lambda_limits(self) -> "CalculationRequest":
"""Validate memory and ephemeral storage are within AWS Lambda limits."""
# Validate memory
if self.memory_unit == "MB":
if self.memory < 128 or self.memory > 10240:
raise ValueError("Memory must be between 128 MB and 10,240 MB")
elif self.memory_unit == "GB":
if self.memory < 0.125 or self.memory > 10.24:
raise ValueError("Memory must be between 0.125 GB and 10.24 GB")

# Validate ephemeral storage
if self.storage_unit == "MB":
if self.ephemeral_storage < 512 or self.ephemeral_storage > 10240:
raise ValueError(
"Ephemeral storage must be between 512 MB and 10,240 MB"
)
elif self.storage_unit == "GB":
if self.ephemeral_storage < 0.5 or self.ephemeral_storage > 10.24:
raise ValueError(
"Ephemeral storage must be between 0.5 GB and 10.24 GB"
)

return self


class CalculationResult(BaseModel):
"""Pydantic model for AWS Lambda cost calculation result."""

total_cost: float = Field(description="Total monthly cost in USD")

calculation_steps: list[str] = Field(
description="Step-by-step calculation breakdown"
)
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def build_region_dict(region_name: str, region_code: str) -> None:
"""
region_data = get_region_data(region_code)

region_dict = {
region_dict: dict[str, dict[str, str] | None] = {
"Requests": None,
"EphemeralStorage": None,
"x86": {"Memory": {}, "Tier": {}},
Expand Down
12 changes: 6 additions & 6 deletions aws-lambda-calculator/tests/test_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def test_calculate(
storage_unit,
expected_cost,
):
cost, steps = calculate(
result = calculate(
region=region,
architecture=architecture,
number_of_requests=number_of_requests,
Expand All @@ -117,9 +117,9 @@ def test_calculate(
ephemeral_storage=ephemeral_storage,
storage_unit=storage_unit,
)
assert cost == approx(expected_cost, abs=0.01), (
f"Expected {expected_cost}, got {cost}"
assert result.total_cost == approx(expected_cost, abs=0.01), (
f"Expected {expected_cost}, got {result.total_cost}"
)
# Verify that steps is a list and contains calculation information
assert isinstance(steps, list)
assert len(steps) > 0
# Verify that calculation_steps is a list and contains calculation information
assert isinstance(result.calculation_steps, list)
assert len(result.calculation_steps) > 0
Loading
Loading