185 lines
5.3 KiB
Markdown
185 lines
5.3 KiB
Markdown
# qualifiers-uefa-api
|
|
|
|
Integriert die UEFA Digital API für Team-Daten, Koeffizienten und Clashes.
|
|
|
|
## Trigger
|
|
|
|
- UEFA API-Aufrufe
|
|
- Team-Synchronisation
|
|
- Clash-Updates
|
|
- "uefa api", "team sync", "clashes aktualisieren"
|
|
|
|
## Kontext
|
|
|
|
Die UEFA Digital API verwendet OAuth2 Client Credentials Flow. API-Skripte befinden sich in [qualifiers/uefadigitalapi/](qualifiers/uefadigitalapi/).
|
|
|
|
## Regeln
|
|
|
|
1. **Umgebungsvariablen für Credentials**:
|
|
```bash
|
|
UEFA_OAUTH_TENANT_ID=<azure-tenant-id>
|
|
UEFA_OAUTH_CLIENT_ID=<client-id>
|
|
UEFA_OAUTH_CLIENT_SECRET=<client-secret>
|
|
UEFA_OAUTH_SCOPE=<api-scope>
|
|
UEFA_SUBSCRIPTION_KEY=<subscription-key>
|
|
UEFA_SUBSCRIPTION_KEY_TOKEN=<subscription-key-for-token>
|
|
```
|
|
|
|
2. **Token-Abruf**:
|
|
```python
|
|
import requests
|
|
import os
|
|
|
|
def get_uefa_token():
|
|
tenant_id = os.getenv("UEFA_OAUTH_TENANT_ID")
|
|
response = requests.post(
|
|
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
data={
|
|
"grant_type": "client_credentials",
|
|
"client_id": os.getenv("UEFA_OAUTH_CLIENT_ID"),
|
|
"client_secret": os.getenv("UEFA_OAUTH_CLIENT_SECRET"),
|
|
"scope": os.getenv("UEFA_OAUTH_SCOPE"),
|
|
},
|
|
)
|
|
return response.json()['access_token']
|
|
```
|
|
|
|
3. **API-Aufruf mit Bearer Token**:
|
|
```python
|
|
token = get_uefa_token()
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Ocp-Apim-Subscription-Key": os.getenv("UEFA_SUBSCRIPTION_KEY_TOKEN"),
|
|
}
|
|
|
|
response = requests.get(
|
|
"https://api.digital.uefa.com/comp/v2/...",
|
|
headers=headers,
|
|
timeout=30
|
|
)
|
|
```
|
|
|
|
4. **Retry-Logik bei Fehlern**:
|
|
```python
|
|
import time
|
|
|
|
def safe_api_call(url, headers, retries=3):
|
|
for attempt in range(retries):
|
|
try:
|
|
response = requests.get(url, headers=headers, timeout=30)
|
|
if response.status_code == 429: # Rate limit
|
|
retry_after = int(response.headers.get('Retry-After', 60))
|
|
time.sleep(retry_after)
|
|
continue
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except requests.exceptions.RequestException as e:
|
|
if attempt == retries - 1:
|
|
raise
|
|
time.sleep(2 ** attempt) # Exponential backoff
|
|
```
|
|
|
|
5. **Daten-Mapping**:
|
|
| UEFA Feld | Django Model | Feld |
|
|
|-----------|--------------|------|
|
|
| `teamId` | Team | `external_id` |
|
|
| `teamName` | Team | `name` |
|
|
| `countryCode` | Team.countryObj | `shortname` |
|
|
| `coefficient` | Team | `coefficient` |
|
|
| `position` | Team | `position` |
|
|
|
|
## Beispiel: Teams importieren
|
|
|
|
```python
|
|
import requests
|
|
from scheduler.models import Team, Country
|
|
|
|
def import_teams(scenario, competition_id):
|
|
token = get_uefa_token()
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Ocp-Apim-Subscription-Key": os.getenv("UEFA_SUBSCRIPTION_KEY_TOKEN"),
|
|
}
|
|
|
|
url = f"https://api.digital.uefa.com/comp/v2/competitions/{competition_id}/teams"
|
|
data = safe_api_call(url, headers)
|
|
|
|
for team_data in data['teams']:
|
|
country = Country.objects.get(shortname=team_data['countryCode'])
|
|
|
|
team, created = Team.objects.update_or_create(
|
|
season=scenario.season,
|
|
external_id=team_data['teamId'],
|
|
defaults={
|
|
'name': team_data['teamName'],
|
|
'countryObj': country,
|
|
'coefficient': team_data.get('coefficient', 0),
|
|
'position': team_data.get('position', 0),
|
|
'active': True,
|
|
}
|
|
)
|
|
|
|
if created:
|
|
print(f"Neues Team: {team.name}")
|
|
```
|
|
|
|
## Beispiel: Clashes aktualisieren
|
|
|
|
```python
|
|
from qualifiers.models import QClash
|
|
|
|
def update_clashes(scenario, season_code):
|
|
token = get_uefa_token()
|
|
headers = {...}
|
|
|
|
url = f"https://api.digital.uefa.com/comp/v2/seasons/{season_code}/clashes"
|
|
data = safe_api_call(url, headers)
|
|
|
|
for clash_data in data['clashes']:
|
|
team1 = Team.objects.get(
|
|
season=scenario.season,
|
|
external_id=clash_data['team1Id']
|
|
)
|
|
team2 = Team.objects.get(
|
|
season=scenario.season,
|
|
external_id=clash_data['team2Id']
|
|
)
|
|
|
|
QClash.objects.update_or_create(
|
|
scenario=scenario,
|
|
team1=team1,
|
|
team2=team2,
|
|
defaults={
|
|
'type': clash_data.get('type', 0),
|
|
'active_q1': True,
|
|
'active_q2': True,
|
|
'active_q3': True,
|
|
'active_po': True,
|
|
}
|
|
)
|
|
```
|
|
|
|
## Caching (geplant)
|
|
|
|
```python
|
|
from django.core.cache import cache
|
|
|
|
def get_cached_api_data(key, url, headers, timeout=3600):
|
|
"""Cached API-Aufruf."""
|
|
cached = cache.get(key)
|
|
if cached:
|
|
return cached
|
|
|
|
data = safe_api_call(url, headers)
|
|
cache.set(key, data, timeout)
|
|
return data
|
|
```
|
|
|
|
## Referenz-Dateien
|
|
|
|
- [qualifiers/uefadigitalapi/uefa_api_2526.py](qualifiers/uefadigitalapi/uefa_api_2526.py)
|
|
- [qualifiers/uefadigitalapi/seed_2025.py](qualifiers/uefadigitalapi/seed_2025.py)
|
|
- [qualifiers/uefadigitalapi/update_clashes.py](qualifiers/uefadigitalapi/update_clashes.py)
|
|
- [docs/qualifiers/api/uefa-oauth2.md](docs/qualifiers/api/uefa-oauth2.md)
|