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
16 changes: 15 additions & 1 deletion app/api/models/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pydantic import BaseModel, Field, HttpUrl

from app.domain.enums import MapGamemode, MapKey
from app.domain.enums import MapGamemode, MapKey, MapUse


class Map(BaseModel):
Expand All @@ -21,6 +21,20 @@ class Map(BaseModel):
...,
description="Main gamemodes on which the map is playable",
)
uses: list[MapUse] = Field(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Cached maps lack classifications

When a pre-deployment /maps proxy-cache entry is still valid, fastAPI never runs for a cache hit, so the cached JSON omits uses and clients receive map entries in the old shape without the new classifications.

Purge or invalidate the /maps proxy cache on deployment.

Prompt for AI agents
In `app/api/models/maps.py` at line 24:

**Cached maps lack classifications**

When a pre-deployment `/maps` proxy-cache entry is still valid, fastAPI never runs for a cache hit, so the cached JSON omits `uses` and clients receive map entries in the old shape without the new classifications.

Purge or invalidate the `/maps` proxy cache on deployment.

...,
min_length=1,
max_length=5,
description="Uses of the map: standard play, Stadium, Workshop, Arcade, or training",
examples=[
["standard"],
["stadium"],
["standard", "stadium"],
["workshop"],
["arcade"],
["training"],
],
)
location: str = Field(
...,
description="Location of the map",
Expand Down
10 changes: 10 additions & 0 deletions app/domain/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ class HeroGamemode(StrEnum):
STADIUM = "stadium"


class MapUse(StrEnum):
"""Categories of play supported by Overwatch maps."""

ARCADE = "arcade"
STANDARD = "standard"
STADIUM = "stadium"
TRAINING = "training"
WORKSHOP = "workshop"


# Dynamically create the MapKey enum by using the CSV File
maps_data = read_csv_file("maps")
MapKey = StrEnum(
Expand Down
4 changes: 3 additions & 1 deletion app/domain/parsers/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ def parse_maps_csv() -> list[dict]:
Parse maps list from CSV file

Returns:
List of map dicts with keys: key, name, screenshot, gamemodes, location, country_code
List of map dicts with keys: key, name, screenshot, gamemodes, uses,
location, country_code
"""
csv_data = read_csv_file("maps")

Expand All @@ -24,6 +25,7 @@ def parse_maps_csv() -> list[dict]:
"name": map_dict["name"],
"screenshot": get_static_url_maps(map_dict["key"]),
"gamemodes": map_dict["gamemodes"].split(","),
"uses": map_dict["uses"].split(","),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Old instances overwrite the new map shape

When old- and new-version API instances overlap during deployment, and an old instance writes to the shared maps cache after a new instance has written its response, an old-version API instance can write its map list to the shared /maps cache key after a new instance has populated it. Its parser omits uses, so clients then receive responses without the new field until a new-version write or cache expiry.

Prevent old-version instances from writing the new-version cache entry, for example by versioning the cache key or ensuring old instances are drained before refreshing it.

Prompt for AI agents
In `app/domain/parsers/maps.py` at line 28:

**Old instances overwrite the new map shape**

When old- and new-version API instances overlap during deployment, and an old instance writes to the shared maps cache after a new instance has written its response, an old-version API instance can write its map list to the shared `/maps` cache key after a new instance has populated it. Its parser omits `uses`, so clients then receive responses without the new field until a new-version write or cache expiry.

Prevent old-version instances from writing the new-version cache entry, for example by versioning the cache key or ensuring old instances are drained before refreshing it.

"location": map_dict["location"],
"country_code": map_dict.get("country_code") or None,
}
Expand Down
120 changes: 60 additions & 60 deletions app/domain/utils/data/maps.csv
Original file line number Diff line number Diff line change
@@ -1,60 +1,60 @@
key,name,gamemodes,location,country_code
aatlis,Aatlis,flashpoint,Morocco,MA
antarctic-peninsula,Antarctic Peninsula,control,Antarctica,AQ
anubis,Temple of Anubis,assault,"Giza Plateau, Egypt",EG
arena-victoriae,Arena Victoriae,control,"Colosseo, Rome, Italy",IT
ayutthaya,Ayutthaya,capture-the-flag,Thailand,TH
black-forest,Black Forest,elimination,Germany,DE
blizzard-world,Blizzard World,hybrid,"Irvine, California, United States",US
busan,Busan,control,South Korea,KR
castillo,Castillo,elimination,Mexico,MX
chateau-guillard,Château Guillard,"deathmatch,team-deathmatch","Annecy, France",FR
circuit-royal,Circuit Royal,escort,"Monte Carlo, Monaco",MC
colosseo,Colosseo,push,"Rome, Italy",IT
dorado,Dorado,escort,Mexico,MX
ecopoint-antarctica,Ecopoint: Antarctica,elimination,Antarctica,AQ
eichenwalde,Eichenwalde,hybrid,"Stuttgart, Germany",DE
esperanca,Esperança,push,Portugal,PT
gogadoro,Gogadoro,control,"Busan, South Korea",KR
hanamura,Hanamura,assault,"Tokyo, Japan",JP
hanaoka,Hanaoka,clash,"Tokyo, Japan",JP
havana,Havana,escort,"Havana, Cuba",CU
hollywood,Hollywood,hybrid,"Los Angeles, United States",US
horizon,Horizon Lunar Colony,assault,Earth's moon,
ilios,Ilios,control,Greece,GR
junkertown,Junkertown,escort,Central Australia,AU
lijiang-tower,Lijiang Tower,control,China,CN
kanezaka,Kanezaka,"deathmatch,team-deathmatch","Tokyo, Japan",JP
kings-row,King’s Row,hybrid,"London, United Kingdom",UK
malevento,Malevento,"deathmatch,team-deathmatch",Italy,IT
midtown,Midtown,hybrid,"New York, United States",US
necropolis,Necropolis,elimination,Egypt,EG
nepal,Nepal,control,Nepal,NP
neon-junction,Neon Junction,hybrid,Japan,JP
new-junk-city,New Junk City,flashpoint,Central Australia,AU
new-queen-street,New Queen Street,push,"Toronto, Canada",CA
numbani,Numbani,hybrid,Numbani (near Nigeria),
oasis,Oasis,control,Iraq,IQ
paraiso,Paraíso,hybrid,"Rio de Janeiro, Brazil",BR
paris,Paris,assault,"Paris, France",FR
petra,Petra,"deathmatch,team-deathmatch",Southern Jordan,JO
place-lacroix,Place Lacroix,push,"Paris, France",FR
powder-keg-mine,Powder Keg Mine,payload-race,"Deadlock Gorge, Arizona, United States",US
practice-range,Practice Range,practice-range,Swiss HQ,CH
redwood-dam,Redwood Dam,push,Gibraltar,GI
rialto,Rialto,escort,"Venice, Italy",IT
route-66,Route 66,escort,"Albuquerque, New Mexico, United States",US
runasapi,Runasapi,push,Peru,PE
samoa,Samoa,control,Samoa,WS
shambali-monastery,Shambali Monastery,escort,Nepal,NP
suravasa,Suravasa,flashpoint,India,IN
thames-district,Thames District,payload-race,"London, United Kingdom",UK
throne-of-anubis,Throne of Anubis,clash,"Giza Plateau, Egypt",EG
volskaya,Volskaya Industries,assault,"St. Petersburg, Russia",RU
watchpoint-gibraltar,Watchpoint: Gibraltar,escort,Gibraltar,GI
watchpoint-grimsvotn,Watchpoint: Grimsvotn,escort,Iceland,IS
workshop-chamber,Workshop Chamber,workshop,Earth,
workshop-expanse,Workshop Expanse,workshop,Earth,
workshop-green-screen,Workshop Green Screen,workshop,Earth,
workshop-island,Workshop Island,workshop,Earth,
wuxing-university,Wuxing University,control,"Chengdu, Sichuan, China",CN
key,name,gamemodes,location,country_code,uses
aatlis,Aatlis,flashpoint,Morocco,MA,standard
antarctic-peninsula,Antarctic Peninsula,control,Antarctica,AQ,standard
anubis,Temple of Anubis,assault,"Giza Plateau, Egypt",EG,arcade
arena-victoriae,Arena Victoriae,control,"Colosseo, Rome, Italy",IT,stadium
ayutthaya,Ayutthaya,capture-the-flag,Thailand,TH,arcade
black-forest,Black Forest,elimination,Germany,DE,arcade
blizzard-world,Blizzard World,hybrid,"Irvine, California, United States",US,standard
busan,Busan,control,South Korea,KR,"standard,stadium"
castillo,Castillo,elimination,Mexico,MX,arcade
chateau-guillard,Château Guillard,"deathmatch,team-deathmatch","Annecy, France",FR,arcade
circuit-royal,Circuit Royal,escort,"Monte Carlo, Monaco",MC,standard
colosseo,Colosseo,push,"Rome, Italy",IT,"standard,stadium"
dorado,Dorado,escort,Mexico,MX,standard
ecopoint-antarctica,Ecopoint: Antarctica,elimination,Antarctica,AQ,arcade
eichenwalde,Eichenwalde,hybrid,"Stuttgart, Germany",DE,standard
esperanca,Esperança,push,Portugal,PT,"standard,stadium"
gogadoro,Gogadoro,control,"Busan, South Korea",KR,stadium
grimsvotn,Grímsvötn,escort,Iceland,IS,standard
hanamura,Hanamura,assault,"Tokyo, Japan",JP,arcade
hanaoka,Hanaoka,clash,"Tokyo, Japan",JP,arcade
havana,Havana,escort,"Havana, Cuba",CU,standard
hollywood,Hollywood,hybrid,"Los Angeles, United States",US,standard
horizon,Horizon Lunar Colony,assault,Earth's moon,,arcade
ilios,Ilios,control,Greece,GR,"standard,stadium"
junkertown,Junkertown,escort,Central Australia,AU,standard
lijiang-tower,Lijiang Tower,control,China,CN,"standard,stadium"
kanezaka,Kanezaka,"deathmatch,team-deathmatch","Tokyo, Japan",JP,arcade
kings-row,King’s Row,hybrid,"London, United Kingdom",UK,standard
malevento,Malevento,"deathmatch,team-deathmatch",Italy,IT,arcade
midtown,Midtown,hybrid,"New York, United States",US,standard
necropolis,Necropolis,elimination,Egypt,EG,arcade
nepal,Nepal,control,Nepal,NP,"standard,stadium"
neon-junction,Neon Junction,hybrid,Japan,JP,standard
new-junk-city,New Junk City,flashpoint,Central Australia,AU,standard
new-queen-street,New Queen Street,push,"Toronto, Canada",CA,standard
numbani,Numbani,hybrid,Numbani (near Nigeria),,standard
oasis,Oasis,control,Iraq,IQ,"standard,stadium"
paraiso,Paraíso,hybrid,"Rio de Janeiro, Brazil",BR,standard
paris,Paris,assault,"Paris, France",FR,arcade
petra,Petra,"deathmatch,team-deathmatch",Southern Jordan,JO,arcade
place-lacroix,Place Lacroix,push,"Paris, France",FR,stadium
powder-keg-mine,Powder Keg Mine,payload-race,"Deadlock Gorge, Arizona, United States",US,stadium
practice-range,Practice Range,practice-range,Swiss HQ,CH,training
redwood-dam,Redwood Dam,push,Gibraltar,GI,stadium
rialto,Rialto,escort,"Venice, Italy",IT,standard
route-66,Route 66,escort,"Albuquerque, New Mexico, United States",US,standard
runasapi,Runasapi,push,Peru,PE,standard
samoa,Samoa,control,Samoa,WS,"standard,stadium"
shambali-monastery,Shambali Monastery,escort,Nepal,NP,standard
suravasa,Suravasa,flashpoint,India,IN,standard
thames-district,Thames District,payload-race,"London, United Kingdom",UK,stadium
throne-of-anubis,Throne of Anubis,clash,"Giza Plateau, Egypt",EG,arcade
volskaya,Volskaya Industries,assault,"St. Petersburg, Russia",RU,arcade
watchpoint-gibraltar,Watchpoint: Gibraltar,escort,Gibraltar,GI,standard
workshop-chamber,Workshop Chamber,workshop,Earth,,workshop
workshop-expanse,Workshop Expanse,workshop,Earth,,workshop
workshop-green-screen,Workshop Green Screen,workshop,Earth,,workshop
workshop-island,Workshop Island,workshop,Earth,,workshop
wuxing-university,Wuxing University,control,"Chengdu, Sichuan, China",CN,stadium
File renamed without changes
1 change: 1 addition & 0 deletions tests/maps/parsers/test_maps_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def test_parse_maps_csv_entry_format():
"name",
"screenshot",
"gamemodes",
"uses",
"location",
"country_code",
}
Expand Down
85 changes: 85 additions & 0 deletions tests/maps/test_maps_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,88 @@ def test_get_maps_filter_by_gamemode(client: TestClient, gamemode: MapGamemode):
def test_get_maps_invalid_gamemode(client: TestClient):
response = client.get("/maps", params={"gamemode": "invalid"})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT


@pytest.mark.parametrize(
("key", "expected_uses"),
[
("aatlis", ["standard"]),
("antarctic-peninsula", ["standard"]),
("blizzard-world", ["standard"]),
("circuit-royal", ["standard"]),
("dorado", ["standard"]),
("eichenwalde", ["standard"]),
("grimsvotn", ["standard"]),
("havana", ["standard"]),
("hollywood", ["standard"]),
("junkertown", ["standard"]),
("kings-row", ["standard"]),
("midtown", ["standard"]),
("neon-junction", ["standard"]),
("new-junk-city", ["standard"]),
("new-queen-street", ["standard"]),
("numbani", ["standard"]),
("paraiso", ["standard"]),
("rialto", ["standard"]),
("route-66", ["standard"]),
("runasapi", ["standard"]),
("shambali-monastery", ["standard"]),
("suravasa", ["standard"]),
("watchpoint-gibraltar", ["standard"]),
("anubis", ["arcade"]),
("hanamura", ["arcade"]),
("horizon", ["arcade"]),
("paris", ["arcade"]),
("volskaya", ["arcade"]),
("practice-range", ["training"]),
("ayutthaya", ["arcade"]),
("black-forest", ["arcade"]),
("castillo", ["arcade"]),
("chateau-guillard", ["arcade"]),
("ecopoint-antarctica", ["arcade"]),
("kanezaka", ["arcade"]),
("malevento", ["arcade"]),
("necropolis", ["arcade"]),
("petra", ["arcade"]),
("workshop-chamber", ["workshop"]),
("workshop-expanse", ["workshop"]),
("workshop-green-screen", ["workshop"]),
("workshop-island", ["workshop"]),
("arena-victoriae", ["stadium"]),
("gogadoro", ["stadium"]),
("place-lacroix", ["stadium"]),
("redwood-dam", ["stadium"]),
("powder-keg-mine", ["stadium"]),
("thames-district", ["stadium"]),
("busan", ["standard", "stadium"]),
("ilios", ["standard", "stadium"]),
("lijiang-tower", ["standard", "stadium"]),
("nepal", ["standard", "stadium"]),
("oasis", ["standard", "stadium"]),
("samoa", ["standard", "stadium"]),
("wuxing-university", ["stadium"]),
("colosseo", ["standard", "stadium"]),
("esperanca", ["standard", "stadium"]),
("hanaoka", ["arcade"]),
("throne-of-anubis", ["arcade"]),
],
)
def test_get_maps_uses(client: TestClient, key: str, expected_uses: list[str]) -> None:
url = "/maps"

response = client.get(url)

assert response.status_code == status.HTTP_200_OK
assert next(m for m in response.json() if m["key"] == key)["uses"] == expected_uses
assert all(
m["uses"]
in (
["standard"],
["stadium"],
["standard", "stadium"],
["workshop"],
["arcade"],
["training"],
)
for m in response.json()
)
Loading