Skip to content

Commit

Permalink
Add support for review context on name changes (#18)
Browse files Browse the repository at this point in the history
* Implement new NameChangeReviewContext

* Update tests

* Update changelog

* Document new review context models
  • Loading branch information
Jonxslays authored May 13, 2023
1 parent efb4b4e commit 47bafd3
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 10 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
- Remove `NameChangeService.get_name_change_details` as it is no longer supported by WOM.
- Remove models and serialization methods associated with the above method.

## Additions

- Add new `review_context` field to `NameChange`.
- Add `NameChangeReviewContext`, `SkippedNameChangeReviewContext`, and
`DeniedNameChangeReviewContext` models.
- Add `NameChangeReviewReason` enum.
- Add serialization method for the above models.

---

# v0.3.3 (Apr 2023)
Expand Down
10 changes: 6 additions & 4 deletions tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,27 +741,29 @@ def test_set_attrs_cased(set_attrs: mock.MagicMock, blank_class: t.Any) -> None:
serializer._set_attrs_cased(blank_class, {}, "test", "other") # type: ignore

set_attrs.assert_called_once_with(
blank_class, {}, "test", "other", transform=None, camel_case=True
blank_class, {}, "test", "other", transform=None, camel_case=True, maybe=False
)


@mock.patch("wom.serializer.Serializer._set_attrs")
def test_set_attrs_cased_no_attrs(set_attrs: mock.MagicMock, blank_class: t.Any) -> None:
serializer._set_attrs_cased(blank_class, {}) # type: ignore

set_attrs.assert_called_once_with(blank_class, {}, transform=None, camel_case=True)
set_attrs.assert_called_once_with(
blank_class, {}, transform=None, camel_case=True, maybe=False
)


@mock.patch("wom.serializer.Serializer._set_attrs")
def test_set_attrs_cased_transform(set_attrs: mock.MagicMock, blank_class: t.Any) -> None:
transform: t.Callable[[t.Any], t.Any] | None = lambda i: i

serializer._set_attrs_cased( # type: ignore
blank_class, {}, "test", "other", transform=transform
blank_class, {}, "test", "other", transform=transform, maybe=False
)

set_attrs.assert_called_once_with(
blank_class, {}, "test", "other", transform=transform, camel_case=True # type: ignore
blank_class, {}, "test", "other", transform=transform, camel_case=True, maybe=False
)


Expand Down
4 changes: 4 additions & 0 deletions wom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"ComputedMetrics",
"DeltaLeaderboardEntry",
"DeltaService",
"DeniedNameChangeReviewContext",
"EfficiencyService",
"Err",
"Gains",
Expand All @@ -125,6 +126,8 @@
"Metric",
"MetricLeaders",
"NameChange",
"NameChangeReviewContext",
"NameChangeReviewReason",
"NameChangeService",
"NameChangeStatus",
"Ok",
Expand Down Expand Up @@ -152,6 +155,7 @@
"SkillGains",
"SkillLeader",
"Skills",
"SkippedNameChangeReviewContext",
"SnapshotData",
"Snapshot",
"Team",
Expand Down
4 changes: 4 additions & 0 deletions wom/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"ComputedMetric",
"ComputedMetricLeader",
"DeltaLeaderboardEntry",
"DeniedNameChangeReviewContext",
"Gains",
"GroupDetail",
"GroupHiscoresActivityItem",
Expand All @@ -74,6 +75,8 @@
"Membership",
"MetricLeaders",
"NameChange",
"NameChangeReviewContext",
"NameChangeReviewReason",
"NameChangeStatus",
"Participation",
"PlayerAchievementProgress",
Expand All @@ -92,6 +95,7 @@
"Skill",
"SkillGains",
"SkillLeader",
"SkippedNameChangeReviewContext",
"SnapshotData",
"Snapshot",
"Team",
Expand Down
4 changes: 4 additions & 0 deletions wom/models/names/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
from __future__ import annotations

__all__ = (
"DeniedNameChangeReviewContext",
"NameChange",
"NameChangeReviewContext",
"NameChangeReviewReason",
"NameChangeStatus",
"SkippedNameChangeReviewContext",
)

from .enums import *
Expand Down
12 changes: 11 additions & 1 deletion wom/models/names/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from wom.enums import BaseEnum

__all__ = ("NameChangeStatus",)
__all__ = ("NameChangeReviewReason", "NameChangeStatus")


class NameChangeStatus(BaseEnum):
Expand All @@ -32,3 +32,13 @@ class NameChangeStatus(BaseEnum):
Pending = "pending"
Approved = "approved"
Denied = "denied"


class NameChangeReviewReason(BaseEnum):
ManualReview = "manual_review"
OldStatsNotFound = "old_stats_cannot_be_found"
NewNameNotFound = "new_name_not_on_the_hiscores"
NegativeGains = "negative_gains"
TransitionTooLong = "transition_period_too_long"
ExcessiveGains = "excessive_gains"
TotalLevelTooLow = "total_level_too_low"
102 changes: 101 additions & 1 deletion wom/models/names/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,105 @@

import attrs

from wom import enums

from ..base import BaseModel
from .enums import NameChangeReviewReason
from .enums import NameChangeStatus

__all__ = ("NameChange",)
__all__ = (
"DeniedNameChangeReviewContext",
"NameChange",
"NameChangeReviewContext",
"SkippedNameChangeReviewContext",
)


@attrs.define(init=False)
class NameChangeReviewContext(BaseModel):
"""The review context for a name change that was not approved.
!!! note
This will always be one of:
- `DeniedNameChangeReviewContext`
- `SkippedNameChangeReviewContext`
You can use an `isinstance(...)` check to determine which one
it is.
"""

reason: NameChangeReviewReason
"""The reason this name change was denied."""


@attrs.define(init=False)
class DeniedNameChangeReviewContext(NameChangeReviewContext): # type: ignore[override]
"""The review context for a name change that was denied."""

reason: t.Literal[
NameChangeReviewReason.ManualReview,
NameChangeReviewReason.OldStatsNotFound,
NameChangeReviewReason.NewNameNotFound,
NameChangeReviewReason.NegativeGains,
]
"""The reason this name change was denied."""

negative_gains: t.Optional[t.Dict[enums.Metric, int]]
"""The negative gains that were observed, if there were any. Only populated
when the reason is
[`NegativeGains`][wom.NameChangeReviewReason.NegativeGains]"""


@attrs.define(init=False)
class SkippedNameChangeReviewContext(NameChangeReviewContext): # type: ignore[override]
"""The review context for a name change that was skipped."""

reason: t.Literal[
NameChangeReviewReason.TransitionTooLong,
NameChangeReviewReason.ExcessiveGains,
NameChangeReviewReason.TotalLevelTooLow,
]
"""The reason this name change was denied."""

max_hours_diff: t.Optional[int]
"""The max number of hours in the transition period. Only populated when
reason is
[`TransitionTooLong`][wom.NameChangeReviewReason.TransitionTooLong].
"""

hours_diff: t.Optional[int]
"""The actual number of hours in the transition period. Only populated when
reason is
[`TransitionTooLong`][wom.NameChangeReviewReason.TransitionTooLong]
or [`ExcessiveGains`][wom.NameChangeReviewReason.ExcessiveGains].
"""

ehp_diff: t.Optional[int]
"""The number difference between the old and new names ehp. Only populated
when the reason is
[`ExcessiveGains`][wom.NameChangeReviewReason.ExcessiveGains].
"""

ehb_diff: t.Optional[int]
"""The number difference between the old and new names ehb. Only populated
when the reason is
[`ExcessiveGains`][wom.NameChangeReviewReason.ExcessiveGains].
"""

min_total_level: t.Optional[int]
"""The minimum total level allowed for this name change. Only populated
when the reason is
[`TotalLevelTooLow`][wom.NameChangeReviewReason.TotalLevelTooLow].
"""

total_level: t.Optional[int]
"""The number difference between the old and new names ehb. Only populated
when the reason is
[`TotalLevelTooLow`][wom.NameChangeReviewReason.TotalLevelTooLow].
"""


@attrs.define(init=False)
Expand All @@ -51,6 +146,11 @@ class NameChange(BaseModel):
status: NameChangeStatus
"""The [`status`][wom.NameChangeStatus] of the name change."""

review_context: t.Optional[NameChangeReviewContext]
"""The [review context][wom.NameChangeReviewContext] associated with
this name change, if it was denied or skipped.
"""

resolved_at: t.Optional[datetime]
"""The date the name change was approved or denied."""

Expand Down
79 changes: 76 additions & 3 deletions wom/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,32 @@ def _set_attrs(
*attrs: str,
transform: TransformT = None,
camel_case: bool = False,
maybe: bool = False,
) -> None:
if transform and maybe:
raise RuntimeError("Only one of 'maybe' and 'transform' may be used.")

for attr in attrs:
cased_attr = self._to_camel_case(attr) if camel_case else attr

if transform:
setattr(model, attr, transform(data[cased_attr]))
setattr(
model,
attr,
transform(data.get(cased_attr, None) if maybe else data[cased_attr]),
)
else:
setattr(model, attr, data[cased_attr])
setattr(model, attr, data.get(cased_attr, None) if maybe else data[cased_attr])

def _set_attrs_cased(
self,
model: t.Any,
data: DictT,
*attrs: str,
transform: TransformT = None,
maybe: bool = False,
) -> None:
self._set_attrs(model, data, *attrs, transform=transform, camel_case=True)
self._set_attrs(model, data, *attrs, transform=transform, camel_case=True, maybe=maybe)

def _deserialize_base_achievement(self, model: AchievementT, data: DictT) -> AchievementT:
model.metric = enums.Metric.from_str(data["metric"])
Expand Down Expand Up @@ -426,6 +435,64 @@ def deserialize_player_gains(self, data: DictT) -> models.PlayerGains:

return gains

def deserialize_name_change_review_context(
self, data: DictT
) -> models.NameChangeReviewContext:
"""Deserializes the data into a name change review context.
Args:
data: The JSON payload.
Returns:
The requested model.
"""
ctx: models.NameChangeReviewContext
reason = models.NameChangeReviewReason.from_str(data["reason"])

skipped_reasons = (
models.NameChangeReviewReason.TransitionTooLong,
models.NameChangeReviewReason.ExcessiveGains,
models.NameChangeReviewReason.TotalLevelTooLow,
)

denied_reasons = (
models.NameChangeReviewReason.ManualReview,
models.NameChangeReviewReason.OldStatsNotFound,
models.NameChangeReviewReason.NewNameNotFound,
models.NameChangeReviewReason.NegativeGains,
)

if reason in skipped_reasons:
ctx = models.SkippedNameChangeReviewContext()
ctx.reason = reason # type: ignore[assignment]
self._set_attrs_cased(
ctx,
data,
"max_hours_diff",
"hours_diff",
"ehp_diff",
"ehb_diff",
"min_total_level",
"total_level",
maybe=True,
)
elif reason in denied_reasons:
ctx = models.DeniedNameChangeReviewContext()
ctx.reason = reason # type: ignore[assignment]
ctx.negative_gains = None

if reason is models.NameChangeReviewReason.NegativeGains:
negative_gains: t.Dict[enums.Metric, int] = {}

for metric, value in data["negativeGains"].items():
negative_gains[enums.Metric.from_str(metric)] = value

ctx.negative_gains = negative_gains
else:
raise RuntimeError("Unreachable code reached! Serializer::name_change_review_context")

return ctx

def deserialize_name_change(self, data: DictT) -> models.NameChange:
"""Deserializes the data into a name change model.
Expand All @@ -441,6 +508,12 @@ def deserialize_name_change(self, data: DictT) -> models.NameChange:
change.created_at = self._dt_from_iso(data["createdAt"])
change.resolved_at = self._dt_from_iso_maybe(data["createdAt"])
self._set_attrs_cased(change, data, "id", "player_id", "old_name", "new_name")

if review_context := data.get("reviewContext", None):
change.review_context = self.deserialize_name_change_review_context(review_context)
else:
change.review_context = review_context

return change

def deserialize_record(self, data: DictT) -> models.Record:
Expand Down
2 changes: 1 addition & 1 deletion wom/services/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ async def update_outdated_participants(
Participants are outdated when either:
- Competition is ending or started with 6h of now and
- Competition is ending or started within 6h of now and
the player hasn't been updated in over 1h.
- Player hasn't been updated in over 24h.
Expand Down

0 comments on commit 47bafd3

Please sign in to comment.