From 4d74373aabafe84012ec04096f154b95513092f9 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:22:56 -0700 Subject: [PATCH] Add support for metric leaders (#6) * Bump minor version * Implement metric leaders * Add changelog * Update client example * Add script for verifying dunder all is correct --- CHANGELOG.md | 40 ++++++++++++ noxfile.py | 6 ++ pyproject.toml | 2 +- scripts/alls.py | 28 +++++++++ wom/__init__.py | 25 +++++++- wom/client.py | 5 +- wom/constants.py | 2 + wom/models/__init__.py | 7 ++- wom/models/groups/__init__.py | 5 ++ wom/models/groups/models.py | 112 +++++++++++++++++++++++++++++++-- wom/models/players/__init__.py | 1 - wom/models/players/models.py | 21 ------- wom/serializer.py | 98 ++++++++++++++++++++++++----- 13 files changed, 301 insertions(+), 51 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 scripts/alls.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ce73c4a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# v0.2.0 (Feb 2023) + +## Bugfixes + +- Add some missing models to `__all__`. + +## Additions + +- Add leaders models: `SkillLeader`, `BossLeader`, `ActivityLeader`, `ComputedMetricLeader`, and + `MetricLeaders`. +- Add `metric_leaders` property to `GroupStatistics`. +- Add deserialization methods for the new leader models. + +## Changes + +- `GroupStatistics.average_stats` is now a `Snapshot` rather than a `GroupSnapshot`. + +## Removals + +- Remove `GroupSnapshot` model since `created_at` on `Snapshot` is now guaranteed to be present. + +--- + +# v0.1.1 (Feb 2023) + +## Bugfixes + +- `EfficiencyService.get_global_leaderboard` now accepts a `both` kwarg, and will no longer + erroneously allow you to pass many computed metrics as `*args`. + +## Changes + +- Relaxed the pinned dependencies for better compatibility. +- The `metric` parameter to `EfficiencyService.get_global_leaderboard` is now defaulted to EHP. + +--- + +# v0.1.0 (Feb 2023) + +- Initial release! diff --git a/noxfile.py b/noxfile.py index 8da68ff..f953463 100644 --- a/noxfile.py +++ b/noxfile.py @@ -144,3 +144,9 @@ def licensing(session: nox.Session) -> None: "\nThe following files are missing license attribution:\n" + "\n".join(f" - {m}" for m in missing) ) + + +@nox.session(reuse_venv=True) +def alls(session: nox.Session) -> None: + session.install(".") + session.run("python", "scripts/alls.py") diff --git a/pyproject.toml b/pyproject.toml index bf6e073..d96ae51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wom.py" -version = "0.1.1" +version = "0.2.0" description = "An asynchronous wrapper for the Wise Old Man API." authors = ["Jonxslays"] license = "MIT" diff --git a/scripts/alls.py b/scripts/alls.py new file mode 100644 index 0000000..1e6eade --- /dev/null +++ b/scripts/alls.py @@ -0,0 +1,28 @@ +import typing as t + + +def validate_alls() -> None: + import wom + + should_include_module: t.Callable[[str], bool] = lambda m: ( + m != "annotations" and m[0] != "_" and m[0].upper() != m[0] + ) + + modules_all: set[str] = set() + modules = [m for m in wom.__dict__ if should_include_module(m)] + modules_all.update(item for module in modules for item in wom.__dict__[module].__all__) + lib_all = set(i for i in wom.__all__ if i not in modules) + + if missing := modules_all.difference(lib_all): + raise Exception( + "Missing exported items at top level:\n" + "\n".join(f" - {m}" for m in missing) + ) + + if missing := lib_all.difference(modules_all): + raise Exception( + "Missing exported items at module level:\n" + "\n".join(f" - {m}" for m in missing) + ) + + +if __name__ == "__main__": + validate_alls() diff --git a/wom/__init__.py b/wom/__init__.py index d917bca..4ef32b8 100644 --- a/wom/__init__.py +++ b/wom/__init__.py @@ -46,10 +46,15 @@ "AchievementProgress", "Activities", "Activity", + "ActivityGains", + "ActivityLeader", "BaseEnum", + "BaseModel", + "BaseService", "Boss", + "BossGains", + "BossLeader", "Bosses", - "BaseModel", "Client", "Country", "Competition", @@ -58,13 +63,18 @@ "CompetitionParticipationDetail", "CompetitionParticipation", "CompetitionProgress", + "CompetitionService", "CompetitionStatus", "CompetitionType", "CompetitionWithParticipations", "CompiledRoute", + "ComputedGains", "ComputedMetric", + "ComputedMetricLeader", "ComputedMetrics", "DeltaLeaderboardEntry", + "DeltaService", + "EfficiencyService", "Err", "Gains", "GroupDetail", @@ -77,14 +87,18 @@ "GroupMembership", "Group", "GroupRole", + "GroupService", "GroupStatistics", "HttpErrorResponse", + "HttpService", "HttpSuccessResponse", "Membership", "Metric", + "MetricLeaders", "NameChangeData", "NameChangeDetail", "NameChange", + "NameChangeService", "NameChangeStatus", "Ok", "Participation", @@ -92,21 +106,26 @@ "PlayerAchievementProgress", "PlayerBuild", "PlayerCompetitionStanding", + "PlayerGains", + "PlayerGainsData", "PlayerMembership", "Player", "PlayerDetail", "PlayerParticipation", + "PlayerService", "PlayerType", "Record", + "RecordService", "RecordLeaderboardEntry", "Result", "Route", "Serializer", "Skill", + "SkillGains", + "SkillLeader", "Skills", "SnapshotData", "Snapshot", - "StatisticsSnapshot", "Team", "Top5ProgressResult", "UnwrapError", @@ -114,7 +133,7 @@ ) __packagename__: Final[str] = "wom.py" -__version__: Final[str] = "0.1.1" +__version__: Final[str] = "0.2.0" __author__: Final[str] = "Jonxslays" __copyright__: Final[str] = "2023-present Jonxslays" __description__: Final[str] = "An asynchronous wrapper for the Wise Old Man API." diff --git a/wom/client.py b/wom/client.py index e64a8ff..f7997ec 100644 --- a/wom/client.py +++ b/wom/client.py @@ -82,10 +82,9 @@ class Client: import wom client = wom.Client( - environ["WOM_API_KEY"], # The WOM api key if you have one - user_agent="@me#1234", # Identifier, i.e. discord username + environ["WOM_API_KEY"], + user_agent="@me#1234", api_base_url=environ["LOCAL_WOM_DOMAIN"], - # Typically you won't need to change the base url ) # ... Use the client diff --git a/wom/constants.py b/wom/constants.py index cda4e04..835a93f 100644 --- a/wom/constants.py +++ b/wom/constants.py @@ -25,6 +25,8 @@ import wom +__all__ = () + WOM_BASE_URL: Final[str] = "https://api.wiseoldman.net/v2" USER_AGENT_BASE: Final[str] = f"(wom.py v{wom.__version__}) -" DEFAULT_USER_AGENT: Final[str] = f"{USER_AGENT_BASE} No contact info provided" diff --git a/wom/models/__init__.py b/wom/models/__init__.py index 01bec63..9f395c4 100644 --- a/wom/models/__init__.py +++ b/wom/models/__init__.py @@ -38,9 +38,11 @@ "AchievementProgress", "Activity", "ActivityGains", + "ActivityLeader", "BaseModel", "Boss", "BossGains", + "BossLeader", "Country", "Competition", "CompetitionDetail", @@ -53,6 +55,7 @@ "CompetitionWithParticipations", "ComputedGains", "ComputedMetric", + "ComputedMetricLeader", "DeltaLeaderboardEntry", "Gains", "GroupDetail", @@ -69,6 +72,7 @@ "HttpErrorResponse", "HttpSuccessResponse", "Membership", + "MetricLeaders", "NameChangeData", "NameChangeDetail", "NameChange", @@ -80,6 +84,7 @@ "PlayerMembership", "Player", "PlayerDetail", + "PlayerGains", "PlayerGainsData", "PlayerParticipation", "PlayerType", @@ -87,9 +92,9 @@ "RecordLeaderboardEntry", "Skill", "SkillGains", + "SkillLeader", "SnapshotData", "Snapshot", - "StatisticsSnapshot", "Team", "Top5ProgressResult", ) diff --git a/wom/models/groups/__init__.py b/wom/models/groups/__init__.py index 40328b0..2779700 100644 --- a/wom/models/groups/__init__.py +++ b/wom/models/groups/__init__.py @@ -24,6 +24,9 @@ from __future__ import annotations __all__ = ( + "ActivityLeader", + "BossLeader", + "ComputedMetricLeader", "GroupDetail", "GroupHiscoresActivityItem", "GroupHiscoresBossItem", @@ -36,7 +39,9 @@ "GroupRole", "GroupStatistics", "Membership", + "MetricLeaders", "PlayerMembership", + "SkillLeader", ) from .enums import * diff --git a/wom/models/groups/models.py b/wom/models/groups/models.py index ddd0902..9df2858 100644 --- a/wom/models/groups/models.py +++ b/wom/models/groups/models.py @@ -25,12 +25,17 @@ import attrs +from wom import enums + from ..base import BaseModel from ..players import Player -from ..players import StatisticsSnapshot +from ..players import Snapshot from .enums import GroupRole __all__ = ( + "ActivityLeader", + "BossLeader", + "ComputedMetricLeader", "GroupDetail", "GroupHiscoresActivityItem", "GroupHiscoresBossItem", @@ -42,7 +47,9 @@ "Group", "GroupStatistics", "Membership", + "MetricLeaders", "PlayerMembership", + "SkillLeader", ) @@ -233,6 +240,100 @@ class GroupHiscoresComputedMetricItem(BaseModel): """The value of the computed metric.""" +@attrs.define(init=False) +class SkillLeader(BaseModel): + """Represents a leader in a particular skill.""" + + metric: enums.Skills + """The [`Skills`][wom.Skills] being measured.""" + + rank: int + """The players rank in the skill.""" + + level: int + """The players level in the skill.""" + + experience: int + """The players experience in the skill.""" + + player: Player + """The player leading in this metric.""" + + +@attrs.define(init=False) +class BossLeader(BaseModel): + """Represents a leader in a particular boss.""" + + metric: enums.Bosses + """The [`Bosses`][wom.Bosses] being measured.""" + + rank: int + """The players rank in killing the boss.""" + + kills: int + """The number of kills the player has.""" + + player: Player + """The player leading in this metric.""" + + +@attrs.define(init=False) +class ActivityLeader(BaseModel): + """Represents a leader in a particular activity.""" + + metric: enums.Activities + """The [`Activities`][wom.Activities] being measured.""" + + rank: int + """The players rank in the activity.""" + + score: int + """The players score in the activity.""" + + player: Player + """The player leading in this metric.""" + + +@attrs.define(init=False) +class ComputedMetricLeader(BaseModel): + """Represents a leader in a particular computed metric.""" + + metric: enums.ComputedMetrics + """The [`ComputedMetrics`][wom.ComputedMetrics] being + measured. + """ + + rank: int + """The players rank in the computed metric.""" + + value: int + """The value of the computed metric.""" + + player: Player + """The player leading in this metric.""" + + +@attrs.define(init=False) +class MetricLeaders(BaseModel): + """The leaders for each metric in a group.""" + + skills: list[SkillLeader] + """A list of [`SkillLeader`][wom.SkillLeader]'s for each skill.""" + + bosses: list[BossLeader] + """A list of all [`BossLeader`][wom.BossLeader]'s for each boss.""" + + activities: list[ActivityLeader] + """A list of all [`ActivityLeader`][wom.ActivityLeader]'s for each + activity. + """ + + computed: list[ComputedMetricLeader] + """A list of all [`ComputedMetricLeader`] + [wom.ComputedMetricLeader]'s for each computed metric. + """ + + @attrs.define(init=False) class GroupStatistics(BaseModel): """Represents accumulated group statistics.""" @@ -246,7 +347,10 @@ class GroupStatistics(BaseModel): maxed_200ms_count: int """The number of maxed 200M xp players in the group.""" - average_stats: StatisticsSnapshot - """The average stat [`StatisticsSnapshot`] - [wom.StatisticsSnapshot]. + average_stats: Snapshot + """The average group statistics in a [`Snapshot`][wom.Snapshot].""" + + metric_leaders: MetricLeaders + """The [`MetricLeader`][wom.MetricLeaders]'s in this group for each + metric. """ diff --git a/wom/models/players/__init__.py b/wom/models/players/__init__.py index 049e185..2ecb2b7 100644 --- a/wom/models/players/__init__.py +++ b/wom/models/players/__init__.py @@ -47,7 +47,6 @@ "Skill", "SnapshotData", "Snapshot", - "StatisticsSnapshot", ) from .enums import * diff --git a/wom/models/players/models.py b/wom/models/players/models.py index d086c41..98a634b 100644 --- a/wom/models/players/models.py +++ b/wom/models/players/models.py @@ -49,7 +49,6 @@ "PlayerGains", "Player", "PlayerDetail", - "StatisticsSnapshot", "SkillGains", "Skill", "SnapshotData", @@ -169,26 +168,6 @@ class Snapshot(BaseModel): """The date the snapshot was created.""" -@attrs.define(init=False) -class StatisticsSnapshot(BaseModel): - """Represents a player statistics snapshot.""" - - id: int - """The unique ID of the snapshot.""" - - player_id: int - """The unique ID of the player for this snapshot.""" - - imported_at: datetime | None - """The date the snapshot was imported, if it was.""" - - data: SnapshotData - """The [`SnapshotData`][wom.SnapshotData] for the snapshot.""" - - created_at: datetime | None - """The optional date the statistics snapshot was created.""" - - @attrs.define(init=False) class Player(BaseModel): """Represents a player on WOM.""" diff --git a/wom/serializer.py b/wom/serializer.py index a15694b..ae69222 100644 --- a/wom/serializer.py +++ b/wom/serializer.py @@ -172,22 +172,6 @@ def deserialize_snapshot(self, data: dict[str, t.Any]) -> models.Snapshot: self._set_attrs_cased(snapshot, data, "id", "player_id") return snapshot - def deserialize_statistics_snapshot(self, data: dict[str, t.Any]) -> models.StatisticsSnapshot: - """Deserializes the data into a statistics snapshot model. - - Args: - data: The JSON payload. - - Returns: - The requested model. - """ - snapshot = models.StatisticsSnapshot() - snapshot.created_at = self._dt_from_iso_maybe(data["createdAt"]) - snapshot.imported_at = self._dt_from_iso_maybe(data.get("importedAt")) - snapshot.data = self.deserialize_snapshot_data(data["data"]) - self._set_attrs_cased(snapshot, data, "id", "player_id") - return snapshot - def gather( self, serializer: t.Callable[[dict[str, t.Any]], T], data: list[dict[str, t.Any]] ) -> list[T]: @@ -734,7 +718,8 @@ def deserialize_group_statistics(self, data: dict[str, t.Any]) -> models.GroupSt """ statistics = models.GroupStatistics() statistics.maxed_200ms_count = data["maxed200msCount"] - statistics.average_stats = self.deserialize_statistics_snapshot(data["averageStats"]) + statistics.average_stats = self.deserialize_snapshot(data["averageStats"]) + statistics.metric_leaders = self.deserialize_metric_leaders(data["metricLeaders"]) self._set_attrs_cased(statistics, data, "maxed_total_count", "maxed_combat_count") return statistics @@ -952,3 +937,82 @@ def deserialize_competition_with_participation( ) return model + + def deserialize_skill_leader(self, data: dict[str, t.Any]) -> models.SkillLeader: + """Deserializes the data into a skill leader model. + + Args: + data: The JSON payload. + + Returns: + The requested model. + """ + leader = models.SkillLeader() + leader.metric = enums.Skills.from_str(data["metric"]) + leader.player = self.deserialize_player(data["player"]) + self._set_attrs(leader, data, "experience", "rank", "level") + return leader + + def deserialize_boss_leader(self, data: dict[str, t.Any]) -> models.BossLeader: + """Deserializes the data into a boss leader model. + + Args: + data: The JSON payload. + + Returns: + The requested model. + """ + leader = models.BossLeader() + leader.metric = enums.Bosses.from_str(data["metric"]) + leader.player = self.deserialize_player(data["player"]) + self._set_attrs(leader, data, "kills", "rank") + return leader + + def deserialize_activity_leader(self, data: dict[str, t.Any]) -> models.ActivityLeader: + """Deserializes the data into an activity leader model. + + Args: + data: The JSON payload. + + Returns: + The requested model. + """ + leader = models.ActivityLeader() + leader.metric = enums.Activities.from_str(data["metric"]) + leader.player = self.deserialize_player(data["player"]) + self._set_attrs(leader, data, "score", "rank") + return leader + + def deserialize_computed_leader(self, data: dict[str, t.Any]) -> models.ComputedMetricLeader: + """Deserializes the data into a computed metric leader model. + + Args: + data: The JSON payload. + + Returns: + The requested model. + """ + leader = models.ComputedMetricLeader() + leader.metric = enums.ComputedMetrics.from_str(data["metric"]) + leader.player = self.deserialize_player(data["player"]) + self._set_attrs(leader, data, "value", "rank") + return leader + + def deserialize_metric_leaders(self, data: dict[str, t.Any]) -> models.MetricLeaders: + """Deserializes the data into a metric leaders model model. + + Args: + data: The JSON payload. + + Returns: + The requested model. + """ + leaders = models.MetricLeaders() + leaders.skills = self.gather(self.deserialize_skill_leader, data["skills"].values()) + leaders.bosses = self.gather(self.deserialize_boss_leader, data["bosses"].values()) + leaders.computed = self.gather(self.deserialize_computed_leader, data["computed"].values()) + leaders.activities = self.gather( + self.deserialize_activity_leader, data["activities"].values() + ) + + return leaders