escalation-policy-seed-data.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. #!/usr/bin/env python
  2. import dataclasses
  3. import datetime
  4. import random
  5. from functools import cached_property
  6. from typing import Protocol
  7. from sentry.runner import configure
  8. configure()
  9. import click
  10. @click.command()
  11. def seed_policy_data():
  12. from sentry.escalation_policies.models.escalation_policy import (
  13. EscalationPolicy,
  14. EscalationPolicyStep,
  15. EscalationPolicyStepRecipient,
  16. )
  17. from sentry.escalation_policies.models.rotation_schedule import (
  18. RotationSchedule,
  19. RotationScheduleLayer,
  20. RotationScheduleLayerRotationType,
  21. RotationScheduleOverride,
  22. RotationScheduleUserOrder,
  23. ScheduleLayerRestriction,
  24. )
  25. from sentry.models.organization import Organization
  26. from sentry.models.team import Team, TeamStatus
  27. from sentry.testutils.factories import Factories # noqa: S007
  28. def make_restrictions() -> list[tuple[str, str]]:
  29. start = 0
  30. result = []
  31. for i in range(random.randint(0, 4)):
  32. start += random.randint(1, 60 * 5)
  33. end = start + random.randint(1, 60 * 5)
  34. if end >= 60 * 24:
  35. break
  36. result.append(
  37. (f"{int(start / 60):02d}:{start % 60:02d}", f"{int(end / 60):02d}:{end % 60:02d}")
  38. )
  39. return result
  40. class TeamAndUserId(Protocol):
  41. team: Team
  42. user_id: int
  43. @dataclasses.dataclass
  44. class OrgContext:
  45. org_id: int = 1
  46. team_count: int = 5
  47. user_count: int = 5
  48. schedule_count: int = 8
  49. policy_count: int = 5
  50. def ensure_team_and_user_pool(self):
  51. for i in range(self.team_count - len(self.teams)):
  52. self.teams.append(
  53. Factories.create_team(
  54. organization=self.organization,
  55. name=f"Demo Team {i}",
  56. )
  57. )
  58. for i in range(self.user_count - len(self.user_ids)):
  59. u = Factories.create_user()
  60. Factories.create_member(teams=self.teams, user=u, organization=self.organization)
  61. self.user_ids.append(u.id)
  62. for i in range(self.schedule_count - len(self.schedules)):
  63. self.schedules.append(RotationScheduleFactory(org_context=self).build())
  64. # Fix restrictions by recomputing
  65. for schedule in self.schedules:
  66. for layer in RotationScheduleLayer.objects.filter(schedule=schedule):
  67. restriction: ScheduleLayerRestriction = {
  68. "Sun": make_restrictions(),
  69. "Mon": make_restrictions(),
  70. "Tue": make_restrictions(),
  71. "Wed": make_restrictions(),
  72. "Thu": make_restrictions(),
  73. "Fri": make_restrictions(),
  74. "Sat": make_restrictions(),
  75. }
  76. layer.schedule_layer_restrictions = restriction
  77. layer.save()
  78. def ensure_policies(self):
  79. for i in range(self.policy_count - len(self.policies)):
  80. pf = PolicyFactory(self)
  81. self.policies.append(pf.build())
  82. def contextualize_slug_or_user_id(
  83. self, model: TeamAndUserId, slug_or_user_id: str | int | None
  84. ):
  85. if isinstance(slug_or_user_id, str):
  86. model.team = next(t for t in self.teams if t.slug == slug_or_user_id)
  87. elif isinstance(slug_or_user_id, int):
  88. model.user_id = slug_or_user_id
  89. else:
  90. if random.random() < 0.5:
  91. slug_or_user_id = random.choice(self.teams).slug
  92. else:
  93. slug_or_user_id = random.choice(self.user_ids)
  94. self.contextualize_slug_or_user_id(model, slug_or_user_id)
  95. @cached_property
  96. def organization(self) -> Organization:
  97. return Organization.objects.get(pk=self.org_id)
  98. @cached_property
  99. def policies(self) -> list[EscalationPolicy]:
  100. return list(EscalationPolicy.objects.filter(organization=self.organization))
  101. @cached_property
  102. def user_ids(self) -> list[int]:
  103. return list(self.organization.member_set.values_list("user_id", flat=True))
  104. @cached_property
  105. def schedules(self) -> list[RotationSchedule]:
  106. return list(RotationSchedule.objects.filter(organization=self.organization))
  107. @cached_property
  108. def teams(self) -> list[Team]:
  109. return list(
  110. Team.objects.filter(
  111. status=TeamStatus.ACTIVE,
  112. organization=self.organization,
  113. )
  114. )
  115. @dataclasses.dataclass
  116. class RotationScheduleFactory:
  117. org_context: OrgContext
  118. name: str = ""
  119. owner_user_id_or_team_slug: str | int | None = None
  120. @cached_property
  121. def organization(self) -> Organization:
  122. return self.org_context.organization
  123. @cached_property
  124. def rotation_schedule(self) -> RotationSchedule:
  125. schedule = RotationSchedule(
  126. organization=self.organization,
  127. name=self.final_name,
  128. description=self.final_desc,
  129. )
  130. self.org_context.contextualize_slug_or_user_id(
  131. schedule, self.owner_user_id_or_team_slug
  132. )
  133. schedule.save()
  134. return schedule
  135. @cached_property
  136. def final_name(self) -> str:
  137. c = 1
  138. name = self.name or "Rotation Schedule"
  139. while True:
  140. if not RotationSchedule.objects.filter(
  141. organization=self.organization, name=name
  142. ).exists():
  143. break
  144. name = name.removesuffix(" " + str(c))
  145. c += 1
  146. name += " " + str(c)
  147. return name
  148. @cached_property
  149. def final_desc(self) -> str:
  150. words = [
  151. "liberty",
  152. "kettle",
  153. "courage",
  154. "stand",
  155. "satisfaction",
  156. "compliance",
  157. "work",
  158. "voyage",
  159. "district",
  160. "dialogue",
  161. "quit",
  162. "management",
  163. "standard",
  164. "remain",
  165. "graze",
  166. "carpet",
  167. "master",
  168. "relevance",
  169. "machinery",
  170. "rubbish",
  171. "singer",
  172. "presidency",
  173. "horn",
  174. "activate",
  175. "carry",
  176. "sphere",
  177. "shape",
  178. "waiter",
  179. "peasant",
  180. "string",
  181. "result",
  182. "rhetoric",
  183. "occasion",
  184. "thanks",
  185. "auction",
  186. "dawn",
  187. "head",
  188. "wind",
  189. "cater",
  190. "brilliance",
  191. "packet",
  192. "unfair",
  193. "wolf",
  194. "loan",
  195. "thick",
  196. "strikebreaker",
  197. "sunrise",
  198. "minimum",
  199. "resource",
  200. "digital",
  201. ]
  202. return "This is a schedule for " + " ".join(random.choices(words, k=5))
  203. @cached_property
  204. def overrides(self) -> list[RotationScheduleOverride]:
  205. start = datetime.datetime.utcnow() - datetime.timedelta(hours=12)
  206. results = []
  207. for i in range(random.randint(0, 6)):
  208. end = start + datetime.timedelta(hours=random.randint(3, 9))
  209. user_id = random.choice(self.org_context.user_ids)
  210. override = RotationScheduleOverride(
  211. rotation_schedule=self.rotation_schedule,
  212. user_id=user_id,
  213. start_time=start,
  214. end_time=end,
  215. )
  216. override.save()
  217. start = end + datetime.timedelta(hours=random.randint(3, 9))
  218. results.append(override)
  219. return results
  220. @cached_property
  221. def schedule_layers(self) -> list[RotationScheduleLayer]:
  222. results = []
  223. for i in range(random.randint(1, 5)):
  224. layer = RotationScheduleLayer(
  225. schedule=self.rotation_schedule,
  226. precedence=i,
  227. rotation_type=random.choice(list(RotationScheduleLayerRotationType)),
  228. handoff_time=f"{random.randint(0, 23):02d}:{random.randint(0, 59):02d}",
  229. start_date=(
  230. datetime.datetime.utcnow() - datetime.timedelta(days=random.randint(1, 30))
  231. ).date(),
  232. )
  233. restriction: ScheduleLayerRestriction = {
  234. "Sun": make_restrictions(),
  235. "Mon": make_restrictions(),
  236. "Tue": make_restrictions(),
  237. "Wed": make_restrictions(),
  238. "Thu": make_restrictions(),
  239. "Fri": make_restrictions(),
  240. "Sat": make_restrictions(),
  241. }
  242. layer.schedule_layer_restrictions = restriction
  243. layer.save()
  244. results.append(layer)
  245. return results
  246. @cached_property
  247. def user_orders(self) -> list[RotationScheduleUserOrder]:
  248. result = []
  249. for layer in self.schedule_layers:
  250. user_ids = random.sample(self.org_context.user_ids, random.randint(1, 5))
  251. for i, user_id in enumerate(user_ids):
  252. uo = RotationScheduleUserOrder(
  253. schedule_layer=layer,
  254. user_id=user_id,
  255. order=i,
  256. )
  257. uo.save()
  258. result.append(uo)
  259. return result
  260. def build(self):
  261. return (
  262. self.rotation_schedule,
  263. self.schedule_layers,
  264. self.user_orders,
  265. )[0]
  266. @dataclasses.dataclass
  267. class PolicyFactory:
  268. org_context: OrgContext
  269. owner_user_id_or_team_slug: str | int | None = None
  270. name: str = ""
  271. repeat_n_times: int = dataclasses.field(default_factory=lambda: random.randint(1, 5))
  272. @cached_property
  273. def organization(self) -> Organization:
  274. return self.org_context.organization
  275. @cached_property
  276. def final_name(self) -> str:
  277. c = 1
  278. name = self.name or "Escalation Policy"
  279. while True:
  280. if not EscalationPolicy.objects.filter(
  281. organization=self.organization, name=name
  282. ).exists():
  283. break
  284. name = name.removesuffix(" " + str(c))
  285. c += 1
  286. name += " " + str(c)
  287. return name
  288. @cached_property
  289. def policy(self) -> EscalationPolicy:
  290. policy = EscalationPolicy(
  291. organization=self.organization,
  292. name=self.final_name,
  293. description="",
  294. repeat_n_times=self.repeat_n_times,
  295. )
  296. self.org_context.contextualize_slug_or_user_id(policy, self.owner_user_id_or_team_slug)
  297. policy.save()
  298. return policy
  299. @cached_property
  300. def steps(self):
  301. result = []
  302. for i in range(5):
  303. step = EscalationPolicyStep(
  304. policy=self.policy,
  305. step_number=i + 1,
  306. escalate_after_sec=random.randint(1, 5) * 15,
  307. )
  308. step.save()
  309. result.append(step)
  310. return result
  311. @cached_property
  312. def recipients(self):
  313. result = []
  314. for step in self.steps:
  315. result.append(
  316. EscalationPolicyStepRecipient(
  317. escalation_policy_step=step,
  318. schedule=random.choice(self.org_context.schedules),
  319. )
  320. )
  321. result.append(
  322. EscalationPolicyStepRecipient(
  323. escalation_policy_step=step,
  324. team=random.choice(self.org_context.teams),
  325. )
  326. )
  327. result.append(
  328. EscalationPolicyStepRecipient(
  329. escalation_policy_step=step,
  330. user_id=random.choice(self.org_context.user_ids),
  331. )
  332. )
  333. for recipient in result:
  334. recipient.save()
  335. return result
  336. def build(self):
  337. return (
  338. self.policy,
  339. self.steps,
  340. self.recipients,
  341. )[0]
  342. context = OrgContext()
  343. context.ensure_team_and_user_pool()
  344. context.ensure_policies()
  345. if __name__ == "__main__":
  346. seed_policy_data()