Merge pull request 'fix: update roster labels to use Minor League and Injured List (#59)' (#63) from ai/major-domo-v2-59 into next-release

Reviewed-on: #63
This commit is contained in:
cal 2026-03-07 03:26:15 +00:00
commit f048a3c04b
2 changed files with 202 additions and 153 deletions

View File

@ -1,6 +1,7 @@
""" """
Team roster commands for Discord Bot v2.0 Team roster commands for Discord Bot v2.0
""" """
from typing import Dict, Any, List from typing import Dict, Any, List
import discord import discord
@ -18,144 +19,173 @@ from views.embeds import EmbedTemplate, EmbedColors
class TeamRosterCommands(commands.Cog): class TeamRosterCommands(commands.Cog):
"""Team roster command handlers.""" """Team roster command handlers."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.TeamRosterCommands') self.logger = get_contextual_logger(f"{__name__}.TeamRosterCommands")
self.logger.info("TeamRosterCommands cog initialized") self.logger.info("TeamRosterCommands cog initialized")
@discord.app_commands.command(name="roster", description="Display team roster") @discord.app_commands.command(name="roster", description="Display team roster")
@discord.app_commands.describe( @discord.app_commands.describe(
abbrev="Team abbreviation (e.g., BSG, DEN, WV, etc.)", abbrev="Team abbreviation (e.g., BSG, DEN, WV, etc.)",
roster_type="Roster week: current or next (defaults to current)" roster_type="Roster week: current or next (defaults to current)",
)
@discord.app_commands.choices(
roster_type=[
discord.app_commands.Choice(name="Current Week", value="current"),
discord.app_commands.Choice(name="Next Week", value="next"),
]
) )
@discord.app_commands.choices(roster_type=[
discord.app_commands.Choice(name="Current Week", value="current"),
discord.app_commands.Choice(name="Next Week", value="next")
])
@requires_team() @requires_team()
@logged_command("/roster") @logged_command("/roster")
async def team_roster(self, interaction: discord.Interaction, abbrev: str, async def team_roster(
roster_type: str = "current"): self,
interaction: discord.Interaction,
abbrev: str,
roster_type: str = "current",
):
"""Display team roster with position breakdowns.""" """Display team roster with position breakdowns."""
await interaction.response.defer() await interaction.response.defer()
# Get team by abbreviation # Get team by abbreviation
team = await team_service.get_team_by_abbrev(abbrev, get_config().sba_season) team = await team_service.get_team_by_abbrev(abbrev, get_config().sba_season)
if team is None: if team is None:
self.logger.info("Team not found", team_abbrev=abbrev) self.logger.info("Team not found", team_abbrev=abbrev)
embed = EmbedTemplate.error( embed = EmbedTemplate.error(
title="Team Not Found", title="Team Not Found",
description=f"No team found with abbreviation '{abbrev.upper()}'" description=f"No team found with abbreviation '{abbrev.upper()}'",
) )
await interaction.followup.send(embed=embed) await interaction.followup.send(embed=embed)
return return
# Get roster data # Get roster data
roster_data = await team_service.get_team_roster(team.id, roster_type) roster_data = await team_service.get_team_roster(team.id, roster_type)
if not roster_data: if not roster_data:
embed = EmbedTemplate.error( embed = EmbedTemplate.error(
title="Roster Not Available", title="Roster Not Available",
description=f"No {roster_type} roster data available for {team.abbrev}" description=f"No {roster_type} roster data available for {team.abbrev}",
) )
await interaction.followup.send(embed=embed) await interaction.followup.send(embed=embed)
return return
# Create roster embeds # Create roster embeds
embeds = await self._create_roster_embeds(team, roster_data, roster_type) embeds = await self._create_roster_embeds(team, roster_data, roster_type)
# Send first embed and follow up with others if needed # Send first embed and follow up with others if needed
await interaction.followup.send(embed=embeds[0]) await interaction.followup.send(embed=embeds[0])
for embed in embeds[1:]: for embed in embeds[1:]:
await interaction.followup.send(embed=embed) await interaction.followup.send(embed=embed)
async def _create_roster_embeds(self, team: Team, roster_data: Dict[str, Any], async def _create_roster_embeds(
roster_type: str) -> List[discord.Embed]: self, team: Team, roster_data: Dict[str, Any], roster_type: str
) -> List[discord.Embed]:
"""Create embeds for team roster data.""" """Create embeds for team roster data."""
embeds = [] embeds = []
# Main roster embed # Main roster embed
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title=f"{team.abbrev} - {roster_type.title()} Week", title=f"{team.abbrev} - {roster_type.title()} Week",
description=f"{team.lname} Roster Breakdown", description=f"{team.lname} Roster Breakdown",
color=int(team.color, 16) if team.color else EmbedColors.PRIMARY color=int(team.color, 16) if team.color else EmbedColors.PRIMARY,
) )
# Position counts for active roster # Position counts for active roster
for key in ['active', 'longil', 'shortil']: roster_titles = {
if key in roster_data: "active": "Active Roster",
"longil": "Minor League",
"shortil": "Injured List",
}
for key in ["active", "longil", "shortil"]:
if key in roster_data:
this_roster = roster_data[key] this_roster = roster_data[key]
players = this_roster.get('players') embed.add_field(name=roster_titles[key], value="\u200b", inline=False)
if len(players) > 0:
this_team = players[0].get("team", {"id": "Unknown", "sname": "Unknown"})
embed.add_field( players = this_roster.get("players")
name='Team (ID)', if len(players) > 0:
value=f'{this_team.get("sname")} ({this_team.get("id")})', this_team = players[0].get(
inline=True "team", {"id": "Unknown", "sname": "Unknown"}
) )
embed.add_field( embed.add_field(
name='Player Count', name="Team (ID)",
value=f'{len(players)} Players' value=f'{this_team.get("sname")} ({this_team.get("id")})',
inline=True,
)
embed.add_field(
name="Player Count", value=f"{len(players)} Players"
) )
# Total WAR # Total WAR
total_war = this_roster.get('WARa', 0) total_war = this_roster.get("WARa", 0)
embed.add_field( embed.add_field(
name="Total sWAR", name="Total sWAR",
value=f"{total_war:.2f}" if isinstance(total_war, (int, float)) else str(total_war), value=(
inline=True f"{total_war:.2f}"
if isinstance(total_war, (int, float))
else str(total_war)
),
inline=True,
) )
embed.add_field( embed.add_field(
name='Position Counts', name="Position Counts",
value=self._position_code_block(this_roster), value=self._position_code_block(this_roster),
inline=False inline=False,
) )
embeds.append(embed) embeds.append(embed)
# Create detailed player list embeds if there are players # Create detailed player list embeds if there are players
for roster_name, roster_info in roster_data.items(): for roster_name, roster_info in roster_data.items():
if roster_name in ['active', 'longil', 'shortil'] and 'players' in roster_info: if (
players = sorted(roster_info['players'], key=lambda player: player.get('wara', 0), reverse=True) roster_name in ["active", "longil", "shortil"]
and "players" in roster_info
):
players = sorted(
roster_info["players"],
key=lambda player: player.get("wara", 0),
reverse=True,
)
if players: if players:
player_embed = self._create_player_list_embed( player_embed = self._create_player_list_embed(
team, roster_name, players team, roster_name, players
) )
embeds.append(player_embed) embeds.append(player_embed)
return embeds
def _position_code_block(self, roster_data: dict) -> str:
return f'```\n C 1B 2B 3B SS\n' \
f' {roster_data.get("C", 0)} {roster_data.get("1B", 0)} {roster_data.get("2B", 0)} ' \
f'{roster_data.get("3B", 0)} {roster_data.get("SS", 0)}\n\nLF CF RF SP RP\n' \
f' {roster_data.get("LF", 0)} {roster_data.get("CF", 0)} {roster_data.get("RF", 0)} ' \
f'{roster_data.get("SP", 0)} {roster_data.get("RP", 0)}\n```'
def _create_player_list_embed(self, team: Team, roster_name: str, return embeds
players: List[Dict[str, Any]]) -> discord.Embed:
def _position_code_block(self, roster_data: dict) -> str:
return (
f"```\n C 1B 2B 3B SS\n"
f' {roster_data.get("C", 0)} {roster_data.get("1B", 0)} {roster_data.get("2B", 0)} '
f'{roster_data.get("3B", 0)} {roster_data.get("SS", 0)}\n\nLF CF RF SP RP\n'
f' {roster_data.get("LF", 0)} {roster_data.get("CF", 0)} {roster_data.get("RF", 0)} '
f'{roster_data.get("SP", 0)} {roster_data.get("RP", 0)}\n```'
)
def _create_player_list_embed(
self, team: Team, roster_name: str, players: List[Dict[str, Any]]
) -> discord.Embed:
"""Create an embed with detailed player list.""" """Create an embed with detailed player list."""
roster_titles = { roster_titles = {
'active': 'Active Roster', "active": "Active Roster",
'longil': 'Minor League', "longil": "Minor League",
'shortil': 'Injured List' "shortil": "Injured List",
} }
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title=f"{team.abbrev} - {roster_titles.get(roster_name, roster_name.title())}", title=f"{team.abbrev} - {roster_titles.get(roster_name, roster_name.title())}",
color=int(team.color, 16) if team.color else EmbedColors.PRIMARY color=int(team.color, 16) if team.color else EmbedColors.PRIMARY,
) )
# Group players by position for better organization # Group players by position for better organization
batters = [] batters = []
pitchers = [] pitchers = []
for player in players: for player in players:
try: try:
this_player = Player.from_api_data(player) this_player = Player.from_api_data(player)
@ -166,8 +196,11 @@ class TeamRosterCommands(commands.Cog):
else: else:
batters.append(player_line) batters.append(player_line)
except Exception as e: except Exception as e:
self.logger.warning(f"Failed to create player from data: {e}", player_id=player.get('id')) self.logger.warning(
f"Failed to create player from data: {e}",
player_id=player.get("id"),
)
# Add player lists to embed # Add player lists to embed
if batters: if batters:
# Split long lists into multiple fields if needed # Split long lists into multiple fields if needed
@ -175,18 +208,18 @@ class TeamRosterCommands(commands.Cog):
for i, chunk in enumerate(batter_chunks): for i, chunk in enumerate(batter_chunks):
field_name = "Batters" if i == 0 else f"Batters (cont.)" field_name = "Batters" if i == 0 else f"Batters (cont.)"
embed.add_field(name=field_name, value="\n".join(chunk), inline=True) embed.add_field(name=field_name, value="\n".join(chunk), inline=True)
embed.add_field(name='', value='', inline=False) embed.add_field(name="", value="", inline=False)
if pitchers: if pitchers:
pitcher_chunks = self._chunk_list(pitchers, 16) pitcher_chunks = self._chunk_list(pitchers, 16)
for i, chunk in enumerate(pitcher_chunks): for i, chunk in enumerate(pitcher_chunks):
field_name = "Pitchers" if i == 0 else f"Pitchers (cont.)" field_name = "Pitchers" if i == 0 else f"Pitchers (cont.)"
embed.add_field(name=field_name, value="\n".join(chunk), inline=False) embed.add_field(name=field_name, value="\n".join(chunk), inline=False)
embed.set_footer(text=f"Total players: {len(players)}") embed.set_footer(text=f"Total players: {len(players)}")
return embed return embed
def _chunk_list(self, lst: List[str], chunk_size: int) -> List[List[str]]: def _chunk_list(self, lst: List[str], chunk_size: int) -> List[List[str]]:
"""Split a list into chunks of specified size.""" """Split a list into chunks of specified size."""
return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)] return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)]

View File

@ -3,6 +3,7 @@ Team service for Discord Bot v2.0
Handles team-related operations with roster management and league queries. Handles team-related operations with roster management and league queries.
""" """
import logging import logging
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
@ -12,13 +13,13 @@ from models.team import Team, RosterType
from exceptions import APIException from exceptions import APIException
from utils.decorators import cached_single_item from utils.decorators import cached_single_item
logger = logging.getLogger(f'{__name__}.TeamService') logger = logging.getLogger(f"{__name__}.TeamService")
class TeamService(BaseService[Team]): class TeamService(BaseService[Team]):
""" """
Service for team-related operations. Service for team-related operations.
Features: Features:
- Team retrieval by ID, abbreviation, and season - Team retrieval by ID, abbreviation, and season
- Manager-based team queries - Manager-based team queries
@ -27,12 +28,12 @@ class TeamService(BaseService[Team]):
- Season-specific team data - Season-specific team data
- Standings integration - Standings integration
""" """
def __init__(self): def __init__(self):
"""Initialize team service.""" """Initialize team service."""
super().__init__(Team, 'teams') super().__init__(Team, "teams")
logger.debug("TeamService initialized") logger.debug("TeamService initialized")
@cached_single_item(ttl=1800) # 30-minute cache @cached_single_item(ttl=1800) # 30-minute cache
async def get_team(self, team_id: int) -> Optional[Team]: async def get_team(self, team_id: int) -> Optional[Team]:
""" """
@ -57,12 +58,12 @@ class TeamService(BaseService[Team]):
except Exception as e: except Exception as e:
logger.error(f"Unexpected error getting team {team_id}: {e}") logger.error(f"Unexpected error getting team {team_id}: {e}")
return None return None
async def get_teams_by_owner( async def get_teams_by_owner(
self, self,
owner_id: int, owner_id: int,
season: Optional[int] = None, season: Optional[int] = None,
roster_type: Optional[str] = None roster_type: Optional[str] = None,
) -> List[Team]: ) -> List[Team]:
""" """
Get teams owned by a specific Discord user. Get teams owned by a specific Discord user.
@ -80,10 +81,7 @@ class TeamService(BaseService[Team]):
Allows caller to distinguish between "no teams" vs "error occurred" Allows caller to distinguish between "no teams" vs "error occurred"
""" """
season = season or get_config().sba_season season = season or get_config().sba_season
params = [ params = [("owner_id", str(owner_id)), ("season", str(season))]
('owner_id', str(owner_id)),
('season', str(season))
]
teams = await self.get_all_items(params=params) teams = await self.get_all_items(params=params)
@ -92,19 +90,27 @@ class TeamService(BaseService[Team]):
try: try:
target_type = RosterType(roster_type) target_type = RosterType(roster_type)
teams = [team for team in teams if team.roster_type() == target_type] teams = [team for team in teams if team.roster_type() == target_type]
logger.debug(f"Filtered to {len(teams)} {roster_type} teams for owner {owner_id}") logger.debug(
f"Filtered to {len(teams)} {roster_type} teams for owner {owner_id}"
)
except ValueError: except ValueError:
logger.warning(f"Invalid roster_type '{roster_type}' - returning all teams") logger.warning(
f"Invalid roster_type '{roster_type}' - returning all teams"
)
if teams: if teams:
logger.debug(f"Found {len(teams)} teams for owner {owner_id} in season {season}") logger.debug(
f"Found {len(teams)} teams for owner {owner_id} in season {season}"
)
return teams return teams
logger.debug(f"No teams found for owner {owner_id} in season {season}") logger.debug(f"No teams found for owner {owner_id} in season {season}")
return [] return []
@cached_single_item(ttl=1800) # 30-minute cache @cached_single_item(ttl=1800) # 30-minute cache
async def get_team_by_owner(self, owner_id: int, season: Optional[int] = None) -> Optional[Team]: async def get_team_by_owner(
self, owner_id: int, season: Optional[int] = None
) -> Optional[Team]:
""" """
Get the primary (Major League) team owned by a Discord user. Get the primary (Major League) team owned by a Discord user.
@ -124,125 +130,129 @@ class TeamService(BaseService[Team]):
Returns: Returns:
Team instance or None if not found Team instance or None if not found
""" """
teams = await self.get_teams_by_owner(owner_id, season, roster_type='ml') teams = await self.get_teams_by_owner(owner_id, season, roster_type="ml")
return teams[0] if teams else None return teams[0] if teams else None
async def get_team_by_abbrev(self, abbrev: str, season: Optional[int] = None) -> Optional[Team]: async def get_team_by_abbrev(
self, abbrev: str, season: Optional[int] = None
) -> Optional[Team]:
""" """
Get team by abbreviation for a specific season. Get team by abbreviation for a specific season.
Args: Args:
abbrev: Team abbreviation (e.g., 'NYY', 'BOS') abbrev: Team abbreviation (e.g., 'NYY', 'BOS')
season: Season number (defaults to current season) season: Season number (defaults to current season)
Returns: Returns:
Team instance or None if not found Team instance or None if not found
""" """
try: try:
season = season or get_config().sba_season season = season or get_config().sba_season
params = [ params = [("team_abbrev", abbrev.upper()), ("season", str(season))]
('team_abbrev', abbrev.upper()),
('season', str(season))
]
teams = await self.get_all_items(params=params) teams = await self.get_all_items(params=params)
if teams: if teams:
team = teams[0] # Should be unique per season team = teams[0] # Should be unique per season
logger.debug(f"Found team {abbrev} for season {season}: {team.lname}") logger.debug(f"Found team {abbrev} for season {season}: {team.lname}")
return team return team
logger.debug(f"No team found for abbreviation '{abbrev}' in season {season}") logger.debug(
f"No team found for abbreviation '{abbrev}' in season {season}"
)
return None return None
except Exception as e: except Exception as e:
logger.error(f"Error getting team by abbreviation '{abbrev}': {e}") logger.error(f"Error getting team by abbreviation '{abbrev}': {e}")
return None return None
async def get_teams_by_season(self, season: int) -> List[Team]: async def get_teams_by_season(self, season: int) -> List[Team]:
""" """
Get all teams for a specific season. Get all teams for a specific season.
Args: Args:
season: Season number season: Season number
Returns: Returns:
List of teams in the season List of teams in the season
""" """
try: try:
params = [('season', str(season))] params = [("season", str(season))]
teams = await self.get_all_items(params=params) teams = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(teams)} teams for season {season}") logger.debug(f"Retrieved {len(teams)} teams for season {season}")
return teams return teams
except Exception as e: except Exception as e:
logger.error(f"Failed to get teams for season {season}: {e}") logger.error(f"Failed to get teams for season {season}: {e}")
return [] return []
async def get_teams_by_manager(self, manager_id: int, season: Optional[int] = None) -> List[Team]: async def get_teams_by_manager(
self, manager_id: int, season: Optional[int] = None
) -> List[Team]:
""" """
Get teams managed by a specific manager. Get teams managed by a specific manager.
Uses 'manager_id' query parameter which supports multiple manager matching. Uses 'manager_id' query parameter which supports multiple manager matching.
Args: Args:
manager_id: Manager identifier manager_id: Manager identifier
season: Season number (optional) season: Season number (optional)
Returns: Returns:
List of teams managed by the manager List of teams managed by the manager
""" """
try: try:
params = [('manager_id', str(manager_id))] params = [("manager_id", str(manager_id))]
if season: if season:
params.append(('season', str(season))) params.append(("season", str(season)))
teams = await self.get_all_items(params=params) teams = await self.get_all_items(params=params)
logger.debug(f"Found {len(teams)} teams for manager {manager_id}") logger.debug(f"Found {len(teams)} teams for manager {manager_id}")
return teams return teams
except Exception as e: except Exception as e:
logger.error(f"Failed to get teams for manager {manager_id}: {e}") logger.error(f"Failed to get teams for manager {manager_id}: {e}")
return [] return []
async def get_teams_by_division(self, division_id: int, season: int) -> List[Team]: async def get_teams_by_division(self, division_id: int, season: int) -> List[Team]:
""" """
Get teams in a specific division for a season. Get teams in a specific division for a season.
Args: Args:
division_id: Division identifier division_id: Division identifier
season: Season number season: Season number
Returns: Returns:
List of teams in the division List of teams in the division
""" """
try: try:
params = [ params = [("division_id", str(division_id)), ("season", str(season))]
('division_id', str(division_id)),
('season', str(season))
]
teams = await self.get_all_items(params=params) teams = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(teams)} teams for division {division_id} in season {season}") logger.debug(
f"Retrieved {len(teams)} teams for division {division_id} in season {season}"
)
return teams return teams
except Exception as e: except Exception as e:
logger.error(f"Failed to get teams for division {division_id}: {e}") logger.error(f"Failed to get teams for division {division_id}: {e}")
return [] return []
async def get_team_roster(self, team_id: int, roster_type: str = 'current') -> Optional[Dict[str, Any]]: async def get_team_roster(
self, team_id: int, roster_type: str = "current"
) -> Optional[Dict[str, Any]]:
""" """
Get the roster for a team with position counts and player lists. Get the roster for a team with position counts and player lists.
Returns roster data with active, shortil (minor league), and longil (injured list) Returns roster data with active, shortil (injured list), and longil (minor league)
rosters. Each roster contains position counts and players sorted by descending WARa. rosters. Each roster contains position counts and players sorted by descending WARa.
Args: Args:
team_id: Team identifier team_id: Team identifier
roster_type: 'current' or 'next' roster roster_type: 'current' or 'next' roster
Returns: Returns:
Dictionary with roster structure: Dictionary with roster structure:
{ {
@ -257,19 +267,19 @@ class TeamService(BaseService[Team]):
""" """
try: try:
client = await self.get_client() client = await self.get_client()
data = await client.get(f'teams/{team_id}/roster/{roster_type}') data = await client.get(f"teams/{team_id}/roster/{roster_type}")
if data: if data:
logger.debug(f"Retrieved {roster_type} roster for team {team_id}") logger.debug(f"Retrieved {roster_type} roster for team {team_id}")
return data return data
logger.debug(f"No roster data found for team {team_id}") logger.debug(f"No roster data found for team {team_id}")
return None return None
except Exception as e: except Exception as e:
logger.error(f"Failed to get roster for team {team_id}: {e}") logger.error(f"Failed to get roster for team {team_id}: {e}")
return None return None
async def update_team(self, team_id: int, updates: dict) -> Optional[Team]: async def update_team(self, team_id: int, updates: dict) -> Optional[Team]:
""" """
Update team information. Update team information.
@ -287,52 +297,58 @@ class TeamService(BaseService[Team]):
except Exception as e: except Exception as e:
logger.error(f"Failed to update team {team_id}: {e}") logger.error(f"Failed to update team {team_id}: {e}")
return None return None
async def get_team_standings_position(self, team_id: int, season: int) -> Optional[dict]: async def get_team_standings_position(
self, team_id: int, season: int
) -> Optional[dict]:
""" """
Get team's standings information. Get team's standings information.
Calls /standings/team/{team_id} endpoint which returns a Standings object. Calls /standings/team/{team_id} endpoint which returns a Standings object.
Args: Args:
team_id: Team identifier team_id: Team identifier
season: Season number season: Season number
Returns: Returns:
Standings object data for the team Standings object data for the team
""" """
try: try:
client = await self.get_client() client = await self.get_client()
data = await client.get(f'standings/team/{team_id}', params=[('season', str(season))]) data = await client.get(
f"standings/team/{team_id}", params=[("season", str(season))]
)
if data: if data:
logger.debug(f"Retrieved standings for team {team_id}") logger.debug(f"Retrieved standings for team {team_id}")
return data return data
return None return None
except Exception as e: except Exception as e:
logger.error(f"Failed to get standings for team {team_id}: {e}") logger.error(f"Failed to get standings for team {team_id}: {e}")
return None return None
async def is_valid_team_abbrev(self, abbrev: str, season: Optional[int] = None) -> bool: async def is_valid_team_abbrev(
self, abbrev: str, season: Optional[int] = None
) -> bool:
""" """
Check if a team abbreviation is valid for a season. Check if a team abbreviation is valid for a season.
Args: Args:
abbrev: Team abbreviation to validate abbrev: Team abbreviation to validate
season: Season number (defaults to current) season: Season number (defaults to current)
Returns: Returns:
True if the abbreviation is valid True if the abbreviation is valid
""" """
team = await self.get_team_by_abbrev(abbrev, season) team = await self.get_team_by_abbrev(abbrev, season)
return team is not None return team is not None
async def get_current_season_teams(self) -> List[Team]: async def get_current_season_teams(self) -> List[Team]:
""" """
Get all teams for the current season. Get all teams for the current season.
Returns: Returns:
List of teams in current season List of teams in current season
""" """
@ -340,4 +356,4 @@ class TeamService(BaseService[Team]):
# Global service instance # Global service instance
team_service = TeamService() team_service = TeamService()