Skip to content

Commit

Permalink
rename classes
Browse files Browse the repository at this point in the history
  • Loading branch information
tanzhijian committed Jan 18, 2024
1 parent 23c7ec3 commit 5e16dec
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 115 deletions.
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# fifacodes

FIFA country codes query and search.
FIFA member associations codes query and search.

A mapping of FIFA country codes to country names.
A mapping of FIFA member associations codes to member associations names.

The default data is sourced from [Wikipedia](https://en.wikipedia.org/wiki/List_of_FIFA_country_codes).

Expand All @@ -17,46 +17,46 @@ pip install fifacodes
You can query like using dict:

```pycon
>>> from fifacodes import Countries
>>> countries = Countries()
>>> countries.get('ENG')
Country(code='ENG', name='England')
>>> len(countries)
>>> from fifacodes import Members
>>> members = Members()
>>> members.get('ENG')
Member(code='ENG', name='England')
>>> len(members)
211
>>> list(countries.items())[0]
('AFG', Country(code='AFG', name='Afghanistan'))
>>> list(members.items())[0]
('AFG', Member(code='AFG', name='Afghanistan'))
```

Query by name:

```pycon
>>> countries['England']
Country(code='ENG', name='England')
>>> members['England']
Member(code='ENG', name='England')
```

Search for a country by name or code, the search uses fuzzy string matching to find potential results.
Search for a member by name or code, the search uses fuzzy string matching to find potential results.

```pycon
>>> countries.search('ARG')
[Country(code='ARG', name='Argentina'), Country(code='AFG', name='Afghanistan'), Country(code='ALG', name='Algeria')]
>>> members.search('argtl')
[Member(code='ARG', name='Argentina'), Member(code='ARM', name='Armenia'), Member(code='ARU', name='Aruba')]
```

Results can be adjusted using parameters:

```pycon
>>> countries.search('Fran', limit=2, score_cutoff=70)
[Country(code='FRA', name='France'), Country(code='IRN', name='Iran')]
>>> members.search('Fran', limit=2, score_cutoff=70, case_sensitive=True)
[Member(code='FRA', name='France'), Member(code='IRN', name='Iran')]
```

Search for a country by name or code and return the first result.
Search for a member by name or code and return the first result.

```pycon
>>> countries.search_one('Argent')
Country(code='ARG', name='Argentina')
>>> members.search_one('Argent')
Member(code='ARG', name='Argentina')
```

## Data Update

To update `default.csv` run `scrape.py`, If there are codes corresponding to other country names, add them to `custom.csv`.
To update `default.csv` run `scrape.py`, If there are codes corresponding to other member names, add them to `custom.csv`.

View source code for detailed usage.
50 changes: 30 additions & 20 deletions fifacodes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import csv
from collections.abc import Mapping
from pathlib import Path
from typing import Any, Generator, Iterator, NamedTuple
from typing import Any, Callable, Generator, Iterator, NamedTuple

from rapidfuzz import process

__version__ = "0.1.1"
__version__ = "0.1.2"

_DATA_PATH = Path(__file__).parent
_DEFAULT_DATA_PATH = _DATA_PATH / "default.csv"
_CUSTOM_DATA_PATH = _DATA_PATH / "custom.csv"


class Country(NamedTuple):
class Member(NamedTuple):
code: str
name: str


_DataTypes = dict[str, Country]
_DataTypes = dict[str, Member]


class Countries(Mapping[str, Country]):
class Members(Mapping[str, Member]):
"""
A mapping of FIFA country codes to country names.
A mapping of FIFA member codes to member names.
The default data is sourced from Wikipedia.
"""
Expand All @@ -41,20 +41,20 @@ def _read_data(self) -> tuple[_DataTypes, _DataTypes]:
default_data: _DataTypes = {}
data: _DataTypes = {}
for code, name in self._read_csv(_DEFAULT_DATA_PATH):
country = Country(code=code, name=name)
member = Member(code=code, name=name)

default_data[code] = country
default_data[code] = member

data[code] = country
data[name] = country
data[code] = member
data[name] = member

for code, name in self._read_csv(_CUSTOM_DATA_PATH):
country = data[code]
data[name] = country
member = data[code]
data[name] = member

return default_data, data

def __getitem__(self, key: str) -> Country:
def __getitem__(self, key: str) -> Member:
return self._data[key]

def __iter__(self) -> Iterator[str]:
Expand All @@ -69,9 +69,10 @@ def search(
*,
limit: int = 3,
score_cutoff: int | float = 60,
) -> list[Country]:
case_sensitive: bool = False,
) -> list[Member]:
"""
Search for a country by name or code.
Search for a member by name or code.
The search uses fuzzy string matching to find potential results.
Expand All @@ -80,23 +81,32 @@ def search(
limit: The maximum number of results to return. Defaults to 3.
score_cutoff: The minimum score for a result to be returned.
Defaults to 60.
case_sensitive: Whether to perform a case-sensitive search.
Defaults to False.
Returns:
A list of potential results.
"""
processor: Callable[[str], str] | None = (
None if case_sensitive else lambda x: x.lower()
)
results = process.extract(
key, self._data.keys(), limit=limit, score_cutoff=score_cutoff
key,
self._data.keys(),
limit=limit,
processor=processor,
score_cutoff=score_cutoff,
)
return [self._data[result[0]] for result in results]
return list({self._data[result[0]]: None for result in results})

def search_one(self, key: str) -> Country | None:
def search_one(self, key: str) -> Member | None:
"""
Search for a country by name or code and return the first result.
Search for a member by name or code and return the first result.
"""
try:
return self.search(key, limit=1)[0]
except IndexError:
return None


