review-request 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. review_request-0.1.0/LICENSE +21 -0
  2. review_request-0.1.0/PKG-INFO +56 -0
  3. review_request-0.1.0/pyproject.toml +76 -0
  4. review_request-0.1.0/setup.cfg +4 -0
  5. review_request-0.1.0/src/review_request/__init__.py +3 -0
  6. review_request-0.1.0/src/review_request/api/__init__.py +0 -0
  7. review_request-0.1.0/src/review_request/api/jira_sprint.py +178 -0
  8. review_request-0.1.0/src/review_request/api/message.py +94 -0
  9. review_request-0.1.0/src/review_request/config/__init__.py +0 -0
  10. review_request-0.1.0/src/review_request/config/settings.py +101 -0
  11. review_request-0.1.0/src/review_request/decorators/__init__.py +0 -0
  12. review_request-0.1.0/src/review_request/decorators/base_message_decorator.py +34 -0
  13. review_request-0.1.0/src/review_request/decorators/jira_overdue_message.py +110 -0
  14. review_request-0.1.0/src/review_request/decorators/message_formatters.py +61 -0
  15. review_request-0.1.0/src/review_request/decorators/message_templates.py +148 -0
  16. review_request-0.1.0/src/review_request/decorators/random_remind_review_messages.py +20 -0
  17. review_request-0.1.0/src/review_request/decorators/random_review_message.py +27 -0
  18. review_request-0.1.0/src/review_request/decorators/review_message.py +0 -0
  19. review_request-0.1.0/src/review_request/routes/__init__.py +0 -0
  20. review_request-0.1.0/src/review_request/routes/api.py +17 -0
  21. review_request-0.1.0/src/review_request/scripts/__init__.py +0 -0
  22. review_request-0.1.0/src/review_request/scripts/jira_overdue_reminder.py +213 -0
  23. review_request-0.1.0/src/review_request/scripts/pr_reminder.py +207 -0
  24. review_request-0.1.0/src/review_request/serializers/__init__.py +0 -0
  25. review_request-0.1.0/src/review_request/serializers/conversation_response.py +6 -0
  26. review_request-0.1.0/src/review_request/serializers/jira_sprint_webhook.py +43 -0
  27. review_request-0.1.0/src/review_request/services/__init__.py +0 -0
  28. review_request-0.1.0/src/review_request/services/cache_service.py +52 -0
  29. review_request-0.1.0/src/review_request/services/github.py +222 -0
  30. review_request-0.1.0/src/review_request/services/jira.py +54 -0
  31. review_request-0.1.0/src/review_request/services/jira_overdue_message_service.py +98 -0
  32. review_request-0.1.0/src/review_request/services/jira_overdue_reminder_service.py +166 -0
  33. review_request-0.1.0/src/review_request/services/jira_sprint_notification_service.py +145 -0
  34. review_request-0.1.0/src/review_request/services/pr_reminder_service.py +220 -0
  35. review_request-0.1.0/src/review_request/services/rollbar_service.py +121 -0
  36. review_request-0.1.0/src/review_request/services/send_message.py +217 -0
  37. review_request-0.1.0/src/review_request/services/shuffle_service.py +38 -0
  38. review_request-0.1.0/src/review_request/services/slack.py +194 -0
  39. review_request-0.1.0/src/review_request/utils/__init__.py +0 -0
  40. review_request-0.1.0/src/review_request/utils/config_validator.py +60 -0
  41. review_request-0.1.0/src/review_request/utils/date_checker.py +49 -0
  42. review_request-0.1.0/src/review_request/utils/logger_setup.py +57 -0
  43. review_request-0.1.0/src/review_request/utils/reminder_result.py +48 -0
  44. review_request-0.1.0/src/review_request.egg-info/PKG-INFO +56 -0
  45. review_request-0.1.0/src/review_request.egg-info/SOURCES.txt +47 -0
  46. review_request-0.1.0/src/review_request.egg-info/dependency_links.txt +1 -0
  47. review_request-0.1.0/src/review_request.egg-info/entry_points.txt +3 -0
  48. review_request-0.1.0/src/review_request.egg-info/requires.txt +22 -0
  49. review_request-0.1.0/src/review_request.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 King Kong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: review-request
