Skip to content

Commit ba5db24

Browse files
committed
Refactor GitHub timeline event handling: replace GithubPRTimelineEvent with GithubPullRequestTimelineEvents, update related imports, and improve event processing logic
1 parent 12f4426 commit ba5db24

File tree

7 files changed

+180
-197
lines changed

7 files changed

+180
-197
lines changed

backend/analytics_server/mhq/exapi/github.py

Lines changed: 25 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import contextlib
22
from datetime import datetime
33
from http import HTTPStatus
4-
from typing import Any, Optional, Dict, Tuple, List, cast
4+
from typing import Optional, Dict, Tuple, List, cast
55

66
import requests
77

@@ -16,11 +16,9 @@
1616
GitHubPullTimelineEvent,
1717
GitHubPrTimelineEventsDict,
1818
)
19-
from mhq.exapi.models.timeline import GithubPRTimelineEvent
20-
from mhq.store.models.code.enums import PullRequestEventType
2119
from mhq.exapi.models.github import GitHubContributor
20+
from mhq.exapi.models.github_timeline import GithubPullRequestTimelineEvents
2221
from mhq.utils.log import LOG
23-
from mhq.utils.time import dt_from_iso_time_string
2422

2523
PAGE_SIZE = 100
2624

@@ -282,7 +280,7 @@ def _fetch_workflow_runs(page: int = 1):
282280

283281
def get_pr_timeline_events(
284282
self, repo_name: str, pr_number: int
285-
) -> List[GithubPRTimelineEvent]:
283+
) -> List[GithubPullRequestTimelineEvents]:
286284

287285
def _fetch_timeline_events(page: int = 1) -> List[Dict]:
288286
github_url = (
@@ -354,154 +352,30 @@ def _create_timeline_event(event_data: Dict) -> GitHubPrTimelineEventsDict:
354352
HTTPStatus.INTERNAL_SERVER_ERROR, f"Unexpected error: {str(e)}"
355353
) from e
356354

357-
return adapt_github_timeline_events(all_timeline_events)
358-
359-
360-
class Event:
361-
EVENT_CONFIG = {
362-
"reviewed": {
363-
"actor_path": "user",
364-
"timestamp_field": "submitted_at",
365-
"id_path": "id",
366-
},
367-
"ready_for_review": {
368-
"actor_path": "actor",
369-
"timestamp_field": "created_at",
370-
"id_path": "id",
371-
},
372-
"commented": {
373-
"actor_path": "user",
374-
"timestamp_field": "created_at",
375-
"id_path": "id",
376-
},
377-
"committed": {
378-
"actor_path": "author.name",
379-
"timestamp_field": "author.date",
380-
"id_path": "sha",
381-
},
382-
"default": {
383-
"actor_path": "actor",
384-
"timestamp_field": "created_at",
385-
"id_path": "id",
386-
},
387-
}
388-
389-
def __init__(self, event_type: str, data: GitHubPullTimelineEvent):
390-
self.event_type = event_type
391-
self.data = data
392-
393-
def _get_nested_value(self, path: str) -> Optional[Any]:
394-
keys = path.split(".")
395-
current = self.data
396-
397-
for key in keys:
398-
if isinstance(current, dict) and key in current:
399-
current = current[key]
400-
else:
401-
return None
402-
return current
355+
return self._adapt_github_timeline_events(all_timeline_events)
403356

404-
@property
405-
def user(self) -> Optional[str]:
406-
config = self.EVENT_CONFIG.get(self.event_type, self.EVENT_CONFIG["default"])
407-
actor_path = config["actor_path"]
357+
@staticmethod
358+
def _adapt_github_timeline_events(
359+
timeline_events: List[GitHubPrTimelineEventsDict],
360+
) -> List[GithubPullRequestTimelineEvents]:
361+
adapted_timeline_events: List[GithubPullRequestTimelineEvents] = []
408362

409-
if not actor_path:
410-
return None
363+
for timeline_event in timeline_events:
364+
event_data = timeline_event.get("data")
365+
if not event_data:
366+
continue
411367

412-
if self.event_type == "committed":
413-
return self._get_nested_value(actor_path)
368+
event_type = timeline_event.get("event")
369+
if not event_type:
370+
continue
414371

