Metadata-Version: 2.5
Name: swgoh_comlink
Version: 2.4.0
Summary: Python 3 interface library for swgoh-comlink (https://github.com/swgoh-utils/swgoh-comlink)
Project-URL: Homepage, https://github.com/swgoh-utils/comlink-python
Project-URL: Bug Tracker, https://github.com/swgoh-utils/comlink-python/issues
Author-email: Mar Trepodi <martrepodi@gmail.com>
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28
Requires-Dist: typing-extensions>=4.15.0
Description-Content-Type: text/markdown

# comlink-python

[![CI](https://github.com/swgoh-utils/comlink-python/actions/workflows/ci.yml/badge.svg)](https://github.com/swgoh-utils/comlink-python/actions/workflows/ci.yml)
[![PyPI version](https://badge.fury.io/py/swgoh-comlink.svg)](https://pypi.org/project/swgoh-comlink/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Coverage](https://img.shields.io/badge/coverage-97%25-brightgreen.svg)](https://github.com/swgoh-utils/comlink-python)

## Description

A python wrapper for the [swgoh-comlink](https://github.com/swgoh-utils/swgoh-comlink) tool, plus an in-package local stat calculator (`StatCalc`).

Supports both **synchronous** and **asynchronous** usage via `SwgohComlink` and `SwgohComlinkAsync`.

**Requires Python 3.10 or higher.**

## Installation

Install from [PyPi package repository](https://pypi.org/project/swgoh-comlink/) using the following shell command.

```bash
uv pip install swgoh_comlink
```

## Synchronous Usage

Basic default usage:

```python
from swgoh_comlink import SwgohComlink

comlink = SwgohComlink()
player_data = comlink.get_player(allycode=245866537)
player_name = player_data['name']
guild_id = player_data['guildId']
guild = comlink.get_guild(guild_id=guild_id)
guild_name = guild['profile']['name']
```

With a custom comlink URL:

```python
from swgoh_comlink import SwgohComlink

comlink = SwgohComlink(url='http://localhost:3500')
player_data = comlink.get_player(allycode=245866537)
```

With an external swgoh-stats service:

```python
from swgoh_comlink import SwgohComlink

comlink = SwgohComlink(url='http://localhost:3500', stats_url='http://localhost:3550')
player_data = comlink.get_player(allycode=245866537)
player_roster = player_data['rosterUnit']
roster_with_stats = comlink.get_unit_stats(player_roster)
```

With HMAC authentication:

```python
from swgoh_comlink import SwgohComlink

comlink = SwgohComlink(
    url='http://localhost:3000',
    access_key='public_key',
    secret_key='this_string_should_be_secret'
)
player_data = comlink.get_player(allycode=245866537)
```

As a context manager (auto-closes connections):

```python
from swgoh_comlink import SwgohComlink

with SwgohComlink() as comlink:
    player_data = comlink.get_player(allycode=245866537)
```

## Async Usage

The `SwgohComlinkAsync` class provides the same API with `async`/`await` support. All public methods have identical signatures to the sync client.

```python
from swgoh_comlink import SwgohComlinkAsync

async with SwgohComlinkAsync() as comlink:
    player_data = await comlink.get_player(allycode=245866537)
    player_name = player_data['name']
    guild_id = player_data['guildId']
    guild = await comlink.get_guild(guild_id=guild_id)
    guild_name = guild['profile']['name']
```

Without a context manager, call `aclose()` when done:

```python
comlink = SwgohComlinkAsync()
try:
    player_data = await comlink.get_player(allycode=245866537)
finally:
    await comlink.aclose()
```

Both clients accept the same constructor parameters and support connection pooling via persistent `httpx` clients.

## StatCalc / StatCalcAsync (Local Stat Calculator)

`StatCalc` and `StatCalcAsync` calculate unit stats and Galactic Power locally without requiring an external swgoh-stats service. `StatCalc` fetches game data synchronously on initialization; `StatCalcAsync` provides an async factory method (`await StatCalcAsync.create()`) for non-blocking initialization. Both accept pre-loaded data for offline use.

### Building game data from Comlink

Instead of fetching a static `gameData.json` from GitHub, you can build game data
dynamically from a running Comlink service using `GameDataBuilder` / `GameDataBuilderAsync`:

```python
from swgoh_comlink import SwgohComlink, StatCalc, GameDataBuilder

comlink = SwgohComlink()
game_data = GameDataBuilder(comlink).build()
calc = StatCalc(game_data=game_data)
```

Async:

```python
from swgoh_comlink import SwgohComlinkAsync, StatCalcAsync, GameDataBuilderAsync

async with SwgohComlinkAsync() as comlink:
    game_data = await GameDataBuilderAsync(comlink).build()
    calc = StatCalcAsync(game_data=game_data)
```

The builder fetches only the required game data collections in a single API call
and transforms them into the format `StatCalc` expects.

### Basic usage

```python
from swgoh_comlink import SwgohComlink, StatCalc

comlink = SwgohComlink()
calc = StatCalc()  # fetches latest game data from GitHub on init

# Calculate stats for a full player roster
player = comlink.get_player(allycode=245866537)
calc.calc_roster_stats(player['rosterUnit'])

# Or calculate stats for a single character
unit = {
    "defId": "BOSSK",
    "rarity": 7,
    "level": 85,
    "gear": 13,
    "equipped": [],
    "skills": [],
}
calc.calc_char_stats(unit)
print(unit["stats"])  # final stats, mods, and GP added in-place
print(unit["gp"])     # galactic power
```

### Offline / pre-loaded game data

```python
calc = StatCalc(game_data=my_game_data_dict)
```

### Async usage (`StatCalcAsync`)

`StatCalcAsync` provides an async factory method for fetching game data without blocking the event loop. All calculation methods are inherited from `StatCalc` and remain synchronous (they are pure computation).

```python
from swgoh_comlink import SwgohComlinkAsync, StatCalcAsync

async with SwgohComlinkAsync() as comlink:
    calc = await StatCalcAsync.create()  # fetches game data from GitHub async

    player = await comlink.get_player(allycode=245866537)
    calc.calc_roster_stats(player['rosterUnit'])
```

With pre-loaded game data (no async fetch needed):

```python
calc = StatCalcAsync(game_data=my_game_data_dict)
```

### StatCalc / StatCalcAsync Methods

| Method | Description |
|--------|-------------|
| `calc_char_stats(unit)` | Calculate stats and GP for a single character (modifies in-place) |
| `calc_ship_stats(unit, crew)` | Calculate stats and GP for a ship with its crew |
| `calc_roster_stats(units)` | Calculate stats for all units in a roster (list or dict) |
| `calc_player_stats(players)` | Calculate stats for one or more full player payloads |
| `calc_char_gp(char)` | Calculate GP for a character |
| `calc_ship_gp(ship, crew)` | Calculate GP for a ship |
| `set_game_data(game_data)` | Replace the game data used by the calculator |

## Parameters

Constructor parameters for `SwgohComlink` and `SwgohComlinkAsync`:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `url` | `str` | `http://localhost:3000` | URL where swgoh-comlink is running |
| `stats_url` | `str` | `http://localhost:3223` | URL where swgoh-stats service is running |
| `access_key` | `str` | `None` | HMAC public key. Also reads from `ACCESS_KEY` env var |
| `secret_key` | `str` | `None` | HMAC private key. Also reads from `SECRET_KEY` env var |
| `host` | `str` | `None` | Server hostname (overrides `url` and `stats_url`) |
| `port` | `int` | `3000` | Comlink TCP port (used with `host`) |
| `stats_port` | `int` | `3223` | Stats service TCP port (used with `host`) |
| `verify_ssl` | `bool` | `True` | Enable TLS certificate verification |
| `version_cache_ttl` | `float` | `3600` | Seconds to cache game/localization versions from `/metadata` (see below) |

### Version caching

Calls to `/data` and `/localization` require the current `latestGamedataVersion` /
`latestLocalizationBundleVersion` values from the `/metadata` endpoint. When you call
`get_game_data()`, `get_localization()`, or `get_latest_game_data_version()` without an explicit
version, the client fetches these values once and caches them on the instance for
`version_cache_ttl` seconds (default: 1 hour), instead of making a fresh `/metadata` request every
time.

- Pass `version_cache_ttl=0` to disable caching (the pre-v2.3 behavior), or `math.inf` to cache
  for the client's lifetime.
- Explicit `version=` / `localization_id=` arguments always bypass the cache.
- Call `invalidate_version_cache()` or `get_latest_game_data_version(refresh=True)` to force a
  re-fetch, e.g. right after a game update lands.
- A successful `get_game_metadata()` call also refreshes the cache opportunistically.

### Enums caching

`get_enums()` caches its response on the instance as `enums`, together with the game data version
it was fetched under as `enums_version`. Later calls return the cached response while that version
is unchanged, and fetch the enums again once it changes. The version check goes through the version
cache above, so it usually makes no request.

- `get_enums(refresh=True)` fetches the version and the enums again.
- When the version cannot be determined (for example `/metadata` fails), the enums are fetched and
  returned without being cached.
- The cached response is shared: copy it before changing it.

## Available Methods

Methods available on both `SwgohComlink` and `SwgohComlinkAsync` (async methods use `await`):

| Method | Description |
|--------|-------------|
| `get_player(allycode, player_id, enums)` | Get player data by allycode or player ID |
| `get_player_arena(allycode, player_id, player_details_only, enums)` | Get player arena profile |
| `get_guild(guild_id, include_recent_guild_activity_info, enums)` | Get guild data by guild ID |
| `get_guilds_by_name(name, start_index, count, enums)` | Search guilds by name |
| `get_guilds_by_criteria(search_criteria, start_index, count, enums)` | Search guilds by criteria |
| `get_game_data(version, include_pve_units, request_segment, enums, items, device_platform)` | Get game data collections (select them with `items=DataItems...`) |
| `get_game_metadata(client_specs, enums)` | Get current game and localization versions |
| `get_localization(localization_id, locale, unzip, enums)` | Get localization bundles |
| `get_enums(refresh)` | Get game data enums (cached per game data version) |
| `get_events(enums)` | Get current game events |
| `get_leaderboard(leaderboard_type, league, division, ...)` | Get GAC leaderboard data |
| `get_guild_leaderboard(leaderboard_id, count, enums)` | Get guild leaderboard data |
| `get_unit_stats(request_payload, flags, language)` | Calculate unit stats via swgoh-stats |
| `get_latest_game_data_version(refresh)` | Get latest game data and language versions (cached) |
| `invalidate_version_cache()` | Discard cached versions so the next lookup re-fetches `/metadata` |
| `get_name_spaces(only_compatible, enums)` | Get available namespaces |
| `get_segmented_content(content_name_space, accept_language, enums)` | Retrieve segmented content |

## Examples

The [`examples/`](examples/) directory contains runnable scripts for common tasks using both the sync and async clients.

### Synchronous ([`examples/Sync/`](examples/Sync/))

| Script | Description |
|--------|-------------|
| [`get_player.py`](examples/Sync/get_player.py) | Get a player's full profile including roster |
| [`get_guild.py`](examples/Sync/get_guild.py) | Look up guild info from a player's ally code |
| [`get_events.py`](examples/Sync/get_events.py) | Retrieve current game events with timestamps |
| [`get_game_data.py`](examples/Sync/get_game_data.py) | Retrieve game data collections |
| [`get_gac_leaderboard.py`](examples/Sync/get_gac_leaderboard.py) | Get GAC leaderboard data |
| [`get_gac_brackets.py`](examples/Sync/get_gac_brackets.py) | Scan GAC brackets and search for a player |
| [`calc_stats_local.py`](examples/Sync/calc_stats_local.py) | Calculate character stats locally with `StatCalc` |
| [`search_for_guilds.py`](examples/Sync/search_for_guilds.py) | Search for guilds by name or criteria |

### Asynchronous ([`examples/Async/`](examples/Async/))

| Script | Description |
|--------|-------------|
| [`get_player.py`](examples/Async/get_player.py) | Get a player's full profile using `await` |
| [`concurrent_requests.py`](examples/Async/concurrent_requests.py) | Fetch multiple players in parallel with `asyncio.gather()` |
| [`streaming_guild_roster.py`](examples/Async/streaming_guild_roster.py) | Fetch all guild members concurrently with `asyncio.TaskGroup` |
| [`streaming_guild_roster_advanced.py`](examples/Async/streaming_guild_roster_advanced.py) | Production-quality guild roster fetch with CLI args and retry logic |
| [`calc_stats_local.py`](examples/Async/calc_stats_local.py) | Calculate character stats locally with `StatCalcAsync` |

See the [full examples listing](examples/README.md) for all available scripts and usage details.

## Logging

Logging is handled by the [python logging module](https://docs.python.org/3/library/logging.html). The library follows Python best practice by attaching only a `NullHandler` — no output is produced unless your application configures logging.

For details on enabling and customizing log output, see [docs/logging.md](docs/logging.md).

## Migrating from v1.x

This release replaces `requests` with `httpx` and changes how logging is configured. If you are upgrading from a previous version, see the [Migration Guide](docs/migration.md) for a walkthrough of required changes:

- Replace `requests` with `httpx` in your dependencies
- Update `except requests.RequestException` to `except httpx.RequestError` (or catch `SwgohComlinkException`)
- Update any `get_logger(log_level=...)` calls — configure logging via the standard `logging` module instead
- Use a context manager (`with SwgohComlink() ...`) or call `close()` to release connections

## Test Configuration

Integration tests are opt-in and require a running comlink service:

```bash
# Run integration tests (requires comlink on localhost:3000)
RUN_INTEGRATION_TESTS=1 uv run pytest tests/integration/ -v

# Run with HMAC tests (requires comlink-hmac on localhost:3001)
HMAC_ACCESS_KEY=your_key HMAC_SECRET_KEY=your_secret \
  RUN_INTEGRATION_TESTS=1 uv run pytest tests/integration/ -v
```

HMAC tests are automatically skipped when `HMAC_ACCESS_KEY` and `HMAC_SECRET_KEY` environment variables are not set.

See the online [wiki](https://github.com/swgoh-utils/swgoh-comlink/wiki) for more information.

## Support

Issues can be reported in [GitHub](https://github.com/swgoh-utils/comlink-python/issues).

Join the [discord server](https://discord.gg/6PBfG5MzR3)
