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
1 change: 1 addition & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/themes
https://old.reddit.com/r/rust
https://zubanls.com/pricing/
3 changes: 2 additions & 1 deletion config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ default_language = "en"
title = "Rob's Blog | Python • Rust • Ramblings?"

# Whether to automatically compile all Sass files in the sass directory
compile_sass = false
compile_sass = true

# Whether to build a search index to be used later on by a JavaScript library
build_search_index = false
Expand All @@ -29,6 +29,7 @@ after_dark_menu = [
{ name = "Home", url = "$BASE_URL" },
{ name = "About", url = "$BASE_URL/about/" },
{ name = "Now", url = "$BASE_URL/now/" },
{ name = "Boardgames", url = "$BASE_URL/boardgames" },
{ name = "TILs", url = "$BASE_URL/tils/" },
{ name = "Tags", url = "$BASE_URL/tags" },
{ name = "Source", url = "https://github.com/sinon/sinon.github.io" },
Expand Down
6 changes: 3 additions & 3 deletions content/future-python-type-checkers.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Before examining these new Rust-based tools, it's worth understanding the curren

- Created by the author of the popular Python LSP tool `jedi`
- Aims for high-degree of compatibility with `mypy` to make adoption in large existing codebases seamless.
- Not FOSS[^2], will require a license for codebases above 1.5 MB (~50,000 lines of code)[^3].
- ~~Not FOSS[^2], will require a license for codebases above 1.5 MB (~50,000 lines of code)[^3]~~**Update (September 2025):** Now open source under the AGPL license, though commercial licensing is available for business who prefer to avoid AGPL compliance.
- Currently maintained by a single author seems a potential risk to long-term sustainability as Python typing does not stand still.

**Philosophy:** Zuban aims to provide the smoothest possible migration path from existing type checkers, particularly `mypy`, making it attractive for organizations with substantial existing typed codebases.
Expand Down Expand Up @@ -108,7 +108,7 @@ __Generated 29/08/2025__
> That being said even though `ty` is lagging on this metric at the moment it is still the type checker that I am most excited to use long-term because of the quality of the tooling Astral has built so far.

| Type Checker | Total Test Case Passes | Total Test Case Partial | Total False Positives | Total False Negatives |
| :---------------------------------------------: | :--------------------: | :---------------------: | :-------------------: | :-------------------: |
| :---------------------------------------------: | :--------------------: | :---------------------: | :-------------------: | :-------------------: |
| zuban 0.0.20 | 97 | 42 | 152 | 89 |
| ty 0.0.1-alpha.19 (e9cb838b3 2025-08-19) | 20 | 119 | 371 | 603 |
| Local:ty ruff/0.12.11+27 (0bf5d2a20 2025-08-29) | 20 | 119 | 370 | 590 |
Expand Down Expand Up @@ -165,7 +165,7 @@ For teams evaluating these type checkers, the conformance scores provide valuabl

[^1]: Which can be demonstrated in the [open issues](https://github.com/astral-sh/ruff/issues?q=is%3Aissue%20state%3Aopen%20label%3Atype-inference ) on ruff tagged with `type-inference` which are bugs or new features that can only be resolved with `ruff` having access to deeper type inference data that `ty` can supply.
[^2]: David has indicated a plan to make [source available in the future](https://github.com/python/typing/pull/2067#issuecomment-3177937964) when adding Zuban to the Python typing conformance suite.
[^3]: Full pricing information at: <https://zubanls.com/pricing/>
[^3]: ~~Full pricing information at: https://zubanls.com/pricing/~~ **Update (September 2025):** Pricing is now available on request for the non-AGPL license.
[^4]: This is just for this blog post, no plans to seek merging this.

<!-- Reference links --->
Expand Down
7 changes: 7 additions & 0 deletions content/pages/boardgames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
+++
title = "Boardgames"
date = 2025-08-27
template = "boardgames.html"
sort_by = "none"
path = "boardgames"
+++
77 changes: 77 additions & 0 deletions sass/boardgames.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.boardgames-container
max-width: 1200px
margin: 0 auto
padding: 20px

.games-grid
display: grid
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))
gap: 20px
margin-top: 20px

.game-card
border: 1px solid #404040
border-radius: 8px
padding: 16px
background: var(--bg-primary)
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3)
transition: transform 0.2s, box-shadow 0.2s

