Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
523a2ca
ci: align workflow triggers with develop branch model (#85)
MarTrepodi May 9, 2026
8c0aa3a
ci(release): guard release workflow against non-main dispatch (#86)
MarTrepodi May 10, 2026
51235c1
chore(dependabot): set target branch to `develop`, limit open PRs to 10
MarTrepodi May 9, 2026
e8a6e05
fix(helpers): handle multi-day offsets in get_arena_payout
MarTrepodi May 9, 2026
c9e3f59
fix(helpers): ensure arena payout time adjusts correctly when shifted…
MarTrepodi May 10, 2026
75feb67
fix(tests): switch HMAC rejection tests from GET to POST endpoint (#89)
MarTrepodi May 10, 2026
df3a796
chore: merge main into develop to heal post-release squash divergence
MarTrepodi May 15, 2026
2b046cc
chore: merge main into develop to sync release pipeline and game data…
MarTrepodi Jun 3, 2026
4f5f662
chore(deps): bump actions/checkout from 6 to 7 (#103)
dependabot[bot] Jun 22, 2026
fdbc4ba
Merge branch 'main' into develop
MarTrepodi Jun 22, 2026
6811d51
chore(deps): bump actions/checkout from 6 to 7 (#105)
dependabot[bot] Jul 5, 2026
6c8e0b0
Merge branch 'main' into develop
MarTrepodi Jul 5, 2026
9362208
chore(typing): migrate type checker from mypy to ty (#107)
MarTrepodi Jul 6, 2026
45dce8a
feat(helpers): add localization dictionary parsing with sync and asyn…
MarTrepodi Jul 6, 2026
a502102
build(changelog): add git-changelog tooling and config
MarTrepodi Jul 12, 2026
559a8f1
ci(release): automate changelog generation and post-release backmerge
MarTrepodi Jul 12, 2026
2aa088f
docs(contributing): document automated changelog and release flow
MarTrepodi Jul 12, 2026
ffd8ac0
ci(ci): lint only PR-unique commits, exempting mergeback PRs
MarTrepodi Jul 12, 2026
d5a5c44
chore: merge main into develop (heal gitflow ancestry)
MarTrepodi Jul 12, 2026
fc1318d
chore: merge main into develop (heal gitflow ancestry) (#112)
MarTrepodi Jul 12, 2026
c98628c
docs(changelog): update for v2.2.0
github-actions[bot] Jul 12, 2026
defe8dc
Merge branch 'main' into release/v2.2.0
MarTrepodi Jul 12, 2026
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
31 changes: 31 additions & 0 deletions .github/workflows/backmerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: back-merge main into develop

on:
release:
types: [published]

permissions:
contents: write
pull-requests: write

jobs:
backmerge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
- name: Open main -> develop PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
branch="backmerge/main-to-develop-${{ github.run_id }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "${branch}"
git push origin "${branch}"
gh pr create --base develop --head "${branch}" \
--title "chore: back-merge main into develop" \
--body "Post-release sync of main (changelog + tag lineage) back into develop."
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ jobs:
- name: Install dependencies
run: uv sync --group typecheck

- name: Run mypy
run: uv run mypy src/swgoh_comlink/
- name: Run ty
run: uv run ty check src/swgoh_comlink/

# ── Test ───────────────────────────────────────────────────────────────
test:
Expand Down
22 changes: 21 additions & 1 deletion .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,24 @@ jobs:
EOF

- name: Validate commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
run: |
set -euo pipefail
git fetch --no-tags --quiet origin main
base="${{ github.event.pull_request.base.sha }}"
head="${{ github.event.pull_request.head.sha }}"
# Lint only the commits this PR actually introduces: exclude merge
# commits and any commit already reachable from main. Mergeback and
# automated back-merge PRs re-introduce main's (sometimes
# non-conventional squash) commits — those are already on main and are
# not this PR's responsibility to police.
commits="$(git rev-list --no-merges --reverse "${base}..${head}" --not FETCH_HEAD)"
if [ -z "${commits}" ]; then
echo "No PR-unique commits to lint (mergeback/back-merge or merge-only PR)."
exit 0
fi
rc=0
while read -r sha; do
echo "── ${sha} ──"
git log -1 --format=%B "${sha}" | npx commitlint --verbose || rc=1
done <<< "${commits}"
exit "${rc}"
45 changes: 16 additions & 29 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,7 @@ name: comlink-python release
# behind if the build or upload fails.

on:
workflow_dispatch:
inputs:
bump:
description: "Semantic version bump from the latest release tag"
type: choice
options:
- patch
- minor
- major
default: minor
workflow_dispatch: {}

permissions:
contents: read
Expand Down Expand Up @@ -56,32 +47,28 @@ jobs:
with:
python-version: "3.12"

- name: Compute next version from the latest tag
- name: Read release version from CHANGELOG.md
id: version
run: |
set -euo pipefail
latest="$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n1)"
latest="${latest:-v0.0.0}"
base="${latest#v}"
base="${base%%[-+]*}" # strip any pre-release / build metadata
IFS='.' read -r major minor patch <<< "$base"
case "${{ inputs.bump }}" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac
version="${major}.${minor}.${patch}"
tag="v${version}"
if git rev-parse "$tag" >/dev/null 2>&1; then
echo "::error::Tag $tag already exists — choose a different bump or delete the tag."
version="$(grep -m1 -oE '^## \[v?[0-9]+\.[0-9]+\.[0-9]+[^]]*\]' CHANGELOG.md \
| sed -E 's/^## \[(v?[0-9][^]]*)\]$/\1/')"
if [ -z "${version}" ]; then
echo "::error::Could not read a version heading from CHANGELOG.md."
exit 1
fi
tag="v${version#v}"
semver="${tag#v}"
if git rev-parse "${tag}" >/dev/null 2>&1; then
echo "::error::Tag ${tag} already exists — was CHANGELOG.md updated for a new version?"
exit 1
fi
{
echo "version=$version"
echo "tag=$tag"
echo "sha=$GITHUB_SHA"
echo "version=${semver}"
echo "tag=${tag}"
echo "sha=${GITHUB_SHA}"
} >> "$GITHUB_OUTPUT"
echo "Releasing $tag from $latest (bump=${{ inputs.bump }}) at $GITHUB_SHA"
echo "Releasing ${tag} (from CHANGELOG.md) at ${GITHUB_SHA}"

- name: Build the package
env:
Expand Down
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ repos:
exclude: ^examples/
- id: ruff-format

# ── Mypy type checking (mirrors CI type-check job) ────────────────────
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
# ── ty type checking (Astral, run via local uv) ───────────────────────
- repo: local
hooks:
- id: mypy
- id: ty
name: ty check
entry: uv run ty check
language: system
files: ^src/swgoh_comlink/
additional_dependencies: [httpx>=0.28]
args: [--strict, --ignore-missing-imports]
types: [python]

# ── Commitlint (mirrors CI commitlint job) ────────────────────────────
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
# CHANGELOG

<!-- insertion marker -->
<a name="v2.2.0"></a>

## [v2.2.0](https://github.com/swgoh-utils/comlink-python/compare/v2.1.0...v2.2.0) (2026-07-12)

### Features

- **helpers:** add localization dictionary parsing with sync and async support (#108) ([45dce8a](https://github.com/swgoh-utils/comlink-python/commit/45dce8a5f402ccb192b9598d308dd86a703ea43b))

### Bug Fixes

- **tests:** switch HMAC rejection tests from GET to POST endpoint (#89) ([75feb67](https://github.com/swgoh-utils/comlink-python/commit/75feb6701648cbaf986624bb90532b8fe24d443a))
- **helpers:** ensure arena payout time adjusts correctly when shifted to past ([c9e3f59](https://github.com/swgoh-utils/comlink-python/commit/c9e3f59b91a51137867f766f448d7deda07e90b0))
- **helpers:** handle multi-day offsets in get_arena_payout ([e8a6e05](https://github.com/swgoh-utils/comlink-python/commit/e8a6e05d997559c23734dda5fee2c44cd172d2e8))

<a name="v2.1.0"></a>

## [v2.1.0](https://github.com/swgoh-utils/comlink-python/compare/v2.0.7...v2.1.0) (2026-06-03)

### Bug Fixes

- **examples): update string quoting and rename params for localization bundle calls docs(helpers): document parse_swgoh_string and its extended tag grammar chore: add commitlint config and ignore .pythonrc.py fix(helpers:** extend parse_swgoh_string to cover full NGUI tag set (#83) ([1536853](https://github.com/swgoh-utils/comlink-python/commit/15368533fc09cbbf60964d3ca5044fc9e8c91499))

<a name="v2.0.7"></a>

## [v2.0.7](https://github.com/swgoh-utils/comlink-python/compare/v2.0.6...v2.0.7) (2026-03-30)

### Bug Fixes

- update `sanitize_url` to handle HTTPS URLs without ports, update tests for improved coverage ([577951a](https://github.com/swgoh-utils/comlink-python/commit/577951a6877f47a62c7bf062399fcca6611c9caf))


## [v2.0.6](https://github.com/swgoh-utils/comlink-python/releases/tag/v2.0.6) - 2026-03-29

Expand Down
12 changes: 9 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ This project follows standard Python conventions. Please keep these in mind:
**General principles:**

- Follow [PEP 8](https://peps.python.org/pep-0008/) for formatting
- Use **complete type annotations** on all functions, parameters, and return values — the project enforces `mypy --strict`
- Use **complete type annotations** on all functions, parameters, and return values — the project type-checks with `ty` (Astral)
- 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
- Keep lines to 120 characters max (the project doesn't enforce 79)
Expand Down Expand Up @@ -354,6 +354,12 @@ This project uses the [Angular commit convention](https://www.conventionalcommit

Other types (`test`, `style`, `ci`) are valid conventional commits but are **not included** in the generated changelog.

> **Changelog.** `CHANGELOG.md` is generated by `git-changelog` from Conventional Commit
> messages — do not edit it by hand. Preview locally with `uv run git-changelog --bumped-version`
> and `uv run git-changelog`. Releases: dispatch **prepare release** on `develop` (opens the
> release PR into `main`), then **comlink-python release** on `main` after that PR merges.
> A **back-merge main into develop** PR opens automatically after each release.

**Scope** is optional but encouraged. Common scopes:

- `core` — changes to the client classes (`swgoh_comlink.py`, `swgoh_comlink_async.py`, `_base.py`)
Expand Down Expand Up @@ -411,7 +417,7 @@ Closes #12
uvx ruff check src/ tests/

# Type check
uv run mypy src/swgoh_comlink/
uv run ty check src/swgoh_comlink/

# Tests
uv run pytest tests/ -v
Expand Down Expand Up @@ -449,7 +455,7 @@ Closes #12
- [ ] Commit messages follow Angular convention
- [ ] Ruff linter passes (`uvx ruff check src/ tests/`)
- [ ] Ruff formatter passes (`uvx ruff format --check src/ tests/`)
- [ ] Mypy strict passes (`uv run mypy src/swgoh_comlink/`)
- [ ] Type check passes (`uv run ty check src/swgoh_comlink/`)
- [ ] No unrelated changes bundled in

7. **After submitting:**
Expand Down
32 changes: 16 additions & 16 deletions examples/Async/get_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@
from swgoh_comlink import SwgohComlinkAsync


def convert_time(timestamp: Union[int, str]) -> str:
def convert_time(timestamp: int | float | str) -> str:
"""
Convert unix timestamp to human-readable string
:param timestamp: integer or string representing a unix timestamp value
:return: str
"""

if not (isinstance(timestamp, str) or isinstance(timestamp, int)):
return f'Invalid argument {timestamp}, type( {type(timestamp)} ). Expecting type str or int.'
if not (isinstance(timestamp, (int | float | str))):
return f"Invalid argument {timestamp}, type( {type(timestamp)} ). Expecting type str or int."

if len(str(timestamp)) > 10:
# timestamp is in milliseconds
timestamp = int(timestamp)
timestamp /= 1000

return datetime.fromtimestamp(int(timestamp), tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
return datetime.fromtimestamp(int(timestamp), tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")


async def main():
Expand All @@ -39,23 +39,23 @@ async def main():
list of all the available game events at that time.
"""

if 'gameEvent' not in events:
print(f'Unexpected response from game server, check your internet connection. {events}')
if "gameEvent" not in events:
print(f"Unexpected response from game server, check your internet connection. {events}")
return

# Loop through the events and print the ID, status relevant time entries for each occurrence
for event in events['gameEvent']:
for event in events["gameEvent"]:
print(f'{event["id"]=}, {event["status"]=}')
instances = event['instance']
instances = event["instance"]
for instance in instances:
display_start = convert_time(instance['displayStartTime'])
display_end = convert_time(instance['displayEndTime'])
start_time = convert_time(instance['startTime'])
reward_time = convert_time(instance['rewardTime'])
print(f'\t{display_start=}')
print(f'\t{display_end=}')
print(f'\t{start_time=}')
print(f'\t{reward_time=}')
display_start = convert_time(instance["displayStartTime"])
display_end = convert_time(instance["displayEndTime"])
start_time = convert_time(instance["startTime"])
reward_time = convert_time(instance["rewardTime"])
print(f"\t{display_start=}")
print(f"\t{display_end=}")
print(f"\t{start_time=}")
print(f"\t{reward_time=}")


asyncio.run(main())
33 changes: 16 additions & 17 deletions examples/Sync/get_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,26 @@
"""

from datetime import datetime, timezone
from typing import Union

from swgoh_comlink import SwgohComlink


def convert_time(timestamp: Union[int, str]) -> str:
def convert_time(timestamp: int | float | str) -> str:
"""
Convert unix timestamp to human-readable string
:param timestamp: integer or string representing a unix timestamp value
:return: str
"""

if not (isinstance(timestamp, str) or isinstance(timestamp, int)):
return f'Invalid argument {timestamp}, type( {type(timestamp)} ). Expecting type str or int.'
if not (isinstance(timestamp, (int | float | str))):
return f"Invalid argument {timestamp}, type( {type(timestamp)} ). Expecting type str or int."

if len(str(timestamp)) > 10:
# timestamp is in milliseconds
timestamp = int(timestamp)
timestamp /= 1000

return datetime.fromtimestamp(int(timestamp), tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
return datetime.fromtimestamp(int(timestamp), tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")


# Create instance of SwgohComlink using default localhost settings
Expand All @@ -38,20 +37,20 @@ def convert_time(timestamp: Union[int, str]) -> str:
list of all the available game events at that time.
"""

if 'gameEvent' not in events:
print(f'Unexpected response from game server, check your internet connection. {events}')
if "gameEvent" not in events:
print(f"Unexpected response from game server, check your internet connection. {events}")
exit(1)

# Loop through the events and print the ID, status relevant time entries for each occurrence
for event in events['gameEvent']:
for event in events["gameEvent"]:
print(f'{event["id"]=}, {event["status"]=}')
instances = event['instance']
instances = event["instance"]
for instance in instances:
display_start = convert_time(instance['displayStartTime'])
display_end = convert_time(instance['displayEndTime'])
start_time = convert_time(instance['startTime'])
reward_time = convert_time(instance['rewardTime'])
print(f'\t{display_start=}')
print(f'\t{display_end=}')
print(f'\t{start_time=}')
print(f'\t{reward_time=}')
display_start = convert_time(instance["displayStartTime"])
display_end = convert_time(instance["displayEndTime"])
start_time = convert_time(instance["startTime"])
reward_time = convert_time(instance["rewardTime"])
print(f"\t{display_start=}")
print(f"\t{display_end=}")
print(f"\t{start_time=}")
print(f"\t{reward_time=}")
Loading
Loading