grid-minion 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grid_minion-0.1.0/LICENSE +21 -0
- grid_minion-0.1.0/PKG-INFO +129 -0
- grid_minion-0.1.0/README.md +108 -0
- grid_minion-0.1.0/pyproject.toml +37 -0
- grid_minion-0.1.0/setup.cfg +4 -0
- grid_minion-0.1.0/src/grid_minion/__init__.py +25 -0
- grid_minion-0.1.0/src/grid_minion/exceptions.py +30 -0
- grid_minion-0.1.0/src/grid_minion/graphql_client.py +282 -0
- grid_minion-0.1.0/src/grid_minion/observers/__init__.py +18 -0
- grid_minion-0.1.0/src/grid_minion/observers/base.py +20 -0
- grid_minion-0.1.0/src/grid_minion/observers/drafts.py +169 -0
- grid_minion-0.1.0/src/grid_minion/observers/objectives.py +68 -0
- grid_minion-0.1.0/src/grid_minion/observers/processor.py +84 -0
- grid_minion-0.1.0/src/grid_minion/observers/stats.py +135 -0
- grid_minion-0.1.0/src/grid_minion/observers/teams.py +160 -0
- grid_minion-0.1.0/src/grid_minion/observers/vision.py +60 -0
- grid_minion-0.1.0/src/grid_minion/rest_client.py +218 -0
- grid_minion-0.1.0/src/grid_minion/utils.py +51 -0
- grid_minion-0.1.0/src/grid_minion.egg-info/PKG-INFO +129 -0
- grid_minion-0.1.0/src/grid_minion.egg-info/SOURCES.txt +24 -0
- grid_minion-0.1.0/src/grid_minion.egg-info/dependency_links.txt +1 -0
- grid_minion-0.1.0/src/grid_minion.egg-info/requires.txt +9 -0
- grid_minion-0.1.0/src/grid_minion.egg-info/top_level.txt +1 -0
- grid_minion-0.1.0/tests/test_gql_mock.py +77 -0
- grid_minion-0.1.0/tests/test_observers.py +78 -0
- grid_minion-0.1.0/tests/test_utils.py +23 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Iznardo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grid-minion
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cliente no oficial de Python para las APIs de GRID (League of Legends)
|
|
5
|
+
Author-email: Iznardo <contact@f1re.es>
|
|
6
|
+
Project-URL: Homepage, https://github.com/tu_usuario/grid-minion
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.28.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: build; extra == "dev"
|
|
17
|
+
Requires-Dist: twine; extra == "dev"
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "test"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Grid Minion
|
|
23
|
+
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
[](https://www.python.org/downloads/)
|
|
26
|
+
|
|
27
|
+
Grid Minion es un cliente de Python ligero y robusto para interactuar con las APIs de GRID.gg (Data Central y Live Data), diseñado específicamente para analistas y desarrolladores de League of Legends.
|
|
28
|
+
|
|
29
|
+
Esta libreria no solo facilita la descarga de datos, sino que implementa un sistema de Observadores que procesa y cruza automaticamente los datos de GRID y Riot (PUUIDs, KDA, Objetivos, Vision) en tiempo real o diferido.
|
|
30
|
+
|
|
31
|
+
## Caracteristicas
|
|
32
|
+
|
|
33
|
+
* Clientes Especializados:
|
|
34
|
+
* GridGraphQLClient: Consulta de metadatos, torneos y series con paginacion automatica.
|
|
35
|
+
* GridRestClient: Descarga de archivos (Summary, LiveStats, GRID Events) con gestion de descompresion ZIP.
|
|
36
|
+
* Sistema de Observadores: Procesa eventos complejos y mantiene el estado de la partida (Drafts, Inventarios, Posicionamiento).
|
|
37
|
+
* Robustez Industrial:
|
|
38
|
+
* Gestion avanzada de Rate Limits (HTTP 429 y GraphQL body errors) con reintentos exponenciales.
|
|
39
|
+
* Jerarquia de excepciones personalizadas (GridRateLimitError, GridAuthError, etc.).
|
|
40
|
+
* Sistema de logging integrado.
|
|
41
|
+
|
|
42
|
+
## Instalacion
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install grid-minion
|
|
46
|
+
```
|
|
47
|
+
(O clonando el repo para desarrollo)
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/tu_usuario/pyGrid.git
|
|
50
|
+
cd pyGrid
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Inicio Rapido
|
|
55
|
+
|
|
56
|
+
### 1. Consultar Series (GraphQL)
|
|
57
|
+
```python
|
|
58
|
+
from grid_minion import GridGraphQLClient
|
|
59
|
+
|
|
60
|
+
client = GridGraphQLClient(api_key="TU_API_KEY")
|
|
61
|
+
|
|
62
|
+
# Obtener IDs de series de la LEC en un rango de fechas
|
|
63
|
+
series_ids = client.get_series(
|
|
64
|
+
start_time="2024-01-01T00:00:00Z",
|
|
65
|
+
tournament_ids=["ID_TORNEO"]
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Descargar y Procesar Partidas (Observers)
|
|
70
|
+
El corazon de la libreria es el GameEventProcessor. Puedes enganchar diferentes observadores para obtener exactamente los datos que necesitas.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from grid_minion import GridRestClient, split_grid_series
|
|
74
|
+
from grid_minion.observers import (
|
|
75
|
+
GameEventProcessor, TeamsObserver, DraftObserver,
|
|
76
|
+
PostGameObserver, ObjectiveKilledObserver, WardsObserver
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
client = GridRestClient(api_key="TU_API_KEY")
|
|
80
|
+
processor = GameEventProcessor()
|
|
81
|
+
|
|
82
|
+
# Instanciar observadores
|
|
83
|
+
teams_obs = TeamsObserver()
|
|
84
|
+
draft_obs = DraftObserver()
|
|
85
|
+
wards_obs = WardsObserver(teams_observer=teams_obs) # Dependencia de equipos
|
|
86
|
+
|
|
87
|
+
# Suscribirlos al procesador
|
|
88
|
+
processor.attach(teams_obs)
|
|
89
|
+
processor.attach(draft_obs)
|
|
90
|
+
processor.attach(wards_obs)
|
|
91
|
+
|
|
92
|
+
# Descargar datos
|
|
93
|
+
grid_events = client.get_grid_events("SERIES_ID")
|
|
94
|
+
riot_summary = client.get_riot_summary("SERIES_ID", game_number=1)
|
|
95
|
+
|
|
96
|
+
# Procesar todo el paquete (el procesador gestiona el orden de ingesta)
|
|
97
|
+
processor.process_bundle(
|
|
98
|
+
grid_livestats=grid_events,
|
|
99
|
+
riot_summary=riot_summary
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Acceder a los resultados limpios
|
|
103
|
+
print(f"Draft: {draft_obs.get_draft()}")
|
|
104
|
+
print(f"Wards totales: {len(wards_obs.get_wards())}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Manejo de Errores
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from grid_minion import GridRateLimitError, GridAuthError
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
data = client.query_central(query)
|
|
114
|
+
except GridRateLimitError:
|
|
115
|
+
print("Se agotaron los reintentos de velocidad.")
|
|
116
|
+
except GridAuthError:
|
|
117
|
+
print("API Key invalida.")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
Ejecutar la suite de pruebas unitarias e integracion:
|
|
123
|
+
```bash
|
|
124
|
+
python -m unittest discover tests
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Licencia
|
|
128
|
+
|
|
129
|
+
Este proyecto esta bajo la Licencia MIT. Consulta el archivo LICENSE para mas detalles.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Grid Minion
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
|
|
6
|
+
Grid Minion es un cliente de Python ligero y robusto para interactuar con las APIs de GRID.gg (Data Central y Live Data), diseñado específicamente para analistas y desarrolladores de League of Legends.
|
|
7
|
+
|
|
8
|
+
Esta libreria no solo facilita la descarga de datos, sino que implementa un sistema de Observadores que procesa y cruza automaticamente los datos de GRID y Riot (PUUIDs, KDA, Objetivos, Vision) en tiempo real o diferido.
|
|
9
|
+
|
|
10
|
+
## Caracteristicas
|
|
11
|
+
|
|
12
|
+
* Clientes Especializados:
|
|
13
|
+
* GridGraphQLClient: Consulta de metadatos, torneos y series con paginacion automatica.
|
|
14
|
+
* GridRestClient: Descarga de archivos (Summary, LiveStats, GRID Events) con gestion de descompresion ZIP.
|
|
15
|
+
* Sistema de Observadores: Procesa eventos complejos y mantiene el estado de la partida (Drafts, Inventarios, Posicionamiento).
|
|
16
|
+
* Robustez Industrial:
|
|
17
|
+
* Gestion avanzada de Rate Limits (HTTP 429 y GraphQL body errors) con reintentos exponenciales.
|
|
18
|
+
* Jerarquia de excepciones personalizadas (GridRateLimitError, GridAuthError, etc.).
|
|
19
|
+
* Sistema de logging integrado.
|
|
20
|
+
|
|
21
|
+
## Instalacion
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install grid-minion
|
|
25
|
+
```
|
|
26
|
+
(O clonando el repo para desarrollo)
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/tu_usuario/pyGrid.git
|
|
29
|
+
cd pyGrid
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Inicio Rapido
|
|
34
|
+
|
|
35
|
+
### 1. Consultar Series (GraphQL)
|
|
36
|
+
```python
|
|
37
|
+
from grid_minion import GridGraphQLClient
|
|
38
|
+
|
|
39
|
+
client = GridGraphQLClient(api_key="TU_API_KEY")
|
|
40
|
+
|
|
41
|
+
# Obtener IDs de series de la LEC en un rango de fechas
|
|
42
|
+
series_ids = client.get_series(
|
|
43
|
+
start_time="2024-01-01T00:00:00Z",
|
|
44
|
+
tournament_ids=["ID_TORNEO"]
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Descargar y Procesar Partidas (Observers)
|
|
49
|
+
El corazon de la libreria es el GameEventProcessor. Puedes enganchar diferentes observadores para obtener exactamente los datos que necesitas.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from grid_minion import GridRestClient, split_grid_series
|
|
53
|
+
from grid_minion.observers import (
|
|
54
|
+
GameEventProcessor, TeamsObserver, DraftObserver,
|
|
55
|
+
PostGameObserver, ObjectiveKilledObserver, WardsObserver
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
client = GridRestClient(api_key="TU_API_KEY")
|
|
59
|
+
processor = GameEventProcessor()
|
|
60
|
+
|
|
61
|
+
# Instanciar observadores
|
|
62
|
+
teams_obs = TeamsObserver()
|
|
63
|
+
draft_obs = DraftObserver()
|
|
64
|
+
wards_obs = WardsObserver(teams_observer=teams_obs) # Dependencia de equipos
|
|
65
|
+
|
|
66
|
+
# Suscribirlos al procesador
|
|
67
|
+
processor.attach(teams_obs)
|
|
68
|
+
processor.attach(draft_obs)
|
|
69
|
+
processor.attach(wards_obs)
|
|
70
|
+
|
|
71
|
+
# Descargar datos
|
|
72
|
+
grid_events = client.get_grid_events("SERIES_ID")
|
|
73
|
+
riot_summary = client.get_riot_summary("SERIES_ID", game_number=1)
|
|
74
|
+
|
|
75
|
+
# Procesar todo el paquete (el procesador gestiona el orden de ingesta)
|
|
76
|
+
processor.process_bundle(
|
|
77
|
+
grid_livestats=grid_events,
|
|
78
|
+
riot_summary=riot_summary
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Acceder a los resultados limpios
|
|
82
|
+
print(f"Draft: {draft_obs.get_draft()}")
|
|
83
|
+
print(f"Wards totales: {len(wards_obs.get_wards())}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Manejo de Errores
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from grid_minion import GridRateLimitError, GridAuthError
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
data = client.query_central(query)
|
|
93
|
+
except GridRateLimitError:
|
|
94
|
+
print("Se agotaron los reintentos de velocidad.")
|
|
95
|
+
except GridAuthError:
|
|
96
|
+
print("API Key invalida.")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Tests
|
|
100
|
+
|
|
101
|
+
Ejecutar la suite de pruebas unitarias e integracion:
|
|
102
|
+
```bash
|
|
103
|
+
python -m unittest discover tests
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Licencia
|
|
107
|
+
|
|
108
|
+
Este proyecto esta bajo la Licencia MIT. Consulta el archivo LICENSE para mas detalles.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "grid-minion"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cliente no oficial de Python para las APIs de GRID (League of Legends)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Iznardo", email = "contact@f1re.es" },
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"requests>=2.28.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"python-dotenv>=1.0.0",
|
|
26
|
+
"build",
|
|
27
|
+
"twine",
|
|
28
|
+
]
|
|
29
|
+
test = [
|
|
30
|
+
"python-dotenv>=1.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
"Homepage" = "https://github.com/tu_usuario/grid-minion"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .graphql_client import GridGraphQLClient
|
|
2
|
+
from .rest_client import GridRestClient
|
|
3
|
+
from .utils import split_grid_series
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
GridError,
|
|
6
|
+
GridAPIError,
|
|
7
|
+
GridAuthError,
|
|
8
|
+
GridRateLimitError,
|
|
9
|
+
GridResourceNotFoundError,
|
|
10
|
+
GridNetworkError,
|
|
11
|
+
GridDataError
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GridGraphQLClient",
|
|
16
|
+
"GridRestClient",
|
|
17
|
+
"split_grid_series",
|
|
18
|
+
"GridError",
|
|
19
|
+
"GridAPIError",
|
|
20
|
+
"GridAuthError",
|
|
21
|
+
"GridRateLimitError",
|
|
22
|
+
"GridResourceNotFoundError",
|
|
23
|
+
"GridNetworkError",
|
|
24
|
+
"GridDataError"
|
|
25
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class GridError(Exception):
|
|
2
|
+
"""Clase base para todas las excepciones de Grid Minion."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class GridAPIError(GridError):
|
|
6
|
+
"""Error devuelto por la API de GRID (status codes no exitosos o errores en el body)."""
|
|
7
|
+
def __init__(self, message, status_code=None, details=None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.details = details
|
|
11
|
+
|
|
12
|
+
class GridAuthError(GridAPIError):
|
|
13
|
+
"""Error de autenticación (API Key inválida o sin permisos)."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class GridRateLimitError(GridAPIError):
|
|
17
|
+
"""Límite de velocidad (Rate Limit) alcanzado y reintentos agotados."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class GridResourceNotFoundError(GridAPIError):
|
|
21
|
+
"""El recurso solicitado (ej: archivo de partida) no existe (404)."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
class GridNetworkError(GridError):
|
|
25
|
+
"""Error de conexión, timeout o red al intentar comunicar con GRID."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
class GridDataError(GridError):
|
|
29
|
+
"""Error al procesar, descomprimir o parsear los datos recibidos."""
|
|
30
|
+
pass
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
from typing import List, Dict, Any, Optional, Union
|
|
6
|
+
from .exceptions import GridAPIError, GridRateLimitError, GridNetworkError, GridError
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class GridGraphQLClient:
|
|
11
|
+
"""
|
|
12
|
+
Cliente para interactuar con las APIs de GraphQL de GRID.
|
|
13
|
+
|
|
14
|
+
Proporciona métodos para consultar metadatos (Data Central) y el estado
|
|
15
|
+
en tiempo real de las series (Live Data).
|
|
16
|
+
"""
|
|
17
|
+
URL_CENTRAL = 'https://api.grid.gg/central-data/graphql'
|
|
18
|
+
URL_LIVE = 'https://api.grid.gg/live-data-feed/series-state/graphql'
|
|
19
|
+
|
|
20
|
+
def __init__(self, api_key: str, max_retries: int = 7):
|
|
21
|
+
"""
|
|
22
|
+
Inicializa el cliente GraphQL.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key (str): Tu clave de API de GRID.
|
|
26
|
+
max_retries (int): Número máximo de reintentos para Rate Limits (default: 7).
|
|
27
|
+
"""
|
|
28
|
+
self.max_retries = max_retries
|
|
29
|
+
self.session = requests.Session()
|
|
30
|
+
self.session.headers.update({
|
|
31
|
+
"x-api-key": api_key,
|
|
32
|
+
"Content-Type": "application/json"
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
def _execute(self, url: str, query: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Ejecuta una petición GraphQL con lógica de reintentos y manejo de errores.
|
|
38
|
+
"""
|
|
39
|
+
payload = {"query": query}
|
|
40
|
+
if variables:
|
|
41
|
+
payload["variables"] = variables
|
|
42
|
+
|
|
43
|
+
# Bucle de reintentos
|
|
44
|
+
for attempt in range(1, self.max_retries + 1):
|
|
45
|
+
try:
|
|
46
|
+
# Factor de espera: 2s, 3s, 4.5s, 6.75s... (Más lento que antes)
|
|
47
|
+
default_wait = 10 * (1.5 ** (attempt - 1))
|
|
48
|
+
|
|
49
|
+
response = self.session.post(url, json=payload)
|
|
50
|
+
|
|
51
|
+
# --- 1. GESTIÓN DE RATE LIMIT HTTP (429) ---
|
|
52
|
+
if response.status_code == 429:
|
|
53
|
+
retry_after = response.headers.get("Retry-After")
|
|
54
|
+
wait_time = float(retry_after) if retry_after else default_wait
|
|
55
|
+
|
|
56
|
+
if attempt == self.max_retries:
|
|
57
|
+
raise GridRateLimitError(f"Rate Limit HTTP 429 persistente tras {self.max_retries} intentos.", status_code=429)
|
|
58
|
+
|
|
59
|
+
logger.warning(f"HTTP 429. Esperando {wait_time:.2f}s... ({attempt}/{self.max_retries})")
|
|
60
|
+
time.sleep(wait_time)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Si es otro error HTTP (5xx, etc)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
|
|
66
|
+
# --- 2. GESTIÓN DE ERRORES EN EL BODY (GraphQL) ---
|
|
67
|
+
data = response.json()
|
|
68
|
+
|
|
69
|
+
if "errors" in data:
|
|
70
|
+
is_rate_limit = False
|
|
71
|
+
error_msg = str(data['errors'])
|
|
72
|
+
|
|
73
|
+
for error in data["errors"]:
|
|
74
|
+
msg = error.get("message", "").lower()
|
|
75
|
+
code = error.get("extensions", {}).get("errorDetail", "")
|
|
76
|
+
|
|
77
|
+
if "rate limit" in msg or "ENHANCE_YOUR_CALM" in code:
|
|
78
|
+
is_rate_limit = True
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
if is_rate_limit:
|
|
82
|
+
# Si es el último intento, lanzamos error y no esperamos más
|
|
83
|
+
if attempt == self.max_retries:
|
|
84
|
+
raise GridRateLimitError(f"Rate Limit GraphQL persistente tras {self.max_retries} intentos. GRID dice: {error_msg}", details=data['errors'])
|
|
85
|
+
|
|
86
|
+
logger.warning(f"Rate Limit GraphQL detectado. Esperando {default_wait:.2f}s... ({attempt}/{self.max_retries})")
|
|
87
|
+
time.sleep(default_wait)
|
|
88
|
+
continue # Reintentamos
|
|
89
|
+
|
|
90
|
+
# Error legítimo de sintaxis o lógica (no se reintenta)
|
|
91
|
+
raise GridAPIError(f"GraphQL Error: {data['errors']}", details=data['errors'])
|
|
92
|
+
|
|
93
|
+
# ÉXITO
|
|
94
|
+
return data.get("data", {})
|
|
95
|
+
|
|
96
|
+
except requests.exceptions.RequestException as e:
|
|
97
|
+
if attempt == self.max_retries:
|
|
98
|
+
raise GridNetworkError(f"Error de conexión Final tras {self.max_retries} intentos: {e}")
|
|
99
|
+
|
|
100
|
+
logger.warning(f"Error de red ({e}). Reintentando en {default_wait:.2f}s...")
|
|
101
|
+
time.sleep(default_wait)
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Este punto teóricamente es inalcanzable por los 'raise' en el último intento,
|
|
105
|
+
# pero por seguridad:
|
|
106
|
+
raise GridError("Error crítico: Fallo en la lógica de reintentos.")
|
|
107
|
+
|
|
108
|
+
def query_central(self, query_body: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Ejecuta una consulta en GRID Data Central (Metadatos).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
query_body (str): Cuerpo de la consulta GraphQL.
|
|
114
|
+
variables (Optional[Dict]): Variables para la consulta.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dict[str, Any]: Datos devueltos por la API.
|
|
118
|
+
"""
|
|
119
|
+
return self._execute(self.URL_CENTRAL, query_body, variables)
|
|
120
|
+
|
|
121
|
+
def query_live(self, query_body: str, variables: Optional[Dict] = None) -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Ejecuta una consulta en GRID Live Data (Series State).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
query_body (str): Cuerpo de la consulta GraphQL.
|
|
127
|
+
variables (Optional[Dict]): Variables para la consulta.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dict[str, Any]: Datos devueltos por la API.
|
|
131
|
+
"""
|
|
132
|
+
return self._execute(self.URL_LIVE, query_body, variables)
|
|
133
|
+
|
|
134
|
+
def get_series(self,
|
|
135
|
+
start_time: Optional[str] = None,
|
|
136
|
+
end_time: Optional[str] = None,
|
|
137
|
+
game_type: Optional[str] = None,
|
|
138
|
+
title_id: Union[int, List[int]] = 3,
|
|
139
|
+
page_games: int = 25,
|
|
140
|
+
team_ids: Union[str, List[str]] = None,
|
|
141
|
+
tournament_ids: Union[str, List[str]] = None) -> List[str]:
|
|
142
|
+
"""
|
|
143
|
+
Obtiene una lista de IDs de series filtradas por diversos criterios.
|
|
144
|
+
Maneja automáticamente la paginación de la API.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
start_time (Optional[str]): Fecha de inicio (ISO 8601).
|
|
148
|
+
end_time (Optional[str]): Fecha de fin (ISO 8601).
|
|
149
|
+
game_type (Optional[str]): Tipo de partida (ej: 'COMPETITIVE').
|
|
150
|
+
title_id (Union[int, List[int]]): ID del juego (default: 3 para LoL).
|
|
151
|
+
page_games (int): Número de series por página (max: 25).
|
|
152
|
+
team_ids (Optional[Union[str, List[str]]]): IDs de equipos.
|
|
153
|
+
tournament_ids (Optional[Union[str, List[str]]]): IDs de torneos.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List[str]: Lista de IDs de series encontrados.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
all_ids = []
|
|
160
|
+
filter_parts = ""
|
|
161
|
+
|
|
162
|
+
if start_time and end_time:
|
|
163
|
+
filter_parts += f'startTimeScheduled: {{ gte: "{start_time}", lte: "{end_time}" }}'
|
|
164
|
+
elif start_time:
|
|
165
|
+
filter_parts += f'startTimeScheduled: {{ gte: "{start_time}" }}'
|
|
166
|
+
elif end_time:
|
|
167
|
+
filter_parts += f'startTimeScheduled: {{ lte: "{end_time}" }}'
|
|
168
|
+
|
|
169
|
+
filter_parts += f'\n titleIds: {{ in: {json.dumps(title_id)} }}'
|
|
170
|
+
|
|
171
|
+
if game_type:
|
|
172
|
+
filter_parts += f'\n types: {game_type}'
|
|
173
|
+
|
|
174
|
+
if team_ids:
|
|
175
|
+
if isinstance(team_ids, list):
|
|
176
|
+
filter_parts += f'\n teamIds: {{ in: {json.dumps(team_ids)} }}'
|
|
177
|
+
else:
|
|
178
|
+
filter_parts += f'\n teamId: "{team_ids}"'
|
|
179
|
+
|
|
180
|
+
if tournament_ids:
|
|
181
|
+
if isinstance(tournament_ids, list):
|
|
182
|
+
filter_parts += f'\n tournamentIds: {{ in: {json.dumps(tournament_ids)} }}'
|
|
183
|
+
else:
|
|
184
|
+
filter_parts += f'\n tournamentId: "{tournament_ids}"'
|
|
185
|
+
|
|
186
|
+
def _fetch_page(cursor=""):
|
|
187
|
+
body = """
|
|
188
|
+
query GetGames {{
|
|
189
|
+
allSeries(
|
|
190
|
+
after: "{cursor}"
|
|
191
|
+
first: {page_games}
|
|
192
|
+
filter: {{
|
|
193
|
+
{filter_parts}
|
|
194
|
+
}}
|
|
195
|
+
orderBy: StartTimeScheduled
|
|
196
|
+
) {{
|
|
197
|
+
totalCount
|
|
198
|
+
pageInfo {{
|
|
199
|
+
hasNextPage
|
|
200
|
+
endCursor
|
|
201
|
+
}}
|
|
202
|
+
edges {{
|
|
203
|
+
node {{
|
|
204
|
+
id
|
|
205
|
+
}}
|
|
206
|
+
}}
|
|
207
|
+
}}
|
|
208
|
+
}}
|
|
209
|
+
""".format(cursor=cursor, page_games=page_games, filter_parts=filter_parts)
|
|
210
|
+
|
|
211
|
+
data = self.query_central(body)
|
|
212
|
+
series_data = data['allSeries']
|
|
213
|
+
|
|
214
|
+
nodes = series_data['edges']
|
|
215
|
+
current_ids = [n['node']['id'] for n in nodes]
|
|
216
|
+
|
|
217
|
+
return current_ids, series_data['pageInfo']['hasNextPage'], series_data['pageInfo']['endCursor']
|
|
218
|
+
|
|
219
|
+
has_next = True
|
|
220
|
+
cursor = ""
|
|
221
|
+
while has_next:
|
|
222
|
+
new_ids, has_next, cursor = _fetch_page(cursor)
|
|
223
|
+
all_ids.extend(new_ids)
|
|
224
|
+
|
|
225
|
+
return all_ids
|
|
226
|
+
|
|
227
|
+
def get_tournament_ids_by_name(self, parent_names: List[str]) -> List[str]:
|
|
228
|
+
"""
|
|
229
|
+
Busca IDs de torneos basándose en una lista de nombres o fragmentos de nombres.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
parent_names (List[str]): Lista de nombres a buscar (ej: ['LEC', 'LVP']).
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List[str]: Lista de IDs de torneos que coinciden con la búsqueda.
|
|
236
|
+
"""
|
|
237
|
+
found_ids = set()
|
|
238
|
+
for name in parent_names:
|
|
239
|
+
query = f"""
|
|
240
|
+
query {{
|
|
241
|
+
tournaments(first: 50, filter: {{ name: {{ contains: "{name}" }} }}) {{
|
|
242
|
+
edges {{
|
|
243
|
+
node {{
|
|
244
|
+
id
|
|
245
|
+
name
|
|
246
|
+
}}
|
|
247
|
+
}}
|
|
248
|
+
}}
|
|
249
|
+
}}
|
|
250
|
+
"""
|
|
251
|
+
data = self.query_central(query)
|
|
252
|
+
edges = data.get("tournaments", {}).get("edges", [])
|
|
253
|
+
for edge in edges:
|
|
254
|
+
if name in edge["node"]["name"]:
|
|
255
|
+
found_ids.add(edge["node"]["id"])
|
|
256
|
+
return list(found_ids)
|
|
257
|
+
|
|
258
|
+
def get_series_state(self, series_id: str) -> Dict[str, Any]:
|
|
259
|
+
"""
|
|
260
|
+
Obtiene el estado actual de una serie (Live Data).
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
series_id (str): ID de la serie de GRID.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dict[str, Any]: Diccionario con el estado de la serie y sus partidas.
|
|
267
|
+
"""
|
|
268
|
+
query = f"""
|
|
269
|
+
query {{
|
|
270
|
+
seriesState(id: {series_id}) {{
|
|
271
|
+
id
|
|
272
|
+
games {{
|
|
273
|
+
id
|
|
274
|
+
sequenceNumber
|
|
275
|
+
started
|
|
276
|
+
finished
|
|
277
|
+
}}
|
|
278
|
+
}}
|
|
279
|
+
}}
|
|
280
|
+
"""
|
|
281
|
+
data = self.query_live(query)
|
|
282
|
+
return data["seriesState"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .base import Observer
|
|
2
|
+
from .processor import GameEventProcessor
|
|
3
|
+
from .teams import TeamsObserver, Participant
|
|
4
|
+
from .drafts import DraftObserver
|
|
5
|
+
from .stats import PostGameObserver
|
|
6
|
+
from .objectives import ObjectiveKilledObserver
|
|
7
|
+
from .vision import WardsObserver
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Observer",
|
|
11
|
+
"GameEventProcessor",
|
|
12
|
+
"TeamsObserver",
|
|
13
|
+
"Participant",
|
|
14
|
+
"DraftObserver",
|
|
15
|
+
"PostGameObserver",
|
|
16
|
+
"ObjectiveKilledObserver",
|
|
17
|
+
"WardsObserver"
|
|
18
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
|
|
4
|
+
class Observer(ABC):
|
|
5
|
+
"""
|
|
6
|
+
Clase base abstracta para todos los observadores de eventos.
|
|
7
|
+
|
|
8
|
+
Cualquier clase que desee procesar eventos del GameEventProcessor debe
|
|
9
|
+
heredar de esta clase e implementar el método notify_event.
|
|
10
|
+
"""
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def notify_event(self, event: Dict[str, Any]):
|
|
13
|
+
"""
|
|
14
|
+
Recibe un evento y decide si procesarlo.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
event (Dict[str, Any]): Diccionario que contiene los datos del evento,
|
|
18
|
+
incluyendo 'source', 'type' o 'rfc461Schema' dependiendo de la fuente.
|
|
19
|
+
"""
|
|
20
|
+
pass
|