415-
user_data = self._get_nested_value(actor_path)
416-
if not user_data:
417-
return None
418-
if isinstance(user_data, dict) and "login" in user_data:
419-
return user_data["login"]
420-
elif hasattr(user_data, "login"):
421-
return user_data.login
422-
423-
LOG.warning(
424-
f"User data does not contain login field for event type: {self.event_type}"
425-
)
426-
return None
427-
428-
@property
429-
def timestamp(self) -> Optional[datetime]:
430-
config = self.EVENT_CONFIG.get(self.event_type, self.EVENT_CONFIG["default"])
431-
timestamp_field = config["timestamp_field"]
432-
timestamp_value = self._get_nested_value(timestamp_field)
433-
434-
if timestamp_value:
435-
timestamp_str = str(timestamp_value)
436-
return dt_from_iso_time_string(timestamp_str)
437-
return None
438-
439-
@property
440-
def raw_data(self) -> Dict:
441-
return cast(Dict, self.data)
442-
443-
@property
444-
def id(self) -> Optional[str]:
445-
config = self.EVENT_CONFIG.get(self.event_type, self.EVENT_CONFIG["default"])
446-
id_path = config["id_path"]
447-
id_value = self._get_nested_value(id_path)
448-
return str(id_value) if id_value is not None else None
449-
450-
@property
451-
def type(self) -> Optional[PullRequestEventType]:
452-
event_type_mapping = {
453-
"assigned": PullRequestEventType.ASSIGNED,
454-
"closed": PullRequestEventType.CLOSED,
455-
"commented": PullRequestEventType.COMMENTED,
456-
"committed": PullRequestEventType.COMMITTED,
457-
"convert_to_draft": PullRequestEventType.CONVERT_TO_DRAFT,
458-
"head_ref_deleted": PullRequestEventType.HEAD_REF_DELETED,
459-
"head_ref_force_pushed": PullRequestEventType.HEAD_REF_FORCE_PUSHED,
460-
"labeled": PullRequestEventType.LABELED,
461-
"locked": PullRequestEventType.LOCKED,
462-
"merged": PullRequestEventType.MERGED,
463-
"ready_for_review": PullRequestEventType.READY_FOR_REVIEW,
464-
"referenced": PullRequestEventType.REFERENCED,
465-
"reopened": PullRequestEventType.REOPENED,
466-
"review_dismissed": PullRequestEventType.REVIEW_DISMISSED,
467-
"review_requested": PullRequestEventType.REVIEW_REQUESTED,
468-
"review_request_removed": PullRequestEventType.REVIEW_REQUEST_REMOVED,
469-
"reviewed": PullRequestEventType.REVIEW,
470-
"unassigned": PullRequestEventType.UNASSIGNED,
471-
"unlabeled": PullRequestEventType.UNLABELED,
472-
"unlocked": PullRequestEventType.UNLOCKED,
473-
}
474-
return event_type_mapping.get(self.event_type, PullRequestEventType.UNKNOWN)
475-
476-
477-
def adapt_github_timeline_events(
478-
timeline_events: List[GitHubPrTimelineEventsDict],
479-
) -> List[GithubPRTimelineEvent]:
480-
normalized: List[GithubPRTimelineEvent] = []
481-
482-
for timeline_event in timeline_events:
483-
event_data = timeline_event.get("data")
484-
if not event_data:
485-
continue
486-
487-
event_type = timeline_event.get("event")
488-
if not event_type:
489-
continue
490-
491-
event = Event(event_type, event_data)
492-
493-
if all([event.timestamp, event.type, event.id, event.user]):
494-
adapted_event = GithubPRTimelineEvent(
495-
id=cast(str, event.id),
496-
user_login=cast(str, event.user),
497-
type=cast(PullRequestEventType, event.type),
498-
timestamp=cast(datetime, event.timestamp),
499-
raw_data=cast(GitHubPullTimelineEvent, event.raw_data),
500-
)
501-
normalized.append(adapted_event)
502-
else:
503-
LOG.warning(
504-
f"Skipping incomplete timeline event: {event_type} with id: {event.id}"
505-
)
372+
event = GithubPullRequestTimelineEvents(event_type, event_data)
373+
374+
if all([event.timestamp, event.type, event.id, event.user]):
375+
adapted_timeline_events.append(event)
376+
else:
377+
LOG.warning(
378+
f"Skipping incomplete timeline event: {event_type} with id: {event.id}"
379+
)
506380

