Skip to content

Instantly share code, notes, and snippets.

@Jaakkonen
Created June 27, 2024 21:15
Show Gist options
  • Select an option

  • Save Jaakkonen/6dfdf86679d4baf82cdc69c99413cda1 to your computer and use it in GitHub Desktop.

Select an option

Save Jaakkonen/6dfdf86679d4baf82cdc69c99413cda1 to your computer and use it in GitHub Desktop.
Clash of clans clan search
#!/bin/python
import re
import statistics
from typing import Any, Callable
import requests
import os
from urllib.parse import quote
from cachecontrol import CacheControl
from cachecontrol.caches.file_cache import FileCache
# Get API key from https://developer.clashofclans.com/#/account
# and specify it here. Note that this uses IP whitelisting so for
# dynamic IP addresses keys need to be created each time the IP changes.
os.environ["COC_APIKEY"]
s = CacheControl(requests.Session(), cache=FileCache(".clashofclans_request_cache"))
s.headers.update(
{
"Authorization": "Bearer " + COCAPIKEY,
}
)
locations = s.get("https://api.clashofclans.com/v1/locations").json()
if locations.get("reason") == "accessDenied.invalidIp":
raise ValueError("API key IP not whitelisted")
assert len(locations["paging"]["cursors"]) == 0
clanlabels_req = s.get("https://api.clashofclans.com/v1/labels/clans").json()
assert len(clanlabels_req["paging"]["cursors"]) == 0
clanlabels = {label["name"]: label["id"] for label in clanlabels_req["items"]}
assert set(clanlabels.keys()) == {
"Trophy Pushing",
"Clan War League",
"Friendly",
"Farming",
"Underdog",
"Base Designing",
"Clan Wars",
"Donations",
"International",
"Builder Base",
"Competitive",
"Friendly Wars",
"Clan Games",
"Talkative",
"Newbie Friendly",
"Clan Capital",
"Relaxed",
}
finland_code = next(
location["id"] for location in locations["items"] if location["name"] == "Finland"
)
# Search clans
finclans = s.get(
"https://api.clashofclans.com/v1/clans",
params={
"locationId": finland_code,
},
).json()
"""
Example clan:
{'tag': '#28RVGJJPU',
'name': 'Tule klaaniin',
'type': 'open',
'location': {'id': 32000086,
'name': 'Finland',
'isCountry': True,
'countryCode': 'FI'},
'isFamilyFriendly': False,
'badgeUrls': {'small': 'https://api-assets.clashofclans.com/badges/70/R5mkb7JcbtE2IwUVrgpQlbpDaN3PYsHKRrx7VVmzm68.png',
'large': 'https://api-assets.clashofclans.com/badges/512/R5mkb7JcbtE2IwUVrgpQlbpDaN3PYsHKRrx7VVmzm68.png',
'medium': 'https://api-assets.clashofclans.com/badges/200/R5mkb7JcbtE2IwUVrgpQlbpDaN3PYsHKRrx7VVmzm68.png'},
'clanLevel': 1,
'clanPoints': 389,
'clanBuilderBasePoints': 69,
'clanCapitalPoints': 0,
'capitalLeague': {'id': 85000000, 'name': 'Unranked'},
'requiredTrophies': 0,
'warFrequency': 'always',
'warWinStreak': 0,
'warWins': 0,
'warTies': 0,
'warLosses': 0,
'isWarLogPublic': True,
'warLeague': {'id': 48000000, 'name': 'Unranked'},
'members': 3,
'labels': [],
'requiredBuilderBaseTrophies': 0,
'requiredTownhallLevel': 1},
"""
SearchFilter = tuple[int, int] | str | None | set | Callable[[Any], bool]
def do_filter(filter: SearchFilter, value: Any) -> bool:
if filter is None:
return True
if isinstance(filter, int):
return value == filter
if isinstance(filter, str):
# Regex match
return re.match(filter, value) is not None
if isinstance(filter, tuple):
return filter[0] <= value <= filter[1]
if isinstance(filter, set):
return value in filter
if callable(filter):
return filter(value)
raise ValueError(f"Invalid filter: {filter}")
# inclusive filter ranges
search_ranges: dict["str", SearchFilter] = {
"members": (lambda x: 35 <= x <= 49),
"requiredTownhallLevel": (0, 10),
# "requiredTrophies": (1000, 1800),
"type": "open",
"chatLanguage": (lambda x: x is None or x["languageCode"] == 'FI'),
# "isFamilyFriendly": (lambda x: x is False)
}
filtered_clans = [
clan
for clan in finclans["items"]
if all(
do_filter(filt, clan.get(key))
for key, filt in search_ranges.items()
)
]
# Request for descriptions
full_clans = [
s.get(f"https://api.clashofclans.com/v1/clans/{quote(clan['tag'])}").json()
for clan in filtered_clans
]
def reformat_clan(clan: dict[str, Any]) -> dict[str, Any]:
# shallow copy clan
clan = dict(clan)
clan['labels'] = [label['name'] for label in clan['labels']]
del clan['badgeUrls']
clan['location'] = clan['location']['name']
clan['warLeague'] = clan['warLeague']['name']
clan['capitalLeague'] = clan['capitalLeague']['name']
clan['chatLanguage'] = clan.get('chatLanguage', {}).get('name')
if 'clanCapital' in clan:
clan['clanCapitalLevels'] = {
district['name']: district['districtHallLevel']
for district in clan['clanCapital']['districts']
}
del clan['clanCapital']
clan['trophies'] = {
'quantiles': statistics.quantiles([member['trophies'] for member in clan['memberList']], n=4),
'quantilesBuilderBase': statistics.quantiles([member['builderBaseTrophies'] for member in clan['memberList']], n=4),
}
# hide list of members
del clan['memberList']
return clan
from devtools import debug
format_clans = [reformat_clan(clan) for clan in full_clans]
# Sort clans by lowest quantile
format_clans.sort(key=lambda x: -x['trophies']['quantiles'][0])
# Take only clans where 50% quantile is between 1700 and 2100
format_clans = [
clan
for clan in format_clans
if 1700 <= clan['trophies']['quantiles'][1] <= 2100
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment