Skip to content

Commit fa20985

Browse files
authored
Merge branch 'master' into dependabot/npm_and_yarn/tests/e2e-frontend/npm_and_yarn-0c3072c8cf
2 parents d4e22cf + 4b2fa25 commit fa20985

File tree

30 files changed

+1103
-229
lines changed

30 files changed

+1103
-229
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
EC2InstanceData,
1818
EC2InstanceType,
1919
EC2Tags,
20+
GenericResourceValueType,
2021
Resources,
2122
)
2223

@@ -36,6 +37,7 @@
3637
"EC2NotConnectedError",
3738
"EC2RuntimeError",
3839
"EC2Tags",
40+
"GenericResourceValueType",
3941
"Resources",
4042
"SimcoreEC2API",
4143
)

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

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import datetime
23
import re
34
import tempfile
@@ -14,45 +15,171 @@
1415
Field,
1516
NonNegativeFloat,
1617
NonNegativeInt,
18+
StrictFloat,
19+
StrictInt,
1720
StringConstraints,
21+
TypeAdapter,
22+
ValidationError,
1823
field_validator,
1924
)
2025
from pydantic.config import JsonDict
2126
from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
2227

28+
GenericResourceValueType: TypeAlias = StrictInt | StrictFloat | str
29+
2330

2431
class Resources(BaseModel, frozen=True):
2532
cpus: NonNegativeFloat
2633
ram: ByteSize
34+
generic_resources: Annotated[
35+
dict[str, GenericResourceValueType],
36+
Field(
37+
default_factory=dict,
38+
description=(
39+
"Arbitrary additional resources (e.g. {'threads': 8}). "
40+
"Numeric values are treated as quantities and participate in add/sub/compare."
41+
),
42+
),
43+
] = DEFAULT_FACTORY
2744

2845
@classmethod
2946
def create_as_empty(cls) -> "Resources":
3047
return cls(cpus=0, ram=ByteSize(0))
3148

3249
def __ge__(self, other: "Resources") -> bool:
33-
return self.cpus >= other.cpus and self.ram >= other.ram
50+
"""operator for >= comparison
51+
if self has greater or equal resources than other, returns True
52+
This will return True only if all of the resources in self are greater or equal to other
53+
54+
Note that generic_resources are compared only if they are numeric
55+
Non-numeric generic resources must be equal in both or only defined in self
56+
to be considered greater or equal
57+
"""
58+
if self == other:
59+
return True
60+
return self > other
3461

3562
def __gt__(self, other: "Resources") -> bool:
36-
return self.cpus > other.cpus or self.ram > other.ram
63+
"""operator for > comparison
64+
if self has resources greater than other, returns True
65+
This will return True only if all of the resources in self are greater than other
66+
67+
Note that generic_resources are compared only if they are numeric
68+
Non-numeric generic resources must only be defined in self
69+
to be considered greater
70+
"""
71+
if (self.cpus < other.cpus) or (self.ram < other.ram):
72+
return False
73+
74+
keys = set(self.generic_resources) | set(other.generic_resources)
75+
for k in keys:
76+
a = self.generic_resources.get(k)
77+
b = other.generic_resources.get(k)
78+
if a is None:
79+
return False
80+
if b is None:
81+
# a is greater as b is not defined
82+
continue
83+
if isinstance(a, int | float) and isinstance(b, int | float):
84+
if a < b:
85+
return False
86+
else:
87+
# remaining options is a is str and b is str or mixed types
88+
# NOTE: we cannot compare strings unless they are equal or some kind of boolean (e.g. "true", "false", "yes", "no", "1", "0")
89+
assert isinstance(a, str) # nosec
90+
assert isinstance(b, int | float | str) # nosec
91+
# let's try to get a boolean out of the values to compare them
92+
with contextlib.suppress(ValidationError):
93+
a_as_boolean = TypeAdapter(bool).validate_python(a)
94+
b_as_boolean = TypeAdapter(bool).validate_python(b)
95+
if not a_as_boolean and b_as_boolean:
96+
return False
97+
98+
# here we have either everything greater or equal or non-comparable strings
99+
100+
return self != other
37101

