Skip to content

Commit e77a60c

Browse files
authored
Version 2.0.0 (#40)
* Refactor base utilities * TransfermarktClubPlayers * TransfermarktClubProfile * TransfermarktClubSearch * TransfermarktCompetitionClubs * TransfermarktCompetitionSearch * TransfermarktPlayerProfile * TransfermarktPlayerTransfers * TransfermarktPlayerMarketValue * TransfermarktPlayerStats * TransfermarktPlayerSearch * Refactor utils * Add updatedAt datetime * Refactor unit tests * Update version and dependencies
1 parent 66ecece commit e77a60c

28 files changed

Lines changed: 2782 additions & 5634 deletions

app/services/base.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from dataclasses import dataclass, field
2+
from typing import Optional
3+
from xml.etree import ElementTree
4+
5+
import requests
6+
from bs4 import BeautifulSoup
7+
from fastapi import HTTPException
8+
from lxml import etree
9+
from requests import Response, TooManyRedirects
10+
11+
from app.utils.utils import trim
12+
from app.utils.xpath import Commons
13+
14+
15+
@dataclass
16+
class TransfermarktBase:
17+
URL: str
18+
page: ElementTree = field(default_factory=lambda: None, init=False)
19+
response: dict = field(default_factory=lambda: {}, init=False)
20+
21+
def make_request(self, url: Optional[str] = None) -> Response:
22+
url = self.URL if not url else url
23+
try:
24+
response: Response = requests.get(
25+
url=url,
26+
headers={
27+
"User-Agent": (
28+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
29+
"AppleWebKit/537.36 (KHTML, like Gecko) "
30+
"Chrome/113.0.0.0 "
31+
"Safari/537.36"
32+
),
33+
},
34+
)
35+
except TooManyRedirects:
36+
raise HTTPException(status_code=404, detail=f"Not found for url: {url}")
37+
if 400 <= response.status_code < 500:
38+
raise HTTPException(
39+
status_code=response.status_code,
40+
detail=f"Client Error. {response.reason} for url: {url}",
41+
)
42+
elif 500 <= response.status_code < 600:
43+
raise HTTPException(
44+
status_code=response.status_code,
45+
detail=f"Server Error. {response.reason} for url: {url}",
46+
)
47+
return response
48+
49+
def request_url_bsoup(self) -> BeautifulSoup:
50+
response: Response = self.make_request()
51+
return BeautifulSoup(markup=response.content, features="html.parser")
52+
53+
@staticmethod
54+
def convert_bsoup_to_page(bsoup: BeautifulSoup) -> ElementTree:
55+
return etree.HTML(str(bsoup))
56+
57+
def request_url_page(self) -> ElementTree:
58+
bsoup: BeautifulSoup = self.request_url_bsoup()
59+
return self.convert_bsoup_to_page(bsoup=bsoup)
60+
61+
def raise_exception_if_not_found(self, xpath: str):
62+
if not self.get_text_by_xpath(xpath):
63+
raise HTTPException(status_code=404, detail=f"Invalid request (url: {self.URL})")
64+
65+
def get_list_by_xpath(self, xpath: str, remove_empty: Optional[bool] = True) -> Optional[list]:
66+
elements: list = self.page.xpath(xpath)
67+
if remove_empty:
68+
elements_valid: list = [trim(e) for e in elements if trim(e)]
69+
else:
70+
elements_valid: list = [trim(e) for e in elements]
71+
return elements_valid or []
72+
73+
def get_text_by_xpath(
74+
self,
75+
xpath: str,
76+
pos: int = 0,
77+
iloc: Optional[int] = None,
78+
iloc_from: Optional[int] = None,
79+
iloc_to: Optional[int] = None,
80+
join_str: Optional[str] = None,
81+
) -> Optional[str]:
82+
element = self.page.xpath(xpath)
83+
84+
if not element:
85+
return None
86+
87+
if isinstance(element, list):
88+
element = [trim(e) for e in element if trim(e)]
89+
90+
if isinstance(iloc, int):
91+
element = element[iloc]
92+
93+
if isinstance(iloc_from, int) and isinstance(iloc_to, int):
94+
element = element[iloc_from:iloc_to]
95+
96+
if isinstance(iloc_to, int):
97+
element = element[:iloc_to]
98+
99+
if isinstance(iloc_from, int):
100+
element = element[iloc_from:]
101+
102+
if isinstance(join_str, str):
103+
return join_str.join([trim(e) for e in element])
104+
105+
try:
106+
return trim(element[pos])
107+
except IndexError:
108+
return None
109+
110+
def get_search_last_page_number(self, xpath_base: str) -> int:
111+
url_page_number_last = self.get_text_by_xpath(xpath_base + Commons.Search.PAGE_NUMBER_LAST)
112+
url_page_number_active = self.get_text_by_xpath(xpath_base + Commons.Search.PAGE_NUMBER_ACTIVE)
113+
114+
if url_page_number_last:
115+
return int(url_page_number_last.split("=")[-1])
116+
117+
if url_page_number_active:
118+
return int(url_page_number_active.split("=")[-1])
119+
120+
return 1

app/services/clubs/players.py

Lines changed: 43 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,68 @@
1-
from dataclasses import dataclass, field
1+
from dataclasses import dataclass
2+
from datetime import datetime
23

3-
from fastapi import HTTPException
4-
5-
from app.utils.utils import (
6-
clean_response,
7-
extract_from_url,
8-
get_list_by_xpath,
9-
get_text_by_xpath,
10-
request_url_page,
11-
safe_regex,
12-
)
4+
from app.services.base import TransfermarktBase
5+
from app.utils.regex import REGEX_DOB
6+
from app.utils.utils import clean_response, extract_from_url, safe_regex
137
from app.utils.xpath import Clubs
148

159

1610
@dataclass
17-
class TransfermarktClubPlayers:
18-
club_id: str
11+
class TransfermarktClubPlayers(TransfermarktBase):
12+
club_id: str = None
1913
season_id: str = None
20-
club_players: dict = field(default_factory=lambda: {})
21-
22-
def get_club_players(self) -> dict:
23-
self._request_page()
24-
self._update_season_id()
25-
self._update_club_in_past()
26-
27-
self.club_players["id"] = self.club_id
28-
self.club_players["clubName"] = get_text_by_xpath(self, Clubs.Players.CLUB_NAME)
29-
30-
self._check_club_found()
31-
32-
self.club_players["seasonYear"] = self.season_id
33-
self.club_players["players"] = self._parse_club_players()
34-
35-
return clean_response(self.club_players)
14+
URL: str = "https://www.transfermarkt.com/-/kader/verein/{club_id}/saison_id/{season_id}/plus/1"
3615

37-
def _request_page(self) -> None:
38-
club_players_url = (
39-
f"https://www.transfermarkt.com/-/kader/verein/{self.club_id}/saison_id/{self.season_id}/plus/1"
40-
)
41-
self.page = request_url_page(url=club_players_url)
16+
def __post_init__(self):
17+
self.URL = self.URL.format(club_id=self.club_id, season_id=self.season_id)
18+
self.page = self.request_url_page()
19+
self.raise_exception_if_not_found(xpath=Clubs.Players.CLUB_NAME)
20+
self.__update_season_id()
21+
self.__update_past_flag()
4222

43-
def _update_season_id(self):
23+
def __update_season_id(self):
4424
if self.season_id is None:
45-
self.season_id = extract_from_url(get_text_by_xpath(self, Clubs.Players.CLUB_URL), "season_id")
25+
self.season_id = extract_from_url(self.get_text_by_xpath(Clubs.Players.CLUB_URL), "season_id")
4626

47-
def _update_club_in_past(self):
48-
self.past = "Current club" in get_list_by_xpath(self, Clubs.Players.PAST_FLAG)
27+
def __update_past_flag(self):
28+
self.past = "Current club" in self.get_list_by_xpath(Clubs.Players.PAST_FLAG)
4929

50-
def _parse_club_players(self) -> list:
30+
def __parse_club_players(self) -> list:
5131
page_nationalities = self.page.xpath(Clubs.Players.PAGE_NATIONALITIES)
5232
page_players_infos = self.page.xpath(Clubs.Players.PAGE_INFOS)
5333
page_players_signed_from = self.page.xpath(
5434
Clubs.Players.Past.PAGE_SIGNED_FROM if self.past else Clubs.Players.Present.PAGE_SIGNED_FROM,
5535
)
56-
57-
players_urls: list = get_list_by_xpath(self, Clubs.Players.URLS)
58-
players_ids: list = [extract_from_url(url) for url in players_urls]
59-
60-
players_names: list = get_list_by_xpath(self, Clubs.Players.NAMES)
61-
62-
players_positions: list = get_list_by_xpath(self, Clubs.Players.POSITIONS)
63-
players_dobs: list = [
64-
safe_regex(dob_age, r"^(?P<dob>.*)\s\((?P<age>\d*)\)", "dob")
65-
for dob_age in get_list_by_xpath(self, Clubs.Players.DOB_AGE)
36+
players_ids = [extract_from_url(url) for url in self.get_list_by_xpath(Clubs.Players.URLS)]
37+
players_names = self.get_list_by_xpath(Clubs.Players.NAMES)
38+
players_positions = self.get_list_by_xpath(Clubs.Players.POSITIONS)
39+
players_dobs = [
40+
safe_regex(dob_age, REGEX_DOB, "dob") for dob_age in self.get_list_by_xpath(Clubs.Players.DOB_AGE)
6641
]
67-
players_ages: list = [
68-
safe_regex(dob_age, r"^(?P<dob>.*)\s\((?P<age>\d*)\)", "age")
69-
for dob_age in get_list_by_xpath(self, Clubs.Players.DOB_AGE)
70-
]
71-
players_nationalities: list = [
72-
nationality.xpath(Clubs.Players.NATIONALITIES) for nationality in page_nationalities
42+
players_ages = [
43+
safe_regex(dob_age, REGEX_DOB, "age") for dob_age in self.get_list_by_xpath(Clubs.Players.DOB_AGE)
7344
]
45+
players_nationalities = [nationality.xpath(Clubs.Players.NATIONALITIES) for nationality in page_nationalities]
7446
players_current_club = (
75-
get_list_by_xpath(self, Clubs.Players.Past.CURRENT_CLUB) if self.past else [None] * len(players_ids)
47+
self.get_list_by_xpath(Clubs.Players.Past.CURRENT_CLUB) if self.past else [None] * len(players_ids)
7648
)
77-
players_heights: list = get_list_by_xpath(
78-
self,
49+
players_heights = self.get_list_by_xpath(
7950
Clubs.Players.Past.HEIGHTS if self.past else Clubs.Players.Present.HEIGHTS,
8051
)
81-
players_foots: list = get_list_by_xpath(
82-
self,
52+
players_foots = self.get_list_by_xpath(
8353
Clubs.Players.Past.FOOTS if self.past else Clubs.Players.Present.FOOTS,
8454
remove_empty=False,
8555
)
86-
players_joined_on: list = get_list_by_xpath(
87-
self,
56+
players_joined_on = self.get_list_by_xpath(
8857
Clubs.Players.Past.JOINED_ON if self.past else Clubs.Players.Present.JOINED_ON,
8958
)
90-
players_joined: list = ["; ".join(e.xpath(Clubs.Players.JOINED)) for e in page_players_infos]
91-
players_signed_from: list = ["; ".join(e.xpath(Clubs.Players.SIGNED_FROM)) for e in page_players_signed_from]
92-
players_contracts: list = get_list_by_xpath(
93-
self,
94-
Clubs.Players.Past.CONTRACTS if self.past else Clubs.Players.Present.CONTRACTS,
59+
players_joined = ["; ".join(e.xpath(Clubs.Players.JOINED)) for e in page_players_infos]
60+
players_signed_from = ["; ".join(e.xpath(Clubs.Players.SIGNED_FROM)) for e in page_players_signed_from]
61+
players_contracts = (
62+
[None] * len(players_ids) if self.past else self.get_list_by_xpath(Clubs.Players.Past.CONTRACTS)
9563
)
96-
players_marketvalues: list = get_list_by_xpath(self, Clubs.Players.MARKET_VALUES)
97-
players_statuses: list = ["; ".join(e.xpath(Clubs.Players.STATUSES)) for e in page_players_infos]
64+
players_marketvalues = self.get_list_by_xpath(Clubs.Players.MARKET_VALUES)
65+
players_statuses = ["; ".join(e.xpath(Clubs.Players.STATUSES)) for e in page_players_infos]
9866

9967
return [
10068
{
@@ -133,6 +101,9 @@ def _parse_club_players(self) -> list:
133101
)
134102
]
135103

136-
def _check_club_found(self) -> None:
137-
if not self.club_players["clubName"]:
138-
raise HTTPException(status_code=404, detail=f"Club Players not found for id: {self.club_id}")
104+
def get_club_players(self) -> dict:
105+
self.response["id"] = self.club_id
106+
self.response["players"] = self.__parse_club_players()
107+
self.response["updatedAt"] = datetime.now()
108+
109+
return clean_response(self.response)

0 commit comments

Comments
 (0)