Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 27 additions & 28 deletions octohook/events.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from enum import Enum
from typing import Optional, List, Any
from typing import Optional, List, Any, Annotated

from octohook.models import (
Repository,
Expand Down Expand Up @@ -35,7 +35,6 @@
Sponsorship,
Branch,
StatusCommit,
RawDict,
Commit,
CommitUser,
_optional,
Expand Down Expand Up @@ -78,12 +77,12 @@ def __init__(self, payload: dict):
class BranchProtectionRuleEvent(BaseWebhookEvent):
payload: dict
rule: Rule
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.rule = Rule(payload.get("rule"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")


class CheckRunEvent(BaseWebhookEvent):
Expand Down Expand Up @@ -278,13 +277,13 @@ class IssueCommentEvent(BaseWebhookEvent):

issue: Issue
comment: Comment
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.issue = Issue(payload.get("issue"))
self.comment = Comment(payload.get("comment"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")


class IssuesEvent(BaseWebhookEvent):
Expand All @@ -293,15 +292,15 @@ class IssuesEvent(BaseWebhookEvent):
"""

issue: Issue
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]
label: Optional[Label]
assignee: Optional[User]
milestone: Optional[Milestone]

def __init__(self, payload: dict):
super().__init__(payload)
self.issue = Issue(payload.get("issue"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")
self.label = _optional(payload, "label", Label)
self.assignee = _optional(payload, "assignee", User)
self.milestone = _optional(payload, "milestone", Milestone)
Expand All @@ -313,12 +312,12 @@ class LabelEvent(BaseWebhookEvent):
"""

label: Label
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.label = Label(payload.get("label"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")


class MarketplacePurchaseEvent(BaseWebhookEvent):
Expand Down Expand Up @@ -385,12 +384,12 @@ class MilestoneEvent(BaseWebhookEvent):
"""

milestone: Milestone
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.milestone = Milestone(payload.get("milestone"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")


class OrganizationEvent(BaseWebhookEvent):
Expand Down Expand Up @@ -451,12 +450,12 @@ class ProjectCardEvent(BaseWebhookEvent):
"""

project_card: ProjectCard
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.project_card = ProjectCard(payload.get("project_card"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")


class ProjectColumnEvent(BaseWebhookEvent):
Expand All @@ -465,12 +464,12 @@ class ProjectColumnEvent(BaseWebhookEvent):
"""

project_column: ProjectColumn
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.project_column = ProjectColumn(payload.get("project_column"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")


class ProjectEvent(BaseWebhookEvent):
Expand Down Expand Up @@ -503,7 +502,7 @@ class PullRequestEvent(BaseWebhookEvent):
pull_request: PullRequest
assignee: Optional[User]
label: Optional[Label]
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]
before: Optional[str]
after: Optional[str]
requested_reviewer: Optional[User]
Expand All @@ -514,7 +513,7 @@ def __init__(self, payload: dict):
self.pull_request = PullRequest(payload.get("pull_request"))
self.assignee = _optional(payload, "assignee", User)
self.label = _optional(payload, "label", Label)
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")
self.before = payload.get("before")
self.after = payload.get("after")
self.requested_reviewer = _optional(payload, "requested_reviewer", User)
Expand All @@ -527,13 +526,13 @@ class PullRequestReviewEvent(BaseWebhookEvent):

review: Review
pull_request: PullRequest
changes: RawDict
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.review = Review(payload.get("review"))
self.pull_request = PullRequest(payload.get("pull_request"))
self.changes = RawDict(payload.get("pull_request"))
self.changes = payload.get("changes")


class PullRequestReviewCommentEvent(BaseWebhookEvent):
Expand All @@ -543,13 +542,13 @@ class PullRequestReviewCommentEvent(BaseWebhookEvent):

comment: Comment
pull_request: PullRequest
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.comment = Comment(payload.get("comment"))
self.pull_request = PullRequest(payload.get("pull_request"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")


class PullRequestReviewThreadEvent(BaseWebhookEvent):
Expand Down Expand Up @@ -604,12 +603,12 @@ class ReleaseEvent(BaseWebhookEvent):
"""

release: Release
changes: RawDict
changes: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
super().__init__(payload)
self.release = Release(payload.get("release"))
self.changes = RawDict(payload.get("release"))
self.changes = payload.get("changes")


class RepositoryDispatchEvent(BaseWebhookEvent):
Expand All @@ -618,13 +617,13 @@ class RepositoryDispatchEvent(BaseWebhookEvent):
"""

branch: str
client_payload: RawDict
client_payload: Annotated[dict, "unstructured"]
installation: ShortInstallation

def __init__(self, payload: dict):
super().__init__(payload)
self.branch = payload.get("branch")
self.client_payload = RawDict(payload.get("client_payload"))
self.client_payload = payload.get("client_payload")
self.installation = ShortInstallation(payload.get("installation"))


Expand Down Expand Up @@ -682,13 +681,13 @@ class SponsorshipEvent(BaseWebhookEvent):
"""

sponsorship: Sponsorship
changes: Optional[RawDict]
changes: Optional[Annotated[dict, "unstructured"]]
effective_date: Optional[str]

def __init__(self, payload: dict):
super().__init__(payload)
self.sponsorship = Sponsorship(payload.get("sponsorship"))
self.changes = _optional(payload, "changes", RawDict)
self.changes = payload.get("changes")
self.effective_date = payload.get("effective_date", None)


Expand Down
62 changes: 39 additions & 23 deletions octohook/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
"""
GitHub webhook model classes.

This module uses Annotated[dict, "unstructured"] to mark intentionally unstructured
dictionary data in webhook payloads. This annotation serves two purposes:

1. Documentation: Clearly indicates fields containing variable or user-defined data
2. Type safety: Tests enforce that all dicts are either annotated or proper model classes

When to use Annotated[dict, "unstructured"]:
- GitHub's variable payload structures (e.g., 'changes' field format varies by event)
- User-defined data (e.g., deployment payloads, client_payload)
- Hypermedia links (_links fields)
- Configuration dictionaries (webhook config)
- Error details with varying structures

When to create a model class instead:
- Structured GitHub data with consistent fields across events
- Data that benefits from type hints and IDE autocomplete
"""
from abc import ABC
from typing import TypeVar, Optional, Type, List, Any
from typing import TypeVar, Optional, Type, List, Any, Annotated

T = TypeVar("T")

Expand Down Expand Up @@ -137,11 +157,6 @@ def __str__(self):
return self.full_name


class RawDict(dict):
def __init__(self, payload: dict):
super().__init__(payload)


class Permissions(BaseGithubModel):
payload: dict
metadata: str
Expand Down Expand Up @@ -486,7 +501,8 @@ class Comment(BaseGithubModel):
start_side: Optional[str]
original_line: Optional[int]
side: Optional[str]
reactions: Optional[RawDict]
reactions: Optional[Annotated[dict, "unstructured"]]
_links: Optional[Annotated[dict, "unstructured"]]

def __init__(self, payload: dict):
self.payload = payload
Expand All @@ -509,13 +525,13 @@ def __init__(self, payload: dict):
self.updated_at = payload.get("updated_at")
self.author_association = payload.get("author_association")
self.body = payload.get("body")
self._links = _optional(payload, "_links", RawDict)
self._links = payload.get("_links")
self.start_line = payload.get("start_line")
self.original_start_line = payload.get("original_start_line")
self.start_side = payload.get("start_side")
self.original_line = payload.get("original_line")
self.side = payload.get("side")
self.reactions = _optional(payload, "reactions", RawDict)
self.reactions = payload.get("reactions")

def __str__(self):
return self.body
Expand Down Expand Up @@ -566,16 +582,16 @@ class ChecksPullRequest(BaseGithubModel):
url: str
id: int
number: int
head: RawDict
base: RawDict
head: Annotated[dict, "unstructured"]
base: Annotated[dict, "unstructured"]

def __init__(self, payload: dict):
self.payload = payload
self.url = payload.get("url")
self.id = payload.get("id")
self.number = payload.get("number")
self.head = RawDict(payload.get("head"))
self.base = RawDict(payload.get("base"))
self.head = payload.get("head")
self.base = payload.get("base")


class CommitUser(BaseGithubModel):
Expand Down Expand Up @@ -796,7 +812,7 @@ class Deployment(BaseGithubModel):
sha: str
ref: str
task: str
payload: RawDict
payload: Annotated[dict, "unstructured"]
original_environment: str
environment: str
description: Optional[str]
Expand All @@ -814,7 +830,7 @@ def __init__(self, payload: dict):
self.sha = payload.get("sha")
self.ref = payload.get("ref")
self.task = payload.get("task")
self.payload = RawDict(payload.get("payload"))
self.payload = payload.get("payload")
self.original_environment = payload.get("original_environment")
self.environment = payload.get("environment")
self.description = payload.get("description")
Expand Down Expand Up @@ -1098,7 +1114,7 @@ class Hook(BaseGithubModel):
name: str
active: bool
events: List[str]
config: RawDict
config: Annotated[dict, "unstructured"]
updated_at: str
created_at: str

Expand All @@ -1109,7 +1125,7 @@ def __init__(self, payload: dict):
self.name = payload.get("name")
self.active = payload.get("active")
self.events = payload.get("events")
self.config = RawDict(payload.get("config"))
self.config = payload.get("config")
self.updated_at = payload.get("updated_at")
self.created_at = payload.get("created_at")

Expand Down Expand Up @@ -1329,7 +1345,7 @@ class PageBuild(BaseGithubModel):
payload: dict
url: str
status: str
error: RawDict
error: Annotated[dict, "unstructured"]
pusher: User
commit: str
duration: int
Expand All @@ -1340,7 +1356,7 @@ def __init__(self, payload: dict):
self.payload = payload
self.url = payload.get("url")
self.status = payload.get("status")
self.error = RawDict(payload.get("error"))
self.error = payload.get("error")
self.pusher = User(payload.get("pusher"))
self.commit = payload.get("commit")
self.duration = payload.get("duration")
Expand Down Expand Up @@ -1488,7 +1504,7 @@ class PullRequest(BaseGithubModel):
statuses_url: str
head: Ref
base: Ref
_links: RawDict
_links: Annotated[dict, "unstructured"]
author_association: str
draft: bool
merged: Optional[bool]
Expand Down Expand Up @@ -1540,7 +1556,7 @@ def __init__(self, payload: dict):
self.statuses_url = payload.get("statuses_url")
self.head = Ref(payload.get("head"))
self.base = Ref(payload.get("base"))
self._links = RawDict(payload.get("_links"))
self._links = payload.get("_links")
self.author_association = payload.get("author_association")
self.draft = payload.get("draft")
self.merged = payload.get("merged")
Expand Down Expand Up @@ -1577,7 +1593,7 @@ class Review(BaseGithubModel):
html_url: str
pull_request_url: str
author_association: str
_links: RawDict
_links: Annotated[dict, "unstructured"]

def __init__(self, payload: dict):
self.payload = payload
Expand All @@ -1591,7 +1607,7 @@ def __init__(self, payload: dict):
self.html_url = payload.get("html_url")
self.pull_request_url = payload.get("pull_request_url")
self.author_association = payload.get("author_association")
self._links = RawDict(payload.get("_links"))
self._links = payload.get("_links")


class VulnerabilityAlert(BaseGithubModel):
Expand Down
Loading