Skip to content

Commit

Permalink
Merge pull request #94 from GreenScheduler/i89-93
Browse files Browse the repository at this point in the history
Misc fixes
  • Loading branch information
abhidg authored May 7, 2024
2 parents 59e3668 + 5af7c94 commit 5755be1
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 23 deletions.
5 changes: 4 additions & 1 deletion cats/CI_api_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ class InvalidLocationError(Exception):
pass


APIInterface = namedtuple("APIInterface", ["get_request_url", "parse_response_data"])
APIInterface = namedtuple(
"APIInterface", ["get_request_url", "parse_response_data", "max_duration"]
)


def ciuk_request_url(timestamp: datetime, postcode: str):
Expand Down Expand Up @@ -83,5 +85,6 @@ def invalid_code(r: dict) -> bool:
"carbonintensity.org.uk": APIInterface(
get_request_url=ciuk_request_url,
parse_response_data=ciuk_parse_response_data,
max_duration=2880, # 48h
),
}
57 changes: 40 additions & 17 deletions cats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,16 @@ class CATSOutput:
emmissionEstimate: Optional[Estimates] = None

def __str__(self) -> str:
out = f"Best job start time: {self.carbonIntensityOptimal.start}"
out = f"""Best job start time {self.carbonIntensityOptimal.start}
Carbon intensity if job started now = {self.carbonIntensityNow.value:.2f} gCO2eq/kWh
Carbon intensity at optimal time = {self.carbonIntensityOptimal.value:.2f} gCO2eq/kWh"""

if self.emmissionEstimate:
out += (
f"\nEstimated emmissions for running job now: {self.emmissionEstimate.now}\n"
f"Estimated emmissions for running delayed job: {self.emmissionEstimate.best}"
f" (- {self.emmissionEstimate.savings})"
)
out += f"""
Estimated emissions if job started now = {self.emmissionEstimate.now}
Estimated emissions at optimal time = {self.emmissionEstimate.best} (- {self.emmissionEstimate.savings})"""

out += "\n\nUse --format=json to get this in machine readable format"
return out

def to_json(self, dateformat: str = "", **kwargs) -> str:
Expand All @@ -215,17 +217,30 @@ def to_json(self, dateformat: str = "", **kwargs) -> str:
return json.dumps(data, **kwargs)


def schedule_at(output: CATSOutput, args: list[str]) -> None:
"Schedule job with optimal start time using at(1)"
def schedule_at(
output: CATSOutput, args: list[str], at_command: str = "at"
) -> Optional[str]:
"""Schedule job with optimal start time using at(1)
:return: Error as a string, or None if successful
"""
proc = subprocess.Popen(args, stdout=subprocess.PIPE)
proc_output = subprocess.check_output(
(
"at",
"-t",
output.carbonIntensityOptimal.start.strftime(SCHEDULER_DATE_FORMAT["at"]),
),
stdin=proc.stdout,
)
try:
proc_output = subprocess.check_output(
(
at_command,
"-t",
output.carbonIntensityOptimal.start.strftime(
SCHEDULER_DATE_FORMAT["at"]
),
),
stdin=proc.stdout,
)
return None
except FileNotFoundError:
return "No at command found in PATH, please install one"
except subprocess.CalledProcessError as e:
return f"Scheduling with at failed with code {e.returncode}, see output below:\n{e.output}"


def main(arguments=None) -> int:
Expand All @@ -239,6 +254,12 @@ def main(arguments=None) -> int:
return 1

CI_API_interface, location, duration, jobinfo, PUE = get_runtime_config(args)
if duration > CI_API_interface.max_duration:
print(
f"""API allows a maximum job duration of {CI_API_interface.max_duration} minutes.
This is usually due to forecast limitations."""
)
return 1

########################
## Obtain CI forecast ##
Expand Down Expand Up @@ -287,7 +308,9 @@ def main(arguments=None) -> int:
else:
print(output)
if args.command and args.scheduler == "at":
schedule_at(output, args.command.split())
if err := schedule_at(output, args.command.split()):
print(err)
return 1
return 0


Expand Down
18 changes: 17 additions & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
)


def test_schedule_at(fp):
def test_schedule_at_success(fp):
fp.register_subprocess(["ls"], stdout=b"foobar.txt")
fp.register_subprocess(
[
Expand All @@ -40,6 +40,19 @@ def test_schedule_at(fp):
)


def test_schedule_at_missing():
assert (
schedule_at(OUTPUT, ["ls"], at_command="at_imaginary")
== "No at command found in PATH, please install one"
)


def test_schedule_at_failure():
assert schedule_at(OUTPUT, ["ls"], at_command="/usr/bin/false").startswith(
"Scheduling with at failed with code 1, see output below:"
)


def raiseLocationError():
raise InvalidLocationError

Expand All @@ -54,3 +67,6 @@ def test_main_failures(get_CI_forecast):

# Invalid location
assert main(["-d", "5", "--loc", "oxford"]) == 1

# Duration larger than API maximum
assert main(["-d", "5000", "--loc", "OX1"]) == 1
19 changes: 15 additions & 4 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,23 @@
@pytest.mark.parametrize(
"output,expected",
[
(OUTPUT, "Best job start time: 2024-03-16 02:00:00"),
(
OUTPUT,
"""Best job start time 2024-03-16 02:00:00
Carbon intensity if job started now = 50.00 gCO2eq/kWh
Carbon intensity at optimal time = 20.00 gCO2eq/kWh
Use --format=json to get this in machine readable format""",
),
(
OUTPUT_WITH_EMISSION_ESTIMATE,
"""Best job start time: 2024-03-16 02:00:00
Estimated emmissions for running job now: 19
Estimated emmissions for running delayed job: 9 (- 10)""",
"""Best job start time 2024-03-16 02:00:00
Carbon intensity if job started now = 50.00 gCO2eq/kWh
Carbon intensity at optimal time = 20.00 gCO2eq/kWh
Estimated emissions if job started now = 19
Estimated emissions at optimal time = 9 (- 10)
Use --format=json to get this in machine readable format""",
),
],
)
Expand Down

0 comments on commit 5755be1

Please sign in to comment.