From 5cc2f90f1507d7b0a5d0a2285818f5e74bf97aac Mon Sep 17 00:00:00 2001 From: fcoagz Date: Mon, 18 Sep 2023 14:44:24 -0400 Subject: [PATCH] Initial commit --- README.md | 26 ++- app.py | 37 +++++ conmebol/__init__.py | 3 + conmebol/classification.py | 60 +++++++ conmebol/flags.py | 12 ++ conmebol/matches.py | 88 ++++++++++ conmebol/models/__init__.py | 34 ++++ conmebol/results.py | 69 ++++++++ conmebol/util.py | 3 + exceptions.py | 11 ++ requirements.txt | 4 + static/assets/football.svg | 313 ++++++++++++++++++++++++++++++++++++ templates/index.html | 14 ++ vercel.json | 6 + 14 files changed, 679 insertions(+), 1 deletion(-) create mode 100644 app.py create mode 100644 conmebol/__init__.py create mode 100644 conmebol/classification.py create mode 100644 conmebol/flags.py create mode 100644 conmebol/matches.py create mode 100644 conmebol/models/__init__.py create mode 100644 conmebol/results.py create mode 100644 conmebol/util.py create mode 100644 exceptions.py create mode 100644 requirements.txt create mode 100644 static/assets/football.svg create mode 100644 templates/index.html create mode 100644 vercel.json diff --git a/README.md b/README.md index ab7e2fe..9d7d846 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# conmebol \ No newline at end of file +# conmebol-api +CONMEBOL API te permite obtener información de los resultados, clasificación y próximos partidos referentes a la clasificación de la CONMEBOL para la Copa América. + +los datos se obtienen de [onefootball](https://onefootball.com/en/home) + +## URL Base +``` +https://conmebol-api.vercel.app/ +``` + +## Endpoints +`GET /`: Muestra un mensaje de bienvenida y proporciona un enlace a la documentación de la API. + +`GET /api/classification`: Permite obtener las clasificaciones de los países que forman parte de la CONMEBOL. + +`GET /api/results`: Permite obtener los resultados de las últimas jornadas de los partidos jugados en la CONMEBOL. + +`GET /api/matches`: Permite obtener información sobre los próximos partidos de la CONMEBOL y los partidos que se están jugando en vivo. + +### Uso +En el siguiente enlace devolverá un objeto json de los resultados de los últimos días de los partidos jugados. + +``` +https://conmebol-api.vercel.app/api/results +``` \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..a342ae3 --- /dev/null +++ b/app.py @@ -0,0 +1,37 @@ +import json + +from flask import Flask, render_template +from flask_cors import CORS +from exceptions import page_not_found, internal_server_error +from conmebol import Classification, Results, Matches + +app = Flask(__name__) +CORS(app) + +app.register_error_handler(404, page_not_found) +app.register_error_handler(500, internal_server_error) + +def render_json(_object: dict): + response = app.response_class( + response=json.dumps(_object), + status=200, + mimetype='application/json' + ) + + return response + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/api/classification') +def classification(): + return render_json(Classification().get_positions) + +@app.route('/api/results') +def results(): + return render_json(Results().get_results) + +@app.route('/api/matches') +def matches(): + return render_json(Matches().get_matches) \ No newline at end of file diff --git a/conmebol/__init__.py b/conmebol/__init__.py new file mode 100644 index 0000000..84aa256 --- /dev/null +++ b/conmebol/__init__.py @@ -0,0 +1,3 @@ +from conmebol.classification import Classification +from conmebol.results import Results +from conmebol.matches import Matches \ No newline at end of file diff --git a/conmebol/classification.py b/conmebol/classification.py new file mode 100644 index 0000000..e9fcc72 --- /dev/null +++ b/conmebol/classification.py @@ -0,0 +1,60 @@ +import httpx +from bs4 import BeautifulSoup + +from .models import Statistics +from .util import API_CLASSIFICATION +# from flags import participants + +def _get_the_scores(soup: BeautifulSoup): + values = [value.text for value in soup] + + if len(values) >= 2: + score_one, score_two = values[:2] + score_three = values[2] if len(values) > 2 else None + + if score_three == None: + return score_one, score_two + return score_one, score_two, score_three + +class Classification(object): + def __init__(self) -> None: + self.response = httpx.get(API_CLASSIFICATION, timeout=10.0) + self.response.raise_for_status() + + def _get_statistics_country(self, soup: BeautifulSoup): + from dataclasses import asdict + + _standing = soup.find_all('li', 'Standing_standings__rowLink__Skr86') + self.statistics = {'results': []} + + for standing in _standing: + position = standing.find('div', 'Standing_standings__cell__5Kd0W').text + label = standing.find('div', 'Standing_standings__cellIcon__EbcOR').get('title') + country = standing.find('p', 'title-7-medium Standing_standings__teamName__psv61').text + # flag = participants[country] + + matches_played, goal_difference = _get_the_scores(standing.find_all('div', 'Standing_standings__cell__5Kd0W Standing_standings__cellTextDimmed__vpZYH')) + won, tied, losses = _get_the_scores(standing.find_all('div', 'Standing_standings__cellLargeScreen__ttPap')) + points = standing.find('span', 'title-7-bold').text + + data = Statistics( + # flag=flag, + country=country, + position=position, + label=label, + matches_played=matches_played, + won=won, tied=tied, losses=losses, + goal_difference=goal_difference, + points=points + ) + + self.statistics['results'].append(asdict(data)) + + @property + def get_positions(self): + soup = BeautifulSoup(self.response.content, "html.parser") + section_classification_country = soup.find('div', 'xpaLayoutContainerComponentResolver--standings') + + self._get_statistics_country(section_classification_country) + + return self.statistics \ No newline at end of file diff --git a/conmebol/flags.py b/conmebol/flags.py new file mode 100644 index 0000000..000f7ed --- /dev/null +++ b/conmebol/flags.py @@ -0,0 +1,12 @@ +participants = { + "Argentina": "🇦🇷", + "Bolivia": "🇧🇴", + "Brasil": "🇧🇷", + "Colombia": "🇨🇴", + "Chile": "🇨🇱", + "Uruguay": "🇺🇾", + "Paraguay": "🇵🇾", + "Venezuela": "🇻🇪", + "Ecuador": "🇪🇨", + "Perú": "🇵🇪" +} \ No newline at end of file diff --git a/conmebol/matches.py b/conmebol/matches.py new file mode 100644 index 0000000..8fb755d --- /dev/null +++ b/conmebol/matches.py @@ -0,0 +1,88 @@ +import httpx +from bs4 import BeautifulSoup + +from .util import API_MATCHES +from .models import NextMatches, LiveMatches +# from flags import participants + +def get_match_prox_or_live(_journeys: list, matches: BeautifulSoup): + from dataclasses import asdict + + team_names = [x.text for team in matches for x in team.find_all('span', 'SimpleMatchCardTeam_simpleMatchCardTeam__name__7Ud8D')] + goals = [x.text for team in matches for x in team.find_all('span', 'SimpleMatchCardTeam_simpleMatchCardTeam__score__UYMc_')] + match_dates = [x.find('time').get('datetime') if x.find('time') else x.text for team in matches for x in team.find_all('div', 'SimpleMatchCard_simpleMatchCard__matchContent__prwTf')] + + team_counter = 0 + date_index = 0 + journey_index = 0 + result_counter = 0 + results = {} + + for i in range(len(team_names)): + team_counter += 1 + + if team_counter == 10: + for j in range(i-9, i+1): + result_counter += 1 + + if result_counter == 2: + if all(g == '' for g in [goals[j-1], goals[j]]): + first_team = team_names[j-1] + second_team = team_names[j] + date = match_dates[date_index] + + data = NextMatches( + first_team=first_team, + second_team=second_team, + date=date + ) + + if _journeys[journey_index] not in results: + results[_journeys[journey_index]] = [] + + results[_journeys[journey_index]].append(asdict(data)) + + date_index += 1 + result_counter = 0 + else: + first_team = {'country': team_names[j-1], 'goals': goals[j-1]} + second_team = {'country': team_names[j-1], 'goals': goals[j-1]} + winner = 'Tie' if goals[j-1] == goals[j] else team_names[j-1] if goals[j-1] > goals[j] else team_names[j] + time = match_dates[date_index] + + data = LiveMatches( + first_team=first_team, + second_team=second_team, + winner=winner, + time=time + ) + + if _journeys[journey_index] not in results: + results[_journeys[journey_index]] = [] + + results[_journeys[journey_index]].append(asdict(data)) + + date_index += 1 + team_counter = 0 + + journey_index += 1 + team_counter = 0 + + return results + +class Matches(object): + def __init__(self) -> None: + self.response = httpx.get(API_MATCHES, timeout=10.0) + self.response.raise_for_status() + + @property + def get_matches(self): + soup = BeautifulSoup(self.response.content, "html.parser") + section_matches = soup.find('div', 'MatchCardsListsAppender_container__y5ame') + + _journeys = [journey.text for journey in section_matches.find_all('div', 'SectionHeader_container__iVfZ9')] + _matches = section_matches.find_all('div', 'SimpleMatchCard_simpleMatchCard__content__ZWt2p') + + self.results = get_match_prox_or_live(_journeys, _matches) + + return self.results \ No newline at end of file diff --git a/conmebol/models/__init__.py b/conmebol/models/__init__.py new file mode 100644 index 0000000..d2ec1c6 --- /dev/null +++ b/conmebol/models/__init__.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass + +@dataclass +class Statistics: + # flag: str + country: str + position: int + label: str + matches_played: int + won: int + tied: int + losses: int + goal_difference: int + points: int + +@dataclass +class LastMatches: + first_team: dict + second_team: dict + winner: str + date: str + +@dataclass +class NextMatches: + first_team: str + second_team: str + date: str + +@dataclass +class LiveMatches: + first_team: dict + second_team: dict + winner: str + time: str \ No newline at end of file diff --git a/conmebol/results.py b/conmebol/results.py new file mode 100644 index 0000000..ff82256 --- /dev/null +++ b/conmebol/results.py @@ -0,0 +1,69 @@ +import httpx +from bs4 import BeautifulSoup + +from .models import LastMatches +from .util import API_RESULTS +# from flags import participants + +def get_match_statistics(_journeys: list, matches: BeautifulSoup): + from dataclasses import asdict + + team_names = [x.text for team in matches for x in team.find_all('span', 'SimpleMatchCardTeam_simpleMatchCardTeam__name__7Ud8D')] + goals = [x.text for team in matches for x in team.find_all('span', 'SimpleMatchCardTeam_simpleMatchCardTeam__score__UYMc_')] + match_dates = [x.find('time').get('datetime') for team in matches for x in team.find_all('div', 'SimpleMatchCard_simpleMatchCard__matchContent__prwTf')] + + team_counter = 0 + date_index = 0 + journey_index = 0 + result_counter = 0 + results = {} + + for i in range(len(team_names)): + team_counter += 1 + + if team_counter == 10: + for j in range(i-9, i+1): + result_counter += 1 + + if result_counter == 2: + first_team = {'country': team_names[j-1], 'goals': goals[j-1]} + second_team = {'country': team_names[j], 'goals': goals[j]} + winner = 'Tie' if goals[j-1] == goals[j] else team_names[j-1] if goals[j-1] > goals[j] else team_names[j] + date = match_dates[date_index] + + data = LastMatches( + first_team=first_team, + second_team=second_team, + winner=winner, + date=date + ) + + if _journeys[journey_index] not in results: + results[_journeys[journey_index]] = [] + + results[_journeys[journey_index]].append(asdict(data)) + + date_index += 1 + result_counter = 0 + + journey_index += 1 + team_counter = 0 + + return results + +class Results(object): + def __init__(self) -> None: + self.response = httpx.get(API_RESULTS, timeout=10.0) + self.response.raise_for_status() + + @property + def get_results(self): + soup = BeautifulSoup(self.response.content, "html.parser") + section_journeys = soup.find('div', 'MatchCardsListsAppender_container__y5ame') + + _journeys = [journey.text for journey in section_journeys.find_all('div', 'SectionHeader_container__iVfZ9')] + _matches = section_journeys.find_all('div', 'SimpleMatchCard_simpleMatchCard__content__ZWt2p') + + self.results = get_match_statistics(_journeys, _matches) + + return self.results \ No newline at end of file diff --git a/conmebol/util.py b/conmebol/util.py new file mode 100644 index 0000000..6335d0a --- /dev/null +++ b/conmebol/util.py @@ -0,0 +1,3 @@ +API_MATCHES='https://onefootball.com/es/competicion/conmebol-eliminatorias-copa-mundial-74/partidos' +API_RESULTS='https://onefootball.com/es/competicion/conmebol-eliminatorias-copa-mundial-74/resultados' +API_CLASSIFICATION='https://onefootball.com/es/competicion/conmebol-eliminatorias-copa-mundial-74/clasificacion' \ No newline at end of file diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..971f30f --- /dev/null +++ b/exceptions.py @@ -0,0 +1,11 @@ +from flask import jsonify + +def page_not_found(e): + message = jsonify({"error": "Sorry, the page you were looking for could not be found."}) + message.status_code = 404 + return message + +def internal_server_error(e): + message = jsonify({"error": "Sorry, an internal problem has occurred on the server. Please try again later."}) + message.status_code = 500 + return message \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8d5c7a7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +flask_cors +httpx +bs4 \ No newline at end of file diff --git a/static/assets/football.svg b/static/assets/football.svg new file mode 100644 index 0000000..afa473e --- /dev/null +++ b/static/assets/football.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..dd3dd1f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,14 @@ + + + CONMEBOL API Documentation + + + +

+ CONMEBOL API + Icono +

+ +

Bienvenido a la API de CONMEBOL. Puedes ver la documentación en el repositorio de github: https://github.com/fcoagz/conmebol

+ + \ No newline at end of file diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..eef104f --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "builds": [ + {"src":"app.py", "use":"@vercel/python"}], + "routes":[ + {"src":"/(.*)", "dest":"app.py"}] +} \ No newline at end of file