__all__ = ("Countries", "Country")
__all__ = ("Members", "Member")
3 changes: 3 additions & 0 deletions fifacodes/custom.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
code,name
CHN,China PR
CUW,Curacao
IRL,Ireland
STP,Sao Tome and Principe
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "fifacodes"
version = "0.1.1"
description = "FIFA country codes query and search."
version = "0.1.2"
description = "FIFA member associations codes query and search."
authors = ["tanzhijian <[email protected]>"]
license = "MIT"
readme = "README.md"
Expand Down
22 changes: 11 additions & 11 deletions scrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from parsel import Selector

from fifacodes import _DEFAULT_DATA_PATH as EXPORT_PATH
from fifacodes import Country
from fifacodes import Member

URL = "https://en.m.wikipedia.org/wiki/List_of_FIFA_country_codes"


CountriesTypes = list[Country]
MembersTypes = list[Member]


def fetch(client: Client) -> Response:
Expand All @@ -18,32 +18,32 @@ def fetch(client: Client) -> Response:
return response


def parse(response: Response) -> CountriesTypes:
def parse(response: Response) -> MembersTypes:
selector = Selector(response.text)
countries: CountriesTypes = []
members: MembersTypes = []
tables = selector.xpath('//*[@id="mf-section-1"]/table')
for table in tables:
trs = table.xpath(".//tr")
for tr in trs[1:]:
code = tr.xpath("./td[2]/text()").get()
name = tr.xpath("./td[1]//a/text()").get()
if code and name:
countries.append(Country(code=code.strip(), name=name.strip()))
return countries
members.append(Member(code=code.strip(), name=name.strip()))
return members


def export(countries: CountriesTypes) -> None:
def export(members: MembersTypes) -> None:
with open(EXPORT_PATH, "w") as f:
writer = csv.writer(f)
writer.writerow(Country._fields)
writer.writerows(countries)
writer.writerow(Member._fields)
writer.writerows(members)


def main() -> None:
client = Client()
response = fetch(client)
countries = parse(response)
export(countries)
members = parse(response)
export(members)


if __name__ == "__main__":
Expand Down
114 changes: 60 additions & 54 deletions tests/test_fifacodes.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,75 @@
import pytest

from fifacodes import Countries
from fifacodes import Members


class TestCountries:
class TestMembers:
@pytest.fixture(scope="class")
def countries(self) -> Countries:
return Countries()

def test_read_data(self, countries: Countries) -> None:
assert len(countries._default_data) == 211
assert len(countries._data) == 423

def test_init(self, countries: Countries) -> None:
assert len(countries) == 211

def test_get_key(self, countries: Countries) -> None:
country = countries.get("ENG")
if country is not None:
assert country.code == "ENG"
assert country.name == "England"

def test_get_value(self, countries: Countries) -> None:
country = countries.get("England")
if country is not None:
assert country.code == "ENG"
assert country.name == "England"

def test_get_custom_value(self, countries: Countries) -> None:
country = countries["China PR"]
assert country.code == "CHN"
assert country.name == "China"

def test_get_none(self, countries: Countries) -> None:
country = countries.get("foo")
assert country is None

def test_raise_keyerror(self, countries: Countries) -> None:
def members(self) -> Members:
return Members()

def test_read_data(self, members: Members) -> None:
assert len(members._default_data) == 211
assert len(members._data) == 426

def test_init(self, members: Members) -> None:
assert len(members) == 211

def test_get_key(self, members: Members) -> None:
member = members.get("ENG")
if member is not None:
assert member.code == "ENG"
assert member.name == "England"

def test_get_value(self, members: Members) -> None:
member = members.get("England")
if member is not None:
assert member.code == "ENG"
assert member.name == "England"

def test_get_custom_value(self, members: Members) -> None:
member = members["China PR"]
assert member.code == "CHN"
assert member.name == "China"

def test_get_none(self, members: Members) -> None:
member = members.get("foo")
assert member is None

def test_raise_keyerror(self, members: Members) -> None:
with pytest.raises(KeyError):
countries["foo"]
members["foo"]

def test_search(self, countries: Countries) -> None:
results = countries.search("ENG")
assert len(results) == 3
country = results[0]
assert country.name == "England"
def test_search(self, members: Members) -> None:
results = members.search("eng")
assert len(results) == 2
member = results[0]
assert member.name == "England"

def test_search_limit_one(self, countries: Countries) -> None:
results = countries.search("ENG", limit=1)
def test_search_limit_one(self, members: Members) -> None:
results = members.search("eng", limit=1)
assert len(results) == 1

def test_search_score_cutoff(self, countries: Countries) -> None:
results = countries.search("ENG", score_cutoff=90.0)
def test_search_score_cutoff(self, members: Members) -> None:
results = members.search("eng", score_cutoff=90.0)
assert len(results) == 1

def test_search_none(self, countries: Countries) -> None:
results = countries.search("foobar")
def test_search_case_sensitive_true(self, members: Members) -> None:
results = members.search("ENG", case_sensitive=True)
assert len(results) == 3
member = results[0]
assert member.name == "England"

def test_search_none(self, members: Members) -> None:
results = members.search("12345")
assert len(results) == 0

def test_search_one(self, countries: Countries) -> None:
country = countries.search_one("ENG")
assert country is not None
assert country.code == "ENG"
assert country.name == "England"
def test_search_one(self, members: Members) -> None:
member = members.search_one("eng")
assert member is not None
assert member.code == "ENG"
assert member.name == "England"

def test_search_one_none(self, countries: Countries) -> None:
country = countries.search_one("foobar")
assert country is None
def test_search_one_none(self, members: Members) -> None:
member = members.search_one("12345")
assert member is None
Loading

0 comments on commit 5e16dec

Please sign in to comment.