Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
- [ ] I have updated the docs (`README.md`, `docs/`, `examples/`) if user-facing behavior changed
- [ ] I have added unit tests that cover my changes (mocked, not requiring a live comlink service)
- [ ] Tests pass (`uv run pytest tests/`)
- [ ] Ruff linter and formatter pass (`uvx ruff check src/ tests/` and `uvx ruff format --check src/ tests/`)
- [ ] Ruff linter and formatter pass (`uv run ruff check src/ tests/` and `uv run ruff format --check src/ tests/`)
- [ ] Type check passes (`uv run ty check src/swgoh_comlink/`)
- [ ] I have not bundled unrelated changes in this PR

Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ jobs:
with:
python-version: "3.12"

- name: Install dependencies
run: uv sync --locked --only-group lint

- name: Run Ruff linter
run: uvx ruff check src/ tests/
# examples/ is excluded, matching the pre-commit ruff hook
run: uv run --no-sync ruff check src/ tests/ parity/

- name: Run Ruff formatter check
# NOTE: Set continue-on-error to false once the initial formatting PR is merged
continue-on-error: true
run: uvx ruff format --check src/ tests/
run: uv run --no-sync ruff format --check src/ tests/ examples/ parity/

# ── Type check ─────────────────────────────────────────────────────────
type-check:
Expand Down
15 changes: 12 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@ repos:
- id: debug-statements

# ── Ruff linter & formatter (mirrors CI lint job) ─────────────────────
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.4
# Run via local uv so the version comes from uv.lock, same as CI.
- repo: local
hooks:
- id: ruff
args: [--fix]
name: ruff check
entry: uv run ruff check --fix --force-exclude
language: system
exclude: ^examples/
types: [python]
require_serial: true
- id: ruff-format
name: ruff format
entry: uv run ruff format --force-exclude
language: system
types: [python]
require_serial: true