507-
return normalized
381+
return adapted_timeline_events
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from datetime import datetime
2+
from dataclasses import dataclass
3+
from typing import Any, Optional, Dict, cast
4+
5+
6+
from mhq.exapi.schemas.timeline import (
7+
GitHubPullTimelineEvent,
8+
)
9+
from mhq.store.models.code.enums import PullRequestEventType
10+
from mhq.utils.log import LOG
11+
from mhq.utils.time import dt_from_iso_time_string
12+
13+
14+
@dataclass
15+
class GithubPullRequestTimelineEvents:
16+
EVENT_CONFIG = {
17+
"reviewed": {
18+
"actor_path": "user",
19+
"timestamp_field": "submitted_at",
20+
"id_path": "id",
21+
},
22+
"ready_for_review": {
23+
"actor_path": "actor",
24+
"timestamp_field": "created_at",
25+
"id_path": "id",
26+
},
27+
"commented": {
28+
"actor_path": "user",
29+
"timestamp_field": "created_at",
30+
"id_path": "id",
31+
},
32+
"committed": {
33+
"actor_path": "author.name",
34+
"timestamp_field": "author.date",
35+
"id_path": "sha",
36+
},
37+
"default": {
38+
"actor_path": "actor",
39+
"timestamp_field": "created_at",
40+
"id_path": "id",
41+
},
42+
}
43+
44+
def __init__(self, event_type: str, data: GitHubPullTimelineEvent):
45+
self.event_type = event_type
46+
self.data = data
47+
48+
def _get_nested_value(self, path: str) -> Optional[Any]:
49+
keys = path.split(".")
50+
current = self.data
51+
52+
for key in keys:
53+
if isinstance(current, dict) and key in current:
54+
current = current[key]
55+
else:
56+
return None
57+
return current
58+
59+
@property
60+
def user(self) -> Optional[str]:
61+
config = self.EVENT_CONFIG.get(self.event_type, self.EVENT_CONFIG["default"])
62+
actor_path = config["actor_path"]
63+
64+
if not actor_path:
65+
return None
66+
67+
if self.event_type == "committed":
68+
return self._get_nested_value(actor_path)
69+
70+
user_data = self._get_nested_value(actor_path)
71+
if not user_data:
72+
return None
73+
if isinstance(user_data, dict) and "login" in user_data:
74+
return user_data["login"]
75+
elif hasattr(user_data, "login"):
76+
return user_data.login
77+
78+
LOG.warning(
79+
f"User data does not contain login field for event type: {self.event_type}"
80+
)
81+
return None
82+
83+
@property
84+
def timestamp(self) -> Optional[datetime]:
85+
config = self.EVENT_CONFIG.get(self.event_type, self.EVENT_CONFIG["default"])
86+
timestamp_field = config["timestamp_field"]
87+
timestamp_value = self._get_nested_value(timestamp_field)
88+
89+
if timestamp_value:
90+
timestamp_str = str(timestamp_value)
91+
return dt_from_iso_time_string(timestamp_str)
92+
return None
93+
94+
@property
95+
def raw_data(self) -> Dict:
96+
return cast(Dict, self.data)
97+
98+
@property
99+
def id(self) -> Optional[str]:
100+
config = self.EVENT_CONFIG.get(self.event_type, self.EVENT_CONFIG["default"])
101+
id_path = config["id_path"]
102+
id_value = self._get_nested_value(id_path)
103+
return str(id_value) if id_value is not None else None
104+
105+
@property
106+
def type(self) -> Optional[PullRequestEventType]:
107+
event_type_mapping = {
108+
"assigned": PullRequestEventType.ASSIGNED,
109+
"closed": PullRequestEventType.CLOSED,
110+
"commented": PullRequestEventType.COMMENTED,
111+
"committed": PullRequestEventType.COMMITTED,
112+
"convert_to_draft": PullRequestEventType.CONVERT_TO_DRAFT,
113+
"head_ref_deleted": PullRequestEventType.HEAD_REF_DELETED,
114+
"head_ref_force_pushed": PullRequestEventType.HEAD_REF_FORCE_PUSHED,
115+
"labeled": PullRequestEventType.LABELED,
116+
"locked": PullRequestEventType.LOCKED,
117+
"merged": PullRequestEventType.MERGED,
118+
"ready_for_review": PullRequestEventType.READY_FOR_REVIEW,
119+
"referenced": PullRequestEventType.REFERENCED,
120+
"reopened": PullRequestEventType.REOPENED,
121+
"review_dismissed": PullRequestEventType.REVIEW_DISMISSED,
122+
"review_requested": PullRequestEventType.REVIEW_REQUESTED,
123+
"review_request_removed": PullRequestEventType.REVIEW_REQUEST_REMOVED,
124+
"reviewed": PullRequestEventType.REVIEW,
125+
"unassigned": PullRequestEventType.UNASSIGNED,
126+
"unlabeled": PullRequestEventType.UNLABELED,
127+
"unlocked": PullRequestEventType.UNLOCKED,
128+
}
129+
return event_type_mapping.get(self.event_type, PullRequestEventType.UNKNOWN)

backend/analytics_server/mhq/exapi/models/timeline.py

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)