Skip to content

Commit 589966c

Browse files
committed
added generic resource to ec2 resource model
1 parent b16958e commit 589966c

File tree

1 file changed

+91
-15
lines changed
  • packages/aws-library/src/aws_library/ec2

1 file changed

+91
-15
lines changed

packages/aws-library/src/aws_library/ec2/_models.py

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,120 @@
1414
Field,
1515
NonNegativeFloat,
1616
NonNegativeInt,
17+
StrictFloat,
18+
StrictInt,
1719
StringConstraints,
1820
field_validator,
1921
)
2022
from pydantic.config import JsonDict
2123
from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
2224

25+
GenericResourceValue: TypeAlias = StrictInt | StrictFloat | str
26+
2327

2428
class Resources(BaseModel, frozen=True):
2529
cpus: NonNegativeFloat
2630
ram: ByteSize
31+
generic_resources: Annotated[
32+
dict[str, GenericResourceValue],
33+
Field(
34+
default_factory=dict,
35+
description=(
36+
"Arbitrary additional resources (e.g. {'threads': 8}). "
37+
"Numeric values are treated as quantities and participate in add/sub/compare."
38+
),
39+
),
40+
] = DEFAULT_FACTORY
2741

2842
@classmethod
2943
def create_as_empty(cls) -> "Resources":
3044
return cls(cpus=0, ram=ByteSize(0))
3145

3246
def __ge__(self, other: "Resources") -> bool:
33-
return self.cpus >= other.cpus and self.ram >= other.ram
47+
if not (self.cpus >= other.cpus and self.ram >= other.ram):
48+
return False
49+
# ensure all numeric generic resources in `other` are satisfied by `self`
50+
for k, v in other.generic_resources.items():
51+
if isinstance(v, int | float):
52+
lhs_val = self.generic_resources.get(k, 0)
53+
if not isinstance(lhs_val, int | float) or lhs_val < v:
54+
return False
55+
continue
56+
# non-numeric must be equal and present
57+
if k not in self.generic_resources or self.generic_resources[k] != v:
58+
return False
59+
return True
3460

3561
def __gt__(self, other: "Resources") -> bool:
36-
return self.cpus > other.cpus or self.ram > other.ram
62+
if self.cpus > other.cpus or self.ram > other.ram:
63+
return True
64+
for k, v in other.generic_resources.items():
65+
lhs_val = self.generic_resources.get(k)
66+
if (
67+
isinstance(v, int | float)
68+
and isinstance(lhs_val, int | float)
69+
and lhs_val > v
70+
):
71+
return True
72+
if not isinstance(v, int | float) and lhs_val is not None and lhs_val != v:
73+
return True
74+
return False
3775

3876
def __add__(self, other: "Resources") -> "Resources":
77+
"""operator for adding two Resources
78+
Note that only numeric generic resources are added
79+
Non-numeric generic resources are ignored
80+
"""
81+
merged: dict[str, GenericResourceValue] = {}
82+
keys = set(self.generic_resources) | set(other.generic_resources)
83+
for k in keys:
84+
a = self.generic_resources.get(k)
85+
b = other.generic_resources.get(k)
86+
# adding non numeric values does not make sense, so we skip those for the resulting resource
87+
if isinstance(a, int | float) and isinstance(b, int | float):
88+
merged[k] = a + b
89+
elif a is None and isinstance(b, int | float):
90+
merged[k] = b
91+
elif b is None and isinstance(a, int | float):
92+
merged[k] = a
93+
3994
return Resources.model_construct(
40-
**{
41-
key: a + b
42-
for (key, a), b in zip(
43-
self.model_dump().items(), other.model_dump().values(), strict=True
44-
)
45-
}
95+
cpus=self.cpus + other.cpus,
96+
ram=self.ram + other.ram,
97+
generic_resources=merged,
4698
)
4799

48100
def __sub__(self, other: "Resources") -> "Resources":
101+
"""operator for subtracting two Resources
102+
Note that only numeric generic resources are subtracted
103+
Non-numeric generic resources are ignored
104+
"""
105+
merged: dict[str, GenericResourceValue] = {}
106+
keys = set(self.generic_resources) | set(other.generic_resources)
107+
for k in keys:
108+
a = self.generic_resources.get(k)
109+
b = other.generic_resources.get(k)
110+
# subtracting non numeric values does not make sense, so we skip those for the resulting resource
111+
if isinstance(a, int | float) and isinstance(b, int | float):
112+
merged[k] = a - b
113+
elif a is None and isinstance(b, int | float):
114+
merged[k] = -b
115+
elif b is None and isinstance(a, int | float):
116+
merged[k] = a
117+
49118
return Resources.model_construct(
50-
**{
51-
key: a - b
52-
for (key, a), b in zip(
53-
self.model_dump().items(), other.model_dump().values(), strict=True
54-
)
55-
}
119+
cpus=self.cpus - other.cpus,
120+
ram=self.ram - other.ram,
121+
generic_resources=merged,
122+
)
123+
124+
def __hash__(self) -> int:
125+
"""Deterministic hash including cpus, ram (in bytes) and generic_resources."""
126+
# sort generic_resources items to ensure order-independent hashing
127+
generic_items: tuple[tuple[str, GenericResourceValue], ...] = tuple(
128+
sorted(self.generic_resources.items())
56129
)
130+
return hash((self.cpus, self.ram, generic_items))
57131

58132
@field_validator("cpus", mode="before")
59133
@classmethod
@@ -180,7 +254,9 @@ def validate_bash_calls(cls, v):
180254
temp_file.writelines(v)
181255
temp_file.flush()
182256
# NOTE: this will not capture runtime errors, but at least some syntax errors such as invalid quotes
183-
sh.bash("-n", temp_file.name) # pyright: ignore[reportCallIssue] # sh is untyped, but this call is safe for bash syntax checking
257+
sh.bash(
258+
"-n", temp_file.name
259+
) # pyright: ignore[reportCallIssue] # sh is untyped, but this call is safe for bash syntax checking
184260
except sh.ErrorReturnCode as exc:
185261
msg = f"Invalid bash call in custom_boot_scripts: {v}, Error: {exc.stderr}"
186262
raise ValueError(msg) from exc

0 commit comments

Comments
 (0)