# ── ty type checking (Astral, run via local uv) ───────────────────────
- repo: local
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ This project follows standard Python conventions, enforced by [Ruff](https://doc

**General principles:**

- Follow [PEP 8](https://peps.python.org/pep-0008/) for formatting — `uvx ruff format` handles this for you
- Follow [PEP 8](https://peps.python.org/pep-0008/) for formatting — `uv run ruff format` handles this for you
- Use **complete type annotations** on all functions, parameters, and return values — the package ships a `py.typed` marker, so downstream users' type checkers rely on them
- Use `from __future__ import annotations` at the top of each module for modern annotation syntax
- Use docstrings (Google style) on all public classes and methods — they are rendered into the API reference
Expand Down Expand Up @@ -421,8 +421,8 @@ commit would be left out of the changelog entirely.

```bash
# Lint and format
uvx ruff check src/ tests/
uvx ruff format --check src/ tests/
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/

# Type check
uv run ty check src/swgoh_comlink/
Expand Down
2 changes: 2 additions & 0 deletions examples/Async/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ These examples use the `SwgohComlinkAsync` async client. All methods must be `aw
import asyncio
from swgoh_comlink import SwgohComlinkAsync


async def main():
async with SwgohComlinkAsync() as comlink:
player = await comlink.get_player(allycode=314927874)


asyncio.run(main())
```

Expand Down
16 changes: 14 additions & 2 deletions examples/Async/calc_stats_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,20 @@ async def main():
],
"relic": {"currentTier": 7},
"mods": [
{"set": 1, "level": 15, "pips": 6, "tier": 5, "stat": [(48, 8.5), (5, 12), (28, 1198), (41, 113), (49, 116)]},
{"set": 1, "level": 15, "pips": 6, "tier": 5, "stat": [(55, 24.0), (5, 7), (28, 839), (1, 1260), (41, 100)]},
{
"set": 1,
"level": 15,
"pips": 6,
"tier": 5,
"stat": [(48, 8.5), (5, 12), (28, 1198), (41, 113), (49, 116)],
},
{
"set": 1,
"level": 15,
"pips": 6,
"tier": 5,
"stat": [(55, 24.0), (5, 7), (28, 839), (1, 1260), (41, 100)],
},
{"set": 1, "level": 15, "pips": 6, "tier": 5, "stat": [(48, 8.5), (5, 10), (28, 1500), (53, 18), (1, 980)]},
{"set": 1, "level": 15, "pips": 6, "tier": 5, "stat": [(48, 8.5), (5, 15), (28, 900), (41, 80), (49, 95)]},
],
Expand Down
6 changes: 2 additions & 4 deletions examples/Async/concurrent_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ async def main():

# --- Concurrent approach using asyncio.gather ---
start = time.perf_counter()
concurrent_results = await asyncio.gather(
*[comlink.get_player(allycode=ac) for ac in ALLY_CODES]
)
concurrent_results = await asyncio.gather(*[comlink.get_player(allycode=ac) for ac in ALLY_CODES])
concurrent_time = time.perf_counter() - start
print(f"Concurrent fetch of {len(ALLY_CODES)} player(s): {concurrent_time:.2f}s")

Expand All @@ -41,7 +39,7 @@ async def main():
print(f" Player: {player['name']}, Guild: {player.get('guildName', 'N/A')}")

if len(ALLY_CODES) > 1:
speedup = sequential_time / concurrent_time if concurrent_time > 0 else float('inf')
speedup = sequential_time / concurrent_time if concurrent_time > 0 else float("inf")
print(f"\nSpeedup: {speedup:.1f}x faster with concurrent requests")


Expand Down
36 changes: 19 additions & 17 deletions examples/Async/get_gac_leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@


def format_output(lb_list: list):
""" Calculate the necessary column widths for formatted output based on data provided in tb_list """
"""Calculate the necessary column widths for formatted output based on data provided in tb_list"""
if not isinstance(lb_list, list):
return None
# First create a list of (name, guild, skill) tuples
output_list = []
max_name_len = 0
max_guild_len = 0
for player in lb_list:
if len(player['name']) > max_name_len:
max_name_len = len(player['name'])
if len(player['guild']['name']) > max_guild_len:
max_guild_len = len(player['guild']['name'])
output_list.append((player['name'],
player['guild']['name'],
player['playerRating']['playerSkillRating']['skillRating'])
)
return max_name_len+2, max_guild_len+2, output_list
if len(player["name"]) > max_name_len:
max_name_len = len(player["name"])
if len(player["guild"]["name"]) > max_guild_len:
max_guild_len = len(player["guild"]["name"])
output_list.append(
(player["name"], player["guild"]["name"], player["playerRating"]["playerSkillRating"]["skillRating"])
)
return max_name_len + 2, max_guild_len + 2, output_list


async def main():
Expand Down Expand Up @@ -59,14 +58,17 @@ async def main():
gac_kyber_2_lb = await comlink.get_gac_leaderboard(leaderboard_type=6, league=100, division=20)

# Output the GAC leaderboard information
name_col_width, guild_col_width, lb_entries = format_output(gac_kyber_2_lb['player'])
name_col_width, guild_col_width, lb_entries = format_output(gac_kyber_2_lb["player"])
for player in lb_entries:
print("Player nane: {name:{name_width}} Guild: {g_name:{g_width}} Skill Rating: {rating}".format(
name=player[0],
name_width=name_col_width,
g_name=player[1],
g_width=guild_col_width,
rating=player[2]))
print(
"Player nane: {name:{name_width}} Guild: {g_name:{g_width}} Skill Rating: {rating}".format(
name=player[0],
name_width=name_col_width,
g_name=player[1],
g_width=guild_col_width,
rating=player[2],
)
)


asyncio.run(main())
4 changes: 2 additions & 2 deletions examples/Async/get_guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async def main():
guild_id from the response to call the get_guild() method.
"""
player = await comlink.get_player(314927874)
guild = await comlink.get_guild(player['guildId'])
guild = await comlink.get_guild(player["guildId"])

"""
The get_guild() method returns a dictionary containing information about the guild, its members and event
Expand All @@ -26,7 +26,7 @@ async def main():
should be set to True.
"""

guild_with_stats = await comlink.get_guild(player['guildId'], include_recent_guild_activity_info=True)
guild_with_stats = await comlink.get_guild(player["guildId"], include_recent_guild_activity_info=True)

"""
>>> pprint(guild['recentTerritoryWarResult'])
Expand Down
49 changes: 24 additions & 25 deletions examples/Async/get_guild_leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,26 @@


def get_max_guild_name_length(guild_list: list) -> int:
max_length = max(len(leader_guild['name']) for leader_guild in guild_list)
max_length = max(len(leader_guild["name"]) for leader_guild in guild_list)
return max_length


def print_table(leaderboard: list,
table_title: str,
guild_name_col_title: str,
score_col_title: str,
rank_col_title: str) -> None:
def print_table(
leaderboard: list, table_title: str, guild_name_col_title: str, score_col_title: str, rank_col_title: str
) -> None:
guild_name_col_width = get_max_guild_name_length(leaderboard)
total_width = guild_name_col_width + len(score_col_title) + len(rank_col_title) + 7
print()
print(f"{table_title: ^{total_width}}")
print('=' * total_width)
print("=" * total_width)
print(f"{guild_name_col_title:^{guild_name_col_width}} | {score_col_title} | Rank")
print('-' * total_width)
print("-" * total_width)
for leaderboard_entry in leaderboard:
score = f"{int(leaderboard_entry['leaderboardScore']):,}"
print(
f"{leaderboard_entry['name']:{guild_name_col_width}} | {score:^{len(score_col_title)}} | " +
f"{leaderboard_entry['rank']:^{len(rank_col_title)}}")
f"{leaderboard_entry['name']:{guild_name_col_width}} | {score:^{len(score_col_title)}} | "
+ f"{leaderboard_entry['rank']:^{len(rank_col_title)}}"
)


async def main():
Expand All @@ -51,30 +50,30 @@ async def main():
tw_top_5 = await comlink.get_guild_leaderboard(leaderboard_id=tw_leaderboard_id, count=5)

# Print the top 5 TW leaderboard information
print_table(leaderboard=tw_top_5['leaderboard'][0]['guild'],
table_title="Top 5 TW Leaderboard for current month",
guild_name_col_title="Guild Name",
score_col_title="Total GP Defeated",
rank_col_title="Rank")
print_table(
leaderboard=tw_top_5["leaderboard"][0]["guild"],
table_title="Top 5 TW Leaderboard for current month",
guild_name_col_title="Guild Name",
score_col_title="Total GP Defeated",
rank_col_title="Rank",
)

# Get the top 5 Speeder Bike raid leaderboard for the past month
speederbike_raid_leaderboard_id = [
{
"leaderboardType": 6,
"defId": "GUILD:RAIDS:NORMAL_DIFF:ROTJ:SPEEDERBIKE",
"monthOffset": 1
}
{"leaderboardType": 6, "defId": "GUILD:RAIDS:NORMAL_DIFF:ROTJ:SPEEDERBIKE", "monthOffset": 1}
]
speederbike_raid_top_5 = await comlink.get_guild_leaderboard(
leaderboard_id=speederbike_raid_leaderboard_id, count=5
)

# Print the top 5 raid information
print_table(leaderboard=speederbike_raid_top_5['leaderboard'][0]['guild'],
table_title="Top 5 Speeder bike raid Leaderboard for past month",
guild_name_col_title="Guild Name",
score_col_title="Total Raid points achieved",
rank_col_title="Rank")
print_table(
leaderboard=speederbike_raid_top_5["leaderboard"][0]["guild"],
table_title="Top 5 Speeder bike raid Leaderboard for past month",
guild_name_col_title="Guild Name",
score_col_title="Total Raid points achieved",
rank_col_title="Rank",
)


asyncio.run(main())
8 changes: 4 additions & 4 deletions examples/Async/get_language_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async def main():
german_language_bundle = await cl.get_localization(locale="ger_de")

# First decode the base64 string into the compressed object
german_language_bundle_decoded = base64.b64decode(german_language_bundle['localizationBundle'])
german_language_bundle_decoded = base64.b64decode(german_language_bundle["localizationBundle"])

# Next create a ZipFile handler to interact with the compressed data object
zip_obj = zipfile.ZipFile(io.BytesIO(german_language_bundle_decoded))
Expand All @@ -34,16 +34,16 @@ async def main():
"""

# Extract the string information, decode it and split into individual lines
der_obj = zip_obj.read('Loc_GER_DE.txt')
der_obj_decoded = der_obj.decode('utf-8')
der_obj = zip_obj.read("Loc_GER_DE.txt")
der_obj_decoded = der_obj.decode("utf-8")
der_obj_lines = der_obj_decoded.splitlines()

# Each line has a key value and the corresponding text separated by a pipe ('|')
# To create a dictionary for easy mapping of strings from IDs used in game data collections,
# loop through the lines, split on the '|' character and store the information in an empty dict
der_str_lookup = {}
for line in der_obj_lines:
key, value = line.split('|')
key, value = line.split("|")
der_str_lookup[key] = value


Expand Down
4 changes: 2 additions & 2 deletions examples/Async/get_latest_game_data_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ async def main():
"""
current_game_version = await comlink.get_latest_game_data_version()

print(f'{current_game_version}')
print(f"{current_game_version}")
# For those that are averse to typing long descriptive function names, there is an alias that can be used
# which is much shorter, getVersion()

current_versions = await comlink.getVersion()
print(f'{current_versions}')
print(f"{current_versions}")


asyncio.run(main())
2 changes: 1 addition & 1 deletion examples/Async/get_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def main():
player_profile_data = await comlink.get_player(allycode=314927874)

# We can extract a list of the characters in the player's roster
player_roster = player_profile_data['rosterUnit']
player_roster = player_profile_data["rosterUnit"]

"""
# Each unit in a player's roster is a dictionary with entries for all attributes including skills,
Expand Down
12 changes: 5 additions & 7 deletions examples/Async/search_for_guilds.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ async def main():
"""

# Search for a guild by name
guilds_by_name = await comlink.get_guilds_by_name(name='guild')
guilds_by_name = await comlink.get_guilds_by_name(name="guild")
# Print the names of the maximum 10 guilds matched
for guild in guilds_by_name['guild']:
for guild in guilds_by_name["guild"]:
print(f'{guild["name"]=}')

"""
Expand All @@ -52,15 +52,13 @@ async def main():
"includeInviteOnly": True,
"minGuildGalacticPower": 500000000,
"maxGuildGalacticPower": 600000000,
"recentTbParticipatedIn": []
"recentTbParticipatedIn": [],
}

guilds_by_criteria = await comlink.get_guilds_by_criteria(
search_criteria=guild_search_criteria, count=1000
)
guilds_by_criteria = await comlink.get_guilds_by_criteria(search_criteria=guild_search_criteria, count=1000)
# Check the actual number of guilds over 500m GP with 48 players minimum to compare against the max
# count of 1000
print(len(guilds_by_criteria['guild']))
print(len(guilds_by_criteria["guild"]))


asyncio.run(main())
Loading
Loading