3
+ Version: 0.1.0
4
+ Summary: Slack and GitHub integration for automating code review requests
5
+ Author: King Kong
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 King Kong
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: Programming Language :: Python :: 3.11
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Operating System :: OS Independent
32
+ Requires-Python: >=3.11
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: fastapi
36
+ Requires-Dist: uvicorn
37
+ Requires-Dist: slack-sdk
38
+ Requires-Dist: python-dotenv
39
+ Requires-Dist: requests
40
+ Requires-Dist: validators
41
+ Requires-Dist: pydantic-settings
42
+ Requires-Dist: python-multipart
43
+ Requires-Dist: aiohttp
44
+ Requires-Dist: pydantic
45
+ Requires-Dist: cachetools
46
+ Requires-Dist: pytz
47
+ Requires-Dist: tenacity
48
+ Requires-Dist: rollbar
49
+ Provides-Extra: dev
50
+ Requires-Dist: pytest>=8; extra == "dev"
51
+ Requires-Dist: pytest-cov; extra == "dev"
52
+ Requires-Dist: pytest-asyncio; extra == "dev"
53
+ Requires-Dist: black; extra == "dev"
54
+ Requires-Dist: ruff; extra == "dev"
55
+ Requires-Dist: mypy; extra == "dev"
56
+ Dynamic: license-file
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "review-request"
7
+ version = "0.1.0"
8
+ description = "Slack and GitHub integration for automating code review requests"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "King Kong" }]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "fastapi",
21
+ "uvicorn",
22
+ "slack-sdk",
23
+ "python-dotenv",
24
+ "requests",
25
+ "validators",
26
+ "pydantic-settings",
27
+ "python-multipart",
28
+ "aiohttp",
29
+ "pydantic",
30
+ "cachetools",
31
+ "pytz",
32
+ "tenacity",
33
+ "rollbar",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "pytest>=8",
39
+ "pytest-cov",
40
+ "pytest-asyncio",
41
+ "black",
42
+ "ruff",
43
+ "mypy",
44
+ ]
45
+
46
+ [project.scripts]
47
+ pr-reminder = "review_request.scripts.pr_reminder:run"
48
+ jira-overdue-reminder = "review_request.scripts.jira_overdue_reminder:run"
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.pytest.ini_options]
54
+ asyncio_mode = "auto"
55
+ testpaths = ["tests"]
56
+ addopts = "-v --strict-markers --cov=review_request --cov-report=term-missing"
57
+ markers = [
58
+ "unit: Unit tests",
59
+ "integration: Integration tests",
60
+ ]
61
+
62
+ [tool.black]
63
+ line-length = 88
64
+ target-version = ["py311"]
65
+
66
+ [tool.ruff]
67
+ line-length = 88
68
+ target-version = "py311"
69
+
70
+ [tool.ruff.lint]
71
+ select = ["E", "F", "W", "I"]
72
+
73
+ [tool.mypy]
74
+ python_version = "3.11"
75
+ strict = true
76
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """review-request: Slack and GitHub integration for automating code review requests."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,178 @@
1
+ """API endpoints for Jira sprint webhooks."""
2
+
3
+ import logging
4
+ from fastapi import APIRouter, Request, HTTPException
5
+ from fastapi.responses import JSONResponse
6
+
7
+ from review_request.serializers.jira_sprint_webhook import (
8
+ JiraSprintWebhook,
9
+ SprintNotificationRequest,
10
+ )
11
+ from review_request.services.jira_sprint_notification_service import (
12
+ JiraSprintNotificationService,
13
+ SprintNotificationError,
14
+ )
15
+ from review_request.config.settings import settings
16
+ from review_request.services.rollbar_service import RollbarService
17
+
18
+ logger = logging.getLogger(__name__)
19
+ router = APIRouter()
20
+
21
+
22
+ @router.post("/webhook")
23
+ async def jira_sprint_webhook(
24
+ request: Request,
25
+ channel_id: str,
26
+ ) -> JSONResponse:
27
+ try:
28
+ webhook_data = await request.json()
29
+ webhook_event = webhook_data.get("webhookEvent", "")
30
+
31
+ logger.info(f"Received Jira webhook: {webhook_event}")
32
+
33
+ if webhook_event != "sprint_closed":
34
+ logger.info(f"Ignoring non-sprint-closed event: {webhook_event}")
35
+ return JSONResponse(
36
+ status_code=200,
37
+ content={
38
+ "status": "ignored",
39
+ "message": f"Event type '{webhook_event}' is not processed",
40
+ },
41
+ )
42
+
43
+ try:
44
+ validated_webhook = JiraSprintWebhook(**webhook_data)
45
+ except Exception as validation_error:
46
+ logger.error(f"Webhook validation failed: {validation_error}")
47
+ RollbarService.report_error(
48
+ exc=validation_error,
49
+ request=request,
50
+ extra_data={
51
+ "operation": "webhook_validation",
52
+ "webhook_event": webhook_event,
53
+ },
54
+ level="warning",
55
+ )
56
+ raise HTTPException(
57
+ status_code=400,
58
+ detail=f"Invalid webhook payload: {str(validation_error)}",
59
+ )
60
+
61
+ sprint = validated_webhook.sprint
62
+
63
+ logger.info(
64
+ f"Processing sprint completion: {sprint.name} (State: {sprint.state})"
65
+ )
66
+
67
+ service = JiraSprintNotificationService(slack_token=settings.bot_token)
68
+
69
+ result = await service.process_webhook(
70
+ channel_id=channel_id,
71
+ webhook_data=webhook_data,
72
+ )
73
+
74
+ return JSONResponse(
75
+ status_code=200,
76
+ content={
77
+ "status": "success",
78
+ "message": "Sprint completion notification sent",
79
+ "sprint_name": sprint.name,
80
+ "channel_id": channel_id,
81
+ "slack_response": result,
82
+ },
83
+ )
84
+
85
+ except SprintNotificationError as e:
86
+ logger.error(f"Sprint notification error: {str(e)}")
87
+
88
+ RollbarService.report_error(
89
+ exc=e,
90
+ request=request,
91
+ extra_data={
92
+ "operation": "sprint_webhook",
93
+ "channel_id": channel_id,
94
+ },
95
+ )
96
+
97
+ raise HTTPException(
98
+ status_code=500, detail=f"Failed to send notification: {str(e)}"
99
+ )
100
+
101
+ except Exception as e:
102
+ logger.error(f"Unexpected error in sprint webhook: {str(e)}", exc_info=True)
103
+
104
+ RollbarService.report_error(
105
+ exc=e,
106
+ request=request,
107
+ extra_data={
108
+ "operation": "sprint_webhook",
109
+ "channel_id": channel_id,
110
+ },
111
+ )
112
+
113
+ raise HTTPException(status_code=500, detail="Internal server error")
114
+
115
+
116
+ @router.post("/notify")
117
+ async def send_sprint_notification(
118
+ request: Request,
119
+ notification_request: SprintNotificationRequest,
120
+ ) -> JSONResponse:
121
+ try:
122
+ logger.info(
123
+ f"Manual sprint notification requested for: "
124
+ f"{notification_request.sprint_name}"
125
+ )
126
+
127
+ service = JiraSprintNotificationService(slack_token=settings.bot_token)
128
+
129
+ result = await service.send_notification(
130
+ channel_id=notification_request.channel_id,
131
+ sprint_name=notification_request.sprint_name,
132
+ start_date=notification_request.start_date,
133
+ end_date=notification_request.end_date,
134
+ completed_by=notification_request.completed_by,
135
+ )
136
+
137
+ return JSONResponse(
138
+ status_code=200,
139
+ content={
140
+ "status": "success",
141
+ "message": "Sprint notification sent successfully",
142
+ "sprint_name": notification_request.sprint_name,
143
+ "channel_id": notification_request.channel_id,
144
+ "slack_response": result,
145
+ },
146
+ )
147
+
148
+ except SprintNotificationError as e:
149
+ logger.error(f"Failed to send manual notification: {str(e)}")
150
+
151
+ RollbarService.report_error(
152
+ exc=e,
153
+ request=request,
154
+ extra_data={
155
+ "operation": "manual_sprint_notification",
156
+ "sprint_name": notification_request.sprint_name,
157
+ },
158
+ )
159
+
160
+ raise HTTPException(
161
+ status_code=500, detail=f"Failed to send notification: {str(e)}"
162
+ )
163
+
164
+ except Exception as e:
165
+ logger.error(
166
+ f"Unexpected error in manual notification: {str(e)}", exc_info=True
167
+ )
168
+
169
+ RollbarService.report_error(
170
+ exc=e,
171
+ request=request,
172
+ extra_data={
173
+ "operation": "manual_sprint_notification",
174
+ "sprint_name": notification_request.sprint_name,
175
+ },
176
+ )
177
+
178
+ raise HTTPException(status_code=500, detail="Internal server error")
@@ -0,0 +1,94 @@
1
+ from fastapi import APIRouter, Form, Request
2
+ from review_request.services.send_message import SendMessage
3
+ from review_request.serializers.conversation_response import ConversationResponse
4
+ from review_request.services.rollbar_service import RollbarService
5
+ from fastapi.responses import JSONResponse
6
+ from typing import Union
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+ router = APIRouter()
11
+
12
+
13
+ @router.post("/", response_model=ConversationResponse)
14
+ async def conversation(
15
+ request: Request,
16
+ token: str = Form(...),
17
+ team_id: str = Form(...),
18
+ team_domain: str = Form(...),
19
+ channel_id: str = Form(...),
20
+ channel_name: str = Form(...),
21
+ user_id: str = Form(...),
22
+ user_name: str = Form(...),
23
+ command: str = Form(...),
24
+ text: str = Form(...),
25
+ response_url: str = Form(...),
26
+ trigger_id: str = Form(...),
27
+ ) -> Union[ConversationResponse, JSONResponse]:
28
+ try:
29
+ RollbarService.add_person_data(user_id=user_id, username=user_name)
30
+ RollbarService.add_custom_data("team_id", team_id)
31
+ RollbarService.add_custom_data("channel_id", channel_id)
32
+ RollbarService.add_custom_data("command", command)
33
+
34
+ logger.info(
35
+ f"Processing request from user {user_id} ({user_name}) in channel {channel_name}"
36
+ )
37
+
38
+ message_service = SendMessage(user_id, text, channel_id)
39
+ await message_service.send()
40
+
41
+ logger.info(f"Successfully processed request from user {user_id}")
42
+
43
+ return ConversationResponse(
44
+ response_type="ephemeral",
45
+ text=":white_check_mark: Your request has been submitted successfully.",
46
+ )
47
+ except ValueError as e:
48
+ logger.warning(f"Validation error for user {user_id}: {str(e)}")
49
+
50
+ RollbarService.report_error(
51
+ exc=e,
52
+ request=request,
53
+ extra_data={
54
+ "user_id": user_id,
55
+ "user_name": user_name,
56
+ "channel_name": channel_name,
57
+ "error_type": "validation_error",
58
+ },
59
+ level="warning",
60
+ )
61
+
62
+ return JSONResponse(
63
+ status_code=200,
64
+ content=ConversationResponse(
65
+ response_type="ephemeral", text=":alert: " + str(e)
66
+ ).model_dump(),
67
+ )
68
+ except Exception as e:
69
+ logger.error(
70
+ f"Unexpected error processing request from user {user_id}: {str(e)}",
71
+ exc_info=True,
72
+ )
73
+
74
+ RollbarService.report_error(
75
+ exc=e,
76
+ request=request,
77
+ extra_data={
78
+ "user_id": user_id,
79
+ "user_name": user_name,
80
+ "channel_name": channel_name,
81
+ "error_type": "unexpected_error",
82
+ },
83
+ )
84
+
85
+ return JSONResponse(
86
+ status_code=200,
87
+ content=ConversationResponse(
88
+ response_type="ephemeral",
89
+ text=":alert: An unexpected error occurred. Please try again.",
90
+ ).model_dump(),
91
+ )
92
+ finally:
93
+ RollbarService.clear_person_data()
94
+ RollbarService.clear_custom_data()
@@ -0,0 +1,101 @@
1
+ from pydantic_settings import BaseSettings
2
+ from dotenv import load_dotenv
3
+ from typing import Dict, List, Any
4
+
5
+ load_dotenv()
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ github_token: str = ""
10
+ bot_token: str = ""
11
+ channel_id: str = ""
12
+ slack_signing_secret: str = ""
13
+ rollbar_access_token: str = ""
14
+ environment: str = "development"
15
+ max_age_pr_days: int = 60
16
+ app_url: str = ""
17
+ jira_site: str = ""
18
+ jira_email: str = ""
19
+ jira_api_token: str = ""
20
+
21
+ class Config:
22
+ env_file = ".env"
23
+
24
+
25
+ settings = Settings()
26
+
27
+ SLACK_TEAM_MAPPINGS: Dict[str, str] = {
28
+ "@ai-hero-bot": "S0AQB912TSQ",
29
+ "@engineering-managers": "S05FPFUQKK6",
30
+ "@squad-yoshi": "S04FX4MPVHU",
31
+ "@hr-integrations": "S06ENMV00FJ",
32
+ "@squad-bounty-hunters": "S04SB339XED",
33
+ "@talent-seniors": "S085MRPUE5A",
34
+ "@squad-night-s-watch": "SQ0LZDLBY",
35
+ "@pre-payroll": "S03MB6L10AU",
36
+ "@core-security": "S063WTUH4J2",
37
+ "@QA team": "S0841B04Q68",
38
+ "@squad-ai-implementation-ml": "S08SJ45EP9V",
39
+ "@squad-eternals-ml": "S07F62YVB8S",
40
+ "@squad-alchemist-ml": "S089EMP46GM",
41
+ "@core-tech": "S063WTUH4J2",
42
+ "@hr-corepayroll": "S06ENMV00FJ",
43
+ "@squad-maplemoney": "S0AHBRANQQM",
44
+ "@squad-wasabi": "S092KN5DQLC",
45
+ }
46
+
47
+ GITHUB_REPOSITORIES: List[Dict[str, Any]] = [
48
+ {
49
+ "url": "https://github.com/Thinkei/ats",
50
+ "base_branch": "master",
51
+ },
52
+ {
53
+ "url": "https://github.com/Thinkei/employment-hero",
54
+ "base_branch": "master",
55
+ },
56
+ {
57
+ "url": "https://github.com/Thinkei/frontend-core",
58
+ "base_branch": "master",
59
+ },
60
+ {
61
+ "url": "https://github.com/Thinkei/career-page",
62
+ "base_branch": "master",
63
+ },
64
+ {
65
+ "url": "https://github.com/Thinkei/smart-match",
66
+ "base_branch": "main",
67
+ },
68
+ ]
69
+
70
+ GITHUB_TEAM_REMINDER_MAPPING: List[Dict[str, Any]] = [
71
+ {
72
+ "channel_id": "C066UGGS2PJ",
73
+ "slack_group_id": "S06DU7PTYLW",
74
+ "github_team": "squad-eternals",
75
+ "max_age_pr_days": settings.max_age_pr_days,
76
+ "remind_date": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
77
+ },
78
+ {
79
+ "channel_id": "C09Q56D8VGX",
80
+ "slack_group_id": "S01T1977RBK",
81
+ "github_team": "squad-helios",
82
+ "max_age_pr_days": settings.max_age_pr_days,
83
+ "remind_date": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
84
+ },
85
+ {
86
+ "channel_id": "C07EU03AEEL",
87
+ "slack_group_id": "S07EBNZ4PRB",
88
+ "github_team": "squad-alchemist",
89
+ "max_age_pr_days": settings.max_age_pr_days,
90
+ "remind_date": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
91
+ },
92
+ ]
93
+
94
+ JIRA_TEAM_REMINDER_MAPPING: List[Dict[str, Any]] = [
95
+ {
96
+ "channel_id": "C066UGGS2PJ",
97
+ "slack_group_id": "S06DU7PTYLW",
98
+ "jira_jql": 'project = ET AND statusCategory = "In Progress" AND duedate < now() ORDER BY duedate ASC',
99
+ "remind_date": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
100
+ },
101
+ ]
@@ -0,0 +1,34 @@
1
+ """Abstract base class for message decorators."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Any, List
5
+ import random
6
+ from .message_formatters import MessageFormatter
7
+
8
+
9
+ class BaseMessageDecorator(ABC):
10
+ """Abstract base class for message decorators."""
11
+
12
+ def __init__(self, review_message: Dict[str, Any]) -> None:
13
+ self.review_message = review_message
14
+ self.formatter = MessageFormatter()
15
+
16
+ @abstractmethod
17
+ def get_templates(self) -> List[str]:
18
+ pass
19
+
20
+ @abstractmethod
21
+ def get_template_placeholders(self) -> Dict[str, Any]:
22
+ pass
23
+
24
+ def _select_random_template(self) -> str:
25
+ templates = self.get_templates()
26
+ return random.choice(templates)
27
+
28
+ def _format_template(self, template: str) -> str:
29
+ placeholders = self.get_template_placeholders()
30
+ return template.format(**placeholders)
31
+
32
+ def message(self) -> str:
33
+ selected_template = self._select_random_template()
34
+ return self._format_template(selected_template)
@@ -0,0 +1,110 @@
1
+ """Jira Overdue Message Decorator."""
2
+
3
+ from typing import Dict, Any, List
4
+ from .base_message_decorator import BaseMessageDecorator
5
+
6
+
7
+ class JiraOverdueMessageDecorator(BaseMessageDecorator):
8
+ """Decorator for generating Jira overdue reminder intro messages."""
9
+
10
+ def __init__(self, team_config: Dict[str, Any]) -> None:
11
+ self.team_config = team_config
12
+ self.slack_group_id = team_config.get("slack_group_id", "")
13
+ self.total_issues = team_config.get("total_issues", 0)
14
+ super().__init__({"type": "jira_overdue"})
15
+
16
+ def get_templates(self) -> List[str]:
17
+ return [
18
+ (
19
+ "Hey {team_mention} 👋\n"
20
+ "⏰ {total} Jira issues are past their due date…\n"
21
+ "Before they start a support group, let's move them forward."
22
+ ),
23
+ (
24
+ "Hey {team_mention} 👋\n"
25
+ "🧟 {total} overdue issues shambling in 'In Progress'…\n"
26
+ "Only your action can lay them to rest."
27
+ ),
28
+ (
29
+ "Hey {team_mention} 👋\n"
30
+ "🐢 {total} tasks moving slower than Monday mornings…\n"
31
+ "A tiny push today saves a big panic tomorrow."
32
+ ),
33
+ (
34
+ "Hey {team_mention} 👋\n"
35
+ "🧀 {total} aging like cheese… and not the fancy kind.\n"
36
+ "Change the status before it gets… aromatic."
37
+ ),
38
+ (
39
+ "Hey {team_mention} 👋\n"
40
+ "🚨 {total} overdue issues pinging the timeline radar\n"
41
+ "Let's clear the airspace."
42
+ ),
43
+ (
44
+ "Hey {team_mention} 👋\n"
45
+ "📦 {total} stories waiting in the delivery truck…\n"
46
+ "A quick update and they're on the road."
47
+ ),
48
+ (
49
+ "Hey {team_mention} 👋\n"
50
+ "🕰️ {total} tickets time-traveled past their due date…\n"
51
+ "Send them back to the present with an update."
52
+ ),
53
+ (
54
+ "Hey {team_mention} 👋\n"
55
+ "🔥 {total} smoldering tasks in 'In Progress'…\n"
56
+ "A quick status douses the flames."
57
+ ),
58
+ (
59
+ "Hey {team_mention} 👋\n"
60
+ "🧩 {total} pieces waiting to click into Done…\n"
61
+ "One move from you completes the puzzle."
62
+ ),
63
+ (
64
+ "Hey {team_mention} 👋\n"
65
+ "🌧️ {total} clouds hanging over the sprint…\n"
66
+ "Mark some sunshine with a status change."
67
+ ),
68
+ (
69
+ "Hey {team_mention} 👋\n"
70
+ "📣 {total} issues calling from the backlog hotline…\n"
71
+ "They'd like to speak to a human."
72
+ ),
73
+ (
74
+ "Hey {team_mention} 👋\n"
75
+ "🛰️ {total} signals from overdue orbit…\n"
76
+ "Bring them back to Earth with an update."
77
+ ),
78
+ (
79
+ "Hey {team_mention} 👋\n"
80
+ "🏃 {total} tasks stuck at mile 25…\n"
81
+ "A tiny push gets them over the finish line."
82
+ ),
83
+ (
84
+ "Hey {team_mention} 👋\n"
85
+ "🔧 {total} work-in-progress needing a quick tune-up…\n"
86
+ "Tighten a bolt, change a status, done."
87
+ ),
88
+ (
89
+ "Hey {team_mention} 👋\n"
90
+ "📅 {total} dates came and went…\n"
91
+ "Let's not let the sprint retrospective do all the talking."
92
+ ),
93
+ (
94
+ "Hey {team_mention} 👋\n"
95
+ "🧭 {total} tasks looking for direction…\n"
96
+ "Point them to Done with a quick update."
97
+ ),
98
+ ]
99
+
100
+ def get_template_placeholders(self) -> Dict[str, Any]:
101
+ return {
102
+ "team_mention": self._format_team_mention(),
103
+ "total": f"*{self.total_issues}*",
104
+ }
105
+
106
+ def _format_team_mention(self) -> str:
107
+ return f"<!subteam^{self.slack_group_id}>!" if self.slack_group_id else "team"
108
+
109
+ def build_intro(self) -> str:
110
+ return self.message()