&:hover
transform: translateY(-2px)
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4)
border-color: var(--note-color)

.game-header
display: flex
gap: 12px
margin-bottom: 12px

.game-thumbnail
width: 85px
height: 110px
object-fit: cover
border-radius: 4px
border: 1px solid #404040

.game-info
flex: 1

.game-title
font-weight: bold
margin: 0 0 4px 0
color: #ffffff

.game-year
color: #cccccc
font-size: 0.9em
margin: 0

.game-stats
display: flex
gap: 16px
margin-bottom: 8px
font-size: 0.9em
color: #aaaaaa

.game-rating
font-weight: bold
color: var(--note-color)

&.rating-high
color: #27ae60

&.rating-medium
color: #f39c12

&.rating-low
color: #e74c3c

&.rating-unrated
color: #999999

.game-comment
color: #cccccc
font-size: 0.9em
margin-top: 8px
font-style: italic
122 changes: 122 additions & 0 deletions scripts/boardgames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "requests",
# ]
# ///

import requests
import xml.etree.ElementTree as ET
import json
import time
import logging
from typing import List, Dict, Any

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


def fetch_boardgame_collection(username: str) -> str:
url = f"https://boardgamegeek.com/xmlapi2/collection?username={username}&own=1&stats=1&excludesubtype=boardgameexpansion"

max_retries = 5
retry_delay = 2

for attempt in range(max_retries):
response = requests.get(url)

if response.status_code == 202:
logger.info(
f"Collection still processing, retrying in {retry_delay} seconds... (attempt {attempt + 1}/{max_retries})"
)
time.sleep(retry_delay)
retry_delay *= 2
continue

response.raise_for_status()
return response.text

raise Exception(f"Failed to fetch collection after {max_retries} attempts")


def parse_collection_xml(xml_content: str) -> List[Dict[str, Any]]:
root = ET.fromstring(xml_content)
games = []

for item in root.findall("item"):
game = {
"objectid": item.get("objectid"),
"name": None,
"yearpublished": None,
"my_rating": None,
"stats": {},
"comment": None,
}

name_elem = item.find("name")
if name_elem is not None:
game["name"] = name_elem.text

year_elem = item.find("yearpublished")
if year_elem is not None:
game["yearpublished"] = year_elem.text

thumbnail_elem = item.find("image")
if thumbnail_elem is not None:
game["image"] = thumbnail_elem.text
comment_elem = item.find("comment")
if comment_elem is not None:
game["comment"] = comment_elem.text

stats_elem = item.find("stats")
if stats_elem is not None:
rating_elem = stats_elem.find("rating")
if rating_elem is not None:
game["stats"] = {
"minplayers": stats_elem.get("minplayers"),
"maxplayers": stats_elem.get("maxplayers"),
"playingtime": stats_elem.get("playingtime"),
}
if rating_elem.get("value") != "N/A":
game["my_rating"] = float(rating_elem.get("value"))
games.append(game)
games = sorted(games, key=lambda x: x["my_rating"] or 0, reverse=True)
return games


def save_to_json(data: List[Dict[str, Any]], filename: str) -> None:
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)


def main():
username = "sinon88"
output_file = "../static/boardgames_collection.json"

try:
logger.info(f"Fetching board game collection for user: {username}")
xml_content = fetch_boardgame_collection(username)

logger.info("Parsing XML content...")
games_list = parse_collection_xml(xml_content)

logger.info(f"Found {len(games_list)} games in collection")

logger.info(f"Saving to {output_file}...")
save_to_json(games_list, output_file)

logger.info(f"Successfully saved board game collection to {output_file}")

except requests.exceptions.RequestException as e:
logger.error(f"Error fetching data: {e}")
except ET.ParseError as e:
logger.error(f"Error parsing XML: {e}")
except Exception as e:
logger.error(f"Unexpected error: {e}")


if __name__ == "__main__":
main()
Loading
Loading