"""Team transactions and previous players retrieval."""
from __future__ import annotations
from datetime import date
from collections import defaultdict
from bs4 import BeautifulSoup
from ..config import get_config
from ..countries import map_country_code
from ..fetcher import fetch_html # Uses connection pooling automatically
from ..exceptions import NetworkError
from ..utils import extract_text, extract_id_from_url, normalize_whitespace, extract_country_code
from .models import PlayerTransaction, PreviousPlayer
_config = get_config()
def _parse_transaction_date(date_str: str | None) -> date | None:
"""
Parse transaction date string into a date object.
Args:
date_str: Date string in format "YYYY/MM/DD" (e.g., "2025/10/02")
Returns:
date object or None if parsing fails or date is "Unknown"
"""
if not date_str or date_str.strip().lower() == "unknown":
return None
try:
# Parse YYYY/MM/DD format
from datetime import datetime
parsed = datetime.strptime(date_str.strip(), "%Y/%m/%d")
return parsed.date()
except (ValueError, AttributeError):
return None
[docs]
def transactions(team_id: int, timeout: float | None = None) -> list[PlayerTransaction]:
"""
Get all team transactions (joins, leaves, inactive status changes, etc.).
Retrieves the complete transaction history for a team from the VLR.gg transactions page.
Each transaction includes the date, action type, player information, position, and reference URL.
Args:
team_id: Team ID from VLR.gg (e.g., 1034 for NRG)
timeout: Request timeout in seconds (default: 5.0)
Returns:
List of PlayerTransaction objects, ordered by date (most recent first).
Returns empty list if team not found or has no transactions.
Raises:
NetworkError: If the request fails after retries
Example:
>>> import vlrdevapi as vlr
>>>
>>> # Get all transactions for NRG
>>> txns = vlr.teams.transactions(team_id=1034)
>>>
>>> # Display recent transactions
>>> for txn in txns[:5]:
... if txn.date:
... print(f"{txn.date.strftime('%Y/%m/%d')}: {txn.ign} - {txn.action} ({txn.position})")
... else:
... print(f"Unknown: {txn.ign} - {txn.action} ({txn.position})")
2025/10/02: FiNESSE - leave (Player)
2025/05/09: skuba - join (Player)
>>> # Filter by action type
>>> joins = [t for t in txns if t.action == "join"]
>>> leaves = [t for t in txns if t.action == "leave"]
Note:
Transaction actions include: 'join', 'leave', 'inactive', and others.
All text fields are cleaned of extra whitespace, tabs, and newlines.
"""
url = f"{_config.vlr_base}/team/transactions/{team_id}"
effective_timeout = timeout if timeout is not None else _config.default_timeout
try:
html = fetch_html(url, effective_timeout)
except NetworkError:
return []
soup = BeautifulSoup(html, "lxml")
# Find the transactions table
table = soup.select_one("table.wf-faux-table")
if not table:
return []
tbody = table.select_one("tbody")
if not tbody:
return []
transactions_list: list[PlayerTransaction] = []
# Process each transaction row
for row in tbody.select("tr.txn-item"):
# Extract date
date_td = row.select_one("td:nth-of-type(1)")
date_str = normalize_whitespace(extract_text(date_td)) if date_td else None
if date_str == "":
date_str = None
# Parse date
transaction_date = _parse_transaction_date(date_str)
# Extract action
action_td = row.select_one("td.txn-item-action")
action = normalize_whitespace(extract_text(action_td)) if action_td else None
if action == "":
action = None
# Extract country from flag using utility function
flag_td = row.select_one("td:nth-of-type(3)")
country_code = extract_country_code(flag_td)
country = map_country_code(country_code) if country_code else None
# Extract player info
player_td = row.select_one("td:nth-of-type(4)")
player_id = None
ign = None
real_name = None
if player_td:
# Get player link
player_link = player_td.select_one("a[href]")
if player_link:
href_val = player_link.get("href")
href = href_val if isinstance(href_val, str) else None
player_id = extract_id_from_url(href, "player")
ign = normalize_whitespace(extract_text(player_link))
if ign == "":
ign = None
# Get real name
real_name_el = player_td.select_one(".ge-text-light")
if real_name_el:
real_name = normalize_whitespace(extract_text(real_name_el))
if real_name == "":
real_name = None
# Extract position
position_td = row.select_one("td:nth-of-type(5)")
position = normalize_whitespace(extract_text(position_td)) if position_td else None
if position == "":
position = None
# Extract reference URL
reference_url = None
reference_td = row.select_one("td:nth-of-type(6)")
if reference_td:
ref_link = reference_td.select_one("a[href]")
if ref_link:
href_val = ref_link.get("href")
href = href_val if isinstance(href_val, str) else None
reference_url = normalize_whitespace(href or "")
if reference_url == "":
reference_url = None
transactions_list.append(PlayerTransaction(
date=transaction_date,
action=action,
player_id=player_id,
ign=ign,
real_name=real_name,
country=country,
position=position,
reference_url=reference_url,
))
return transactions_list
[docs]
def previous_players(team_id: int, timeout: float | None = None) -> list[PreviousPlayer]:
"""
Get all previous and current players with their status calculated from transaction history.
This function analyzes all team transactions to determine each player's current status,
join/leave dates, and complete transaction history. Players are grouped by their player_id
and their status is calculated based on their most recent transactions.
Status Determination Logic:
- **Active**: Player has joined and has no subsequent leave/inactive action
- **Left**: Player has a 'leave' action as their most recent status change
- **Inactive**: Player has an 'inactive' action as their most recent status change
- **Unknown**: Cannot determine status from available transactions
Args:
team_id: Team ID from VLR.gg (e.g., 1034 for NRG)
timeout: Request timeout in seconds (default: 5.0)
Returns:
List of PreviousPlayer objects, sorted by most recent activity (latest transaction first).
Each player includes:
- Basic info (IGN, real name, country, position)
- Calculated status
- Join and leave dates
- Complete transaction history
Returns empty list if team not found or has no transactions.
Raises:
NetworkError: If the request fails after retries
Example:
>>> import vlrdevapi as vlr
>>>
>>> # Get all players
>>> players = vlr.teams.previous_players(team_id=1034)
>>>
>>> # Display player status
>>> for player in players[:5]:
... print(f"{player.ign} - {player.status} ({player.position})")
... join_str = player.join_date.strftime('%Y/%m/%d') if player.join_date else 'Unknown'
... leave_str = player.leave_date.strftime('%Y/%m/%d') if player.leave_date else 'None'
... print(f" Joined: {join_str}, Left: {leave_str}")
mada - Active (Player)
Joined: 2024/10/10, Left: None
FiNESSE - Left (Player)
Joined: 2024/05/09, Left: 2025/10/02
>>> # Filter by status
>>> active = [p for p in players if p.status == "Active"]
>>> left = [p for p in players if p.status == "Left"]
>>>
>>> # Filter by position
>>> coaches = [p for p in players if p.position and "coach" in p.position.lower()]
>>>
>>> # Access transaction history
>>> player = players[0]
>>> for txn in player.transactions:
... print(f"{txn.date}: {txn.action}")
Note:
- Players without a player_id are excluded from results
- Transaction dates are date objects or None if not available
- Players can have multiple join/leave cycles (rejoining after leaving)
- Status is calculated from the most recent transaction
- All text fields are cleaned of extra whitespace
"""
txns = transactions(team_id, timeout)
# Group transactions by player
player_txns: dict[int, list[PlayerTransaction]] = defaultdict(list)
player_info: dict[int, dict[str, str | None]] = {}
for txn in txns:
if txn.player_id is None:
continue
player_txns[txn.player_id].append(txn)
# Store player info (use most recent non-None values)
if txn.player_id not in player_info:
player_info[txn.player_id] = {
'ign': txn.ign,
'real_name': txn.real_name,
'country': txn.country,
'position': txn.position,
}
else:
# Update with non-None values
if txn.ign:
player_info[txn.player_id]['ign'] = txn.ign
if txn.real_name:
player_info[txn.player_id]['real_name'] = txn.real_name
if txn.country:
player_info[txn.player_id]['country'] = txn.country
if txn.position:
player_info[txn.player_id]['position'] = txn.position
# Calculate status for each player
players: list[PreviousPlayer] = []
for player_id, txn_list in player_txns.items():
# Sort transactions by date (most recent first)
# Use a sentinel date for None values (far in the past)
from datetime import date as date_type
sentinel_date = date_type(1900, 1, 1)
sorted_txns = sorted(txn_list, key=lambda t: t.date or sentinel_date, reverse=True)
# Determine status based on transactions
status = "Unknown"
join_date = None
leave_date = None
# Separate transactions with known dates from unknown dates
txns_with_dates = [t for t in sorted_txns if t.date is not None]
_txns_without_dates = [t for t in sorted_txns if t.date is None]
# Find join and leave dates (most recent of each)
# Process chronologically to track the player's journey
for txn in reversed(sorted_txns): # Process chronologically (oldest first)
action_lower = (txn.action or "").lower()
if action_lower == "join":
# Update join date to the most recent join
join_date = txn.date
# Reset leave date when rejoining
leave_date = None
elif action_lower in ["leave", "inactive"]:
# Update leave date
leave_date = txn.date
# Determine status based on most recent action WITH A KNOWN DATE
# If we have transactions with dates, use those to determine status
if txns_with_dates:
most_recent_dated_action = (txns_with_dates[0].action or "").lower()
if most_recent_dated_action == "join":
status = "Active"
elif most_recent_dated_action == "leave":
status = "Left"
elif most_recent_dated_action == "inactive":
status = "Inactive"
else:
# Fallback based on dates
if leave_date and not join_date:
status = "Left"
elif join_date and not leave_date:
status = "Active"
else:
status = "Unknown"
else:
# All transactions have unknown dates, use the first action
most_recent_action = (sorted_txns[0].action or "").lower()
if most_recent_action == "join":
status = "Active"
elif most_recent_action == "leave":
status = "Left"
elif most_recent_action == "inactive":
status = "Inactive"
else:
status = "Unknown"
info = player_info.get(player_id, {'ign': None, 'real_name': None, 'country': None, 'position': None})
players.append(PreviousPlayer(
player_id=player_id,
ign=info['ign'],
real_name=info['real_name'],
country=info['country'],
position=info['position'],
status=status,
join_date=join_date,
leave_date=leave_date,
transactions=sorted_txns,
))
# Sort by most recent activity (based on latest transaction date)
from datetime import date as date_type
sentinel_date = date_type(1900, 1, 1)
players.sort(key=lambda p: p.transactions[0].date or sentinel_date, reverse=True)
return players