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.
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
25
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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