Skip to content

Commit

Permalink
Update config structure (#5)
Browse files Browse the repository at this point in the history
Co-authored-by: Pedro Rodrigues <[email protected]>
  • Loading branch information
hpedrorodrigues and hpedrorodrigues authored Oct 29, 2024
1 parent ac29ffe commit e1963a0
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 52 deletions.
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,20 @@ schedules_mapping:
# User group name in Slack to be updated based on the OpsGenie schedule.
user_group_name: ''

# Optional
# Overrides OpsGenie schedules based on the given conditions.
# Optional. Overrides OpsGenie schedules based on the given config.
overrides:
# Email of the user that's on-call should be equal to the given value.
- when_user: ''
# Current time (in the given timezone) must be within the given time range.
# Format: HH:MM:SS.
from_time: ''
to_time: ''
with_timezone: ''
# Days that should be considered to override.
# Possible values:
# Email of the user that's on-call.
- user_email: ''
# Current time (in the given timezone) must be within the given time range. Format: HH:MM:SS.
timezone: ''
starts_on: ''
ends_on: ''
# Optional. Defaults to all_days.
# Days that should be considered to override. Possible values:
# - all_days
# - weekdays
# - weekends
on: ''
repeats_on: ''
# Update the user group in Slack with the given users instead of the one that's on-call.
replace_by:
- ''
Expand All @@ -60,11 +58,11 @@ schedules_mapping:
- schedule_name: sre
user_group_name: 'sre-oncall'
overrides:
- when_user: [email protected]
from_time: '23:00:00'
to_time: '01:00:00'
with_timezone: 'America/Fortaleza'
on: weekdays
- user_email: [email protected]
timezone: 'America/Fortaleza'
starts_on: '23:00:00'
ends_on: '01:00:00'
repeats_on: weekdays
replace_by:
- [email protected]
```
Expand Down
19 changes: 17 additions & 2 deletions disturbed/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import yaml
import yamlcore

from disturbed.configuration.types import Config, ScheduleMapping, ScheduleOverride
from disturbed.configuration.types import (
Config,
RepeatsOn,
ScheduleMapping,
ScheduleOverride,
)
from disturbed.opsgenie.api import logger


Expand Down Expand Up @@ -33,7 +38,17 @@ def _load(self) -> Config:
for mapping in config_dict["schedules_mapping"]:
overrides = None
if "overrides" in mapping:
overrides = [ScheduleOverride(**override) for override in mapping["overrides"]]
overrides = [
ScheduleOverride(
user_email=override["user_email"],
timezone=override["timezone"],
starts_on=override["starts_on"],
ends_on=override["ends_on"],
repeats_on=RepeatsOn(override["repeats_on"]) if "repeats_on" in override else None,
replace_by=override["replace_by"],
)
for override in mapping["overrides"]
]
mappings.append(
ScheduleMapping(
schedule_name=mapping["schedule_name"],
Expand Down
19 changes: 11 additions & 8 deletions disturbed/configuration/types.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
from dataclasses import dataclass
from enum import Enum
from typing import Optional

ALL_DAYS = "all_days"
WEEKDAYS = "weekdays"
WEEKENDS = "weekends"

class RepeatsOn(Enum):
ALL_DAYS = "all_days"
WEEKDAYS = "weekdays"
WEEKENDS = "weekends"


@dataclass
class ScheduleOverride:
when_user: str
from_time: str
to_time: str
with_timezone: str
on: str
user_email: str
timezone: str
starts_on: str
ends_on: str
repeats_on: Optional[RepeatsOn]
replace_by: list[str]


Expand Down
33 changes: 21 additions & 12 deletions disturbed/handler/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Optional

from disturbed.configuration import Configuration, ScheduleMapping, ScheduleOverride
from disturbed.configuration.types import ALL_DAYS, WEEKDAYS, WEEKENDS
from disturbed.configuration.types import RepeatsOn
from disturbed.handler.time import is_time_between, is_weekday
from disturbed.opsgenie.api import OpsgenieApi
from disturbed.slack.api import SlackApi
Expand All @@ -18,7 +18,7 @@ def __init__(self, config: Configuration, opsgenie_api: OpsgenieApi, slack_api:
self.opsgenie_api = opsgenie_api
self.slack_api = slack_api

def handle_schedules(self) -> Optional[DisturbedError]:
def process(self) -> Optional[DisturbedError]:
group_id_by_name = self.slack_api.find_user_group_ids(
group_names=[mapping.user_group_name for mapping in self.config.schedules_mapping]
)
Expand All @@ -33,13 +33,17 @@ def handle_schedules(self) -> Optional[DisturbedError]:
if schedule.overrides:
continue_processing = True
for override in schedule.overrides:
override_applied = self._handle_override(
oncall_user_email.value, schedule, override, group_id_by_name.value
override_applied = self._apply_override(
oncall_user_email=oncall_user_email.value,
schedule=schedule,
override=override,
group_id_by_name=group_id_by_name.value,
)
if override_applied.is_left():
return override_applied.value
if override_applied.value:
continue_processing = False
break
if not continue_processing:
continue

Expand All @@ -54,25 +58,30 @@ def handle_schedules(self) -> Optional[DisturbedError]:
return error
logger.info("All done!")

def _handle_override(
def _apply_override(
self,
oncall_user_email: str,
schedule: ScheduleMapping,
override: ScheduleOverride,
group_id_by_name: dict[str, str],
) -> Either[DisturbedError, bool]:
if override.when_user != oncall_user_email:
if oncall_user_email != override.user_email:
return Either.right(False)

is_on_valid_day = (
override.on == ALL_DAYS
or (override.on == WEEKDAYS and is_weekday(override.with_timezone))
or (override.on == WEEKENDS and not is_weekday(override.with_timezone))
is_on_repeat = (
not override.repeats_on
or override.repeats_on == RepeatsOn.ALL_DAYS
or (override.repeats_on == RepeatsOn.WEEKDAYS and is_weekday(override.timezone))
or (override.repeats_on == RepeatsOn.WEEKENDS and not is_weekday(override.timezone))
)
if not is_on_valid_day:
if not is_on_repeat:
return Either.right(False)

is_time_within_range = is_time_between(override.with_timezone, override.from_time, override.to_time)
is_time_within_range = is_time_between(
timezone=override.timezone,
from_time=override.starts_on,
to_time=override.ends_on,
)
if is_time_within_range.is_left():
return is_time_within_range
if not is_time_within_range.value:
Expand Down
85 changes: 84 additions & 1 deletion disturbed/types/either.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Generic, TypeVar, Union
from typing import Callable, Generic, Optional, TypeVar, Union, cast

L = TypeVar("L") # Left type
R = TypeVar("R") # Right type
Expand Down Expand Up @@ -28,3 +28,86 @@ def left(value: L) -> "Either[L, R]":

def is_left(self) -> bool:
return not self.is_right

def map(self, f: Callable[[R], A]) -> "Either[L, A]":
"""
Apply a function to the Right value, leaving Left values unchanged.
"""
if self.is_right:
return Either.right(f(cast(R, self.value)))
return Either.left(cast(L, self.value))

def map_left(self, f: Callable[[L], A]) -> "Either[A, R]":
"""
Apply a function to the Left value, leaving Right values unchanged.
"""
if self.is_left():
return Either.left(f(cast(L, self.value)))
return Either.right(cast(R, self.value))

def get_or_else(self, default: R) -> R:
"""
Get the Right value or a default if this is a Left.
"""
if self.is_right:
return cast(R, self.value)
return default

def get_or_else_get(self, f: Callable[[], R]) -> R:
"""
Get the Right value or compute a default if this is a Left.
"""
if self.is_right:
return cast(R, self.value)
return f()

def or_else(self, other: "Either[L, R]") -> "Either[L, R]":
"""
Return this Either if it's a Right, otherwise return the other Either.
"""
if self.is_right:
return self
return other

def or_else_get(self, f: Callable[[], "Either[L, R]"]) -> "Either[L, R]":
"""
Return this Either if it's a Right, otherwise compute and return another Either.
"""
if self.is_right:
return self
return f()

def contains(self, value: R) -> bool:
"""
Check if this Either is a Right and contains the given value.
"""
if not self.is_right:
return False
return cast(R, self.value) == value

def exists(self, predicate: Callable[[R], bool]) -> bool:
"""
Check if this Either is a Right and its value satisfies the predicate.
"""
if not self.is_right:
return False
return predicate(cast(R, self.value))

def filter_or_else(self, predicate: Callable[[R], bool], zero: L) -> "Either[L, R]":
"""
Convert this Either to a Left if it's a Right and fails the predicate.
"""
if self.is_right:
right_value = cast(R, self.value)
if not predicate(right_value):
return Either.left(zero)
return self
return self

def to_optional(self) -> Optional[R]:
"""
Convert this Either to an Optional, discarding the Left value.
"""
if self.is_right:
return cast(R, self.value)
return None
12 changes: 6 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ def main():
level=get_env("DISTURBED_LOG_LEVEL", logging.INFO),
)

config = Configuration()
opsgenie_api = OpsgenieApi(api_key=get_env("DISTURBED_OPSGENIE_API_KEY"))
slack_api = SlackApi(token=get_env("DISTURBED_SLACK_API_TOKEN"))

handler = ScheduleHandler(config, opsgenie_api, slack_api)
error = handler.handle_schedules()
handler = ScheduleHandler(
config=Configuration(),
opsgenie_api=OpsgenieApi(api_key=get_env("DISTURBED_OPSGENIE_API_KEY")),
slack_api=SlackApi(token=get_env("DISTURBED_SLACK_API_TOKEN")),
)
error = handler.process()
if error:
logger.critical(error)
sys.exit(1)
Expand Down
10 changes: 5 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
requests = "^2.32.3"
slack-sdk = "^3.33.1"
slack-sdk = "^3.33.2"
pyyaml = "^6.0.2"
yamlcore = "^0.0.4"

Expand Down

0 comments on commit e1963a0

Please sign in to comment.