Skip to content

Commit

Permalink
feat: added last_updated_at in players search results and player summ…
Browse files Browse the repository at this point in the history
…ary (#184)
  • Loading branch information
TeKrop authored Aug 25, 2024
1 parent f9d17e4 commit 9325b4f
Show file tree
Hide file tree
Showing 20 changed files with 116 additions and 33 deletions.
6 changes: 5 additions & 1 deletion app/commands/update_search_data_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
cache_manager = CacheManager()

# Mapping between the search data type and the variable name in JS
variable_name_mapping: dict[SearchDataType, str] = {
variable_name_mapping: dict[SearchDataType, str | None] = {
SearchDataType.PORTRAIT: "avatars",
SearchDataType.NAMECARD: "namecards",
SearchDataType.TITLE: "titles",
Expand All @@ -37,6 +37,9 @@ def get_search_page() -> httpx.Response:

def extract_search_data(html_content: str, data_type: SearchDataType) -> dict:
variable_name = variable_name_mapping[data_type]
if not variable_name:
return None

data_regexp = rf"const {variable_name} = (\{{.*\}})\n"

result = re.search(data_regexp, html_content)
Expand Down Expand Up @@ -99,6 +102,7 @@ def update_search_data_cache():
search_data = {
data_type: retrieve_search_data(data_type, search_page)
for data_type in SearchDataType
if data_type != SearchDataType.LAST_UPDATED_AT
}
except SearchDataRetrievalError as error:
raise SystemExit from error
Expand Down
1 change: 1 addition & 0 deletions app/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,4 @@ class SearchDataType(StrEnum):
NAMECARD = "namecard"
PORTRAIT = "portrait"
TITLE = "title"
LAST_UPDATED_AT = "lastUpdatedAt"
14 changes: 10 additions & 4 deletions app/handlers/get_player_career_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from typing import ClassVar

from app.common.enums import SearchDataType
from app.config import settings
from app.parsers.player_parser import PlayerParser
from app.parsers.search_data_parser import NamecardParser
from app.parsers.search_data_parser import LastUpdatedAtParser, NamecardParser

from .api_request_handler import APIRequestHandler

Expand All @@ -15,12 +16,13 @@ class GetPlayerCareerRequestHandler(APIRequestHandler):
PlayerParser class.
"""

parser_classes: ClassVar[list] = [PlayerParser, NamecardParser]
parser_classes: ClassVar[list] = [PlayerParser, NamecardParser, LastUpdatedAtParser]
timeout = settings.career_path_cache_timeout

def merge_parsers_data(self, parsers_data: list[dict], **kwargs) -> dict:
"""Merge parsers data together : PlayerParser for statistics data,
and NamecardParser for namecard (not here in career page)
NamecardParser for namecard and LastUpdatedAtParser
for last time the player profile was updated
"""

# If the user asked for stats, no need to add the namecard
Expand All @@ -31,14 +33,18 @@ def merge_parsers_data(self, parsers_data: list[dict], **kwargs) -> dict:
summary = (
parsers_data[0] if kwargs.get("summary") else parsers_data[0]["summary"]
)
namecard_value = parsers_data[1]["namecard"]
namecard_value = parsers_data[1][SearchDataType.NAMECARD]
last_updated_at_value = parsers_data[2][SearchDataType.LAST_UPDATED_AT]

# We want to insert the namecard before "title" key in "summary"
title_pos = list(summary.keys()).index("title")
summary_items = list(summary.items())
summary_items.insert(title_pos, ("namecard", namecard_value))
summary = dict(summary_items)

# We can insert the last_updated_at at the end
summary["last_updated_at"] = last_updated_at_value

if kwargs.get("summary"):
return summary

Expand Down
1 change: 1 addition & 0 deletions app/handlers/search_players_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def apply_transformations(self, players: Iterable[dict]) -> list[dict]:
"title": self.get_title(player, player_id),
"career_url": f"{settings.app_base_url}/players/{player_id}",
"blizzard_id": player["url"],
"last_updated_at": player["lastUpdated"],
},
)
return transformed_players
Expand Down
18 changes: 18 additions & 0 deletions app/models/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ class PlayerShort(BaseModel):
description="Blizzard unique identifier of the player (hexadecimal)",
examples=["c65b8798bc61d6ffbba120%7Ccfe9dd77a4382165e2b920bdcc035949"],
)
last_updated_at: int | None = Field(
None,
title="Timestamp",
description=(
"Last time the player profile was updated on Blizzard (timestamp). "
"Can be null if couldn't retrieve any"
),
examples=[1704209332],
)


class PlayerSearchResult(BaseModel):
Expand Down Expand Up @@ -227,6 +236,15 @@ class PlayerSummary(BaseModel):
"or if the player doesn't play competitive at all, it's null."
),
)
last_updated_at: int | None = Field(
None,
title="Timestamp",
description=(
"Last time the player profile was updated on Blizzard (timestamp). "
"Can be null if couldn't retrieve any"
),
examples=[1704209332],
)


class HeroesComparisons(BaseModel):
Expand Down
9 changes: 9 additions & 0 deletions app/parsers/search_data_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,12 @@ class TitleParser(SearchDataParser):
"""Title Parser class"""

data_type = SearchDataType.TITLE


class LastUpdatedAtParser(SearchDataParser):
"""LastUpdatedAt Parser class"""

data_type = SearchDataType.LAST_UPDATED_AT

def retrieve_data_value(self, player_data: dict) -> int:
return player_data["lastUpdated"]
2 changes: 1 addition & 1 deletion app/routers/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def search_players(
order_by: str = Query(
"name:asc",
title="Ordering field and the way it's arranged (asc[ending]/desc[ending])",
pattern=r"^(player_id|name):(asc|desc)$",
pattern=r"^(player_id|name|last_updated_at):(asc|desc)$",
),
offset: int = Query(0, title="Offset of the results", ge=0),
limit: int = Query(20, title="Limit of results per page", gt=0),
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "overfast-api"
version = "2.34.1"
version = "2.35.0"
description = "Overwatch API giving data about heroes, maps, and players statistics."
license = {file = "LICENSE"}
authors = [
Expand Down Expand Up @@ -125,4 +125,4 @@ allowed-confusables = ["(", ")", ":"]

[tool.ruff.lint.isort]
# Consider app as first-party for imports in tests
known-first-party = ["app"]
known-first-party = ["app"]
3 changes: 2 additions & 1 deletion tests/fixtures/json/players/Dekk-2677.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"season": 8
},
"console": null
}
},
"last_updated_at": null
},
"stats": {
"pc": {
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/json/players/JohnV1-1190.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"open": null
},
"console": null
}
},
"last_updated_at": null
},
"stats": {
"pc": {
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/json/players/KIRIKO-21253.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"open": null,
"season": 1
}
}
},
"last_updated_at": null
},
"stats": {
"pc": {
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/json/players/Player-1112937.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"support": null,
"open": null
}
}
},
"last_updated_at": null
},
"stats": {
"pc": {
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/json/players/TeKrop-2217.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"open": null
},
"console": null
}
},
"last_updated_at": 1678536999
},
"stats": {
"pc": {
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/json/players/copypasting-1216.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"open": null
},
"console": null
}
},
"last_updated_at": null
},
"stats": {
"pc": {
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/json/players/quibble-11594.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"open": null
},
"console": null
}
},
"last_updated_at": null
},
"stats": {
"pc": {
Expand Down
45 changes: 30 additions & 15 deletions tests/fixtures/json/search_players/search_players_api_result.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/DEKK-11775",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1665953911
},
{
"player_id": "DEKK-21259",
Expand All @@ -17,7 +18,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/DEKK-21259",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1676555200
},
{
"player_id": "Dekk-11904",
Expand All @@ -26,7 +28,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-11904",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1666924000
},
{
"player_id": "Dekk-11906",
Expand All @@ -35,7 +38,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-11906",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1667959199
},
{
"player_id": "Dekk-1380",
Expand All @@ -44,7 +48,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-1380",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1666538443
},
{
"player_id": "Dekk-1637",
Expand All @@ -53,7 +58,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-1637",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1670258731
},
{
"player_id": "Dekk-1766",
Expand All @@ -62,7 +68,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-1766",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1665287787
},
{
"player_id": "Dekk-21129",
Expand All @@ -71,7 +78,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-21129",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1668372404
},
{
"player_id": "Dekk-21260",
Expand All @@ -80,7 +88,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-21260",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1668452370
},
{
"player_id": "Dekk-21386",
Expand All @@ -89,7 +98,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-21386",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1677947164
},
{
"player_id": "Dekk-2162",
Expand All @@ -98,7 +108,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-2162",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1671155709
},
{
"player_id": "Dekk-21771",
Expand All @@ -107,7 +118,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-21771",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1666737903
},
{
"player_id": "Dekk-21904",
Expand All @@ -116,7 +128,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-21904",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1676730323
},
{
"player_id": "Dekk-2677",
Expand All @@ -125,7 +138,8 @@
"namecard": "https://d15f34w2p8l1cc.cloudfront.net/overwatch/757219956129146d84617a7e713dfca1bc33ea27cf6c73df60a33d02a147edc1.png",
"title": "Bytefixer",
"career_url": "https://overfast-api.tekrop.fr/players/Dekk-2677",
"blizzard_id": "d65ba781fe23cdfabe%7C9b3063608098cdbf1b9825d8664f4d96"
"blizzard_id": "d65ba781fe23cdfabe%7C9b3063608098cdbf1b9825d8664f4d96",
"last_updated_at": 1678488893
},
{
"player_id": "dekk-2548",
Expand All @@ -134,7 +148,8 @@
"namecard": null,
"title": null,
"career_url": "https://overfast-api.tekrop.fr/players/dekk-2548",
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05"
"blizzard_id": "d67b87a1fe23caffbca9%7Ce071d6c2df64c91dc7ad8f0eb73d5d05",
"last_updated_at": 1676811985
}
]
}
4 changes: 3 additions & 1 deletion tests/parsers/test_player_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ async def test_player_page_parsing_with_filters(
player_json_data: dict,
kwargs_filter: dict,
):
# Remove "namecard" key from player_json_data, it's been added from another page
# Remove "namecard" and "last_updated_at" keys from player_json_data,
# it's been added from others parsers
player_data = player_json_data.copy()
del player_data["summary"]["namecard"]
del player_data["summary"]["last_updated_at"]

parser = PlayerParser(player_id=player_id)
update_parser_cache_last_update_mock = Mock()
Expand Down
Loading

0 comments on commit 9325b4f

Please sign in to comment.