38102
def __add__(self, other: "Resources") -> "Resources":
103+
"""operator for adding two Resources
104+
Note that only numeric generic resources are added
105+
Non-numeric generic resources are ignored
106+
"""
107+
merged: dict[str, GenericResourceValueType] = {}
108+
keys = set(self.generic_resources) | set(other.generic_resources)
109+
for k in keys:
110+
a = self.generic_resources.get(k)
111+
b = other.generic_resources.get(k)
112+
# adding non numeric values does not make sense, so we skip those for the resulting resource
113+
if isinstance(a, int | float) and isinstance(b, int | float):
114+
merged[k] = a + b
115+
elif a is None and isinstance(b, int | float):
116+
merged[k] = b
117+
elif b is None and isinstance(a, int | float):
118+
merged[k] = a
119+
39120
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-
}
121+
cpus=self.cpus + other.cpus,
122+
ram=self.ram + other.ram,
123+
generic_resources=merged,
46124
)
47125

48126
def __sub__(self, other: "Resources") -> "Resources":
127+
"""operator for subtracting two Resources
128+
Note that only numeric generic resources are subtracted
129+
Non-numeric generic resources are ignored
130+
"""
131+
merged: dict[str, GenericResourceValueType] = {}
132+
keys = set(self.generic_resources) | set(other.generic_resources)
133+
for k in keys:
134+
a = self.generic_resources.get(k)
135+
b = other.generic_resources.get(k)
136+
# subtracting non numeric values does not make sense, so we skip those for the resulting resource
137+
if isinstance(a, int | float) and isinstance(b, int | float):
138+
merged[k] = a - b
139+
elif a is None and isinstance(b, int | float):
140+
merged[k] = -b
141+
elif b is None and isinstance(a, int | float):
142+
merged[k] = a
143+
49144
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-
}
145+
cpus=self.cpus - other.cpus,
146+
ram=self.ram - other.ram,
147+
generic_resources=merged,
148+
)
149+
150+
def __hash__(self) -> int:
151+
"""Deterministic hash including cpus, ram (in bytes) and generic_resources."""
152+
# sort generic_resources items to ensure order-independent hashing
153+
generic_items: tuple[tuple[str, GenericResourceValueType], ...] = tuple(
154+
sorted(self.generic_resources.items())
155+
)
156+
return hash((self.cpus, self.ram, generic_items))
157+
158+
def as_flat_dict(self) -> dict[str, int | float | str]:
159+
"""Like model_dump, but flattens generic_resources to top level keys"""
160+
base = self.model_dump()
161+
base.update(base.pop("generic_resources"))
162+
return base
163+
164+
@classmethod
165+
def from_flat_dict(
166+
cls,
167+
data: dict[str, int | float | str],
168+
*,
169+
mapping: dict[str, str] | None = None,
170+
) -> "Resources":
171+
"""Inverse of as_flat_dict with optional key mapping"""
172+
mapped_data = data
173+
if mapping:
174+
mapped_data = {mapping.get(k, k): v for k, v in data.items()}
175+
generic_resources = {
176+
k: v for k, v in mapped_data.items() if k not in {"cpus", "ram"}
177+
}
178+
179+
return cls(
180+
cpus=float(mapped_data.get("cpus", 0)),
181+
ram=ByteSize(mapped_data.get("ram", 0)),
182+
generic_resources=generic_resources,
56183
)
57184

58185
@field_validator("cpus", mode="before")
@@ -174,8 +301,9 @@ def validate_bash_calls(cls, v):
174301
temp_file.flush()
175302
# NOTE: this will not capture runtime errors, but at least some syntax errors such as invalid quotes
176303
sh.bash(
177-
"-n", temp_file.name
178-
) # pyright: ignore[reportCallIssue] # sh is untyped, but this call is safe for bash syntax checking
304+
"-n",
305+
temp_file.name, # pyright: ignore[reportCallIssue] - sh is untyped but safe for bash syntax checking
306+
)
179307
except sh.ErrorReturnCode as exc:
180308
msg = f"Invalid bash call in custom_boot_scripts: {v}, Error: {exc.stderr}"
181309
raise ValueError(msg) from exc

0 commit comments

Comments
 (0)