Skip to content

Commit ddeecc1

Browse files
authored
Add a boardgames collection page (#18)
* Add a boardgames collection page * fix broken link, update boardgame images sizes * fix spacing in template
1 parent 2908516 commit ddeecc1

8 files changed

Lines changed: 1058 additions & 4 deletions

File tree

.lycheeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/themes
22
https://old.reddit.com/r/rust
3+
https://zubanls.com/pricing/

config.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default_language = "en"
44
title = "Rob's Blog | Python • Rust • Ramblings?"
55

66
# Whether to automatically compile all Sass files in the sass directory
7-
compile_sass = false
7+
compile_sass = true
88

99
# Whether to build a search index to be used later on by a JavaScript library
1010
build_search_index = false
@@ -29,6 +29,7 @@ after_dark_menu = [
2929
{ name = "Home", url = "$BASE_URL" },
3030
{ name = "About", url = "$BASE_URL/about/" },
3131
{ name = "Now", url = "$BASE_URL/now/" },
32+
{ name = "Boardgames", url = "$BASE_URL/boardgames" },
3233
{ name = "TILs", url = "$BASE_URL/tils/" },
3334
{ name = "Tags", url = "$BASE_URL/tags" },
3435
{ name = "Source", url = "https://github.com/sinon/sinon.github.io" },

content/future-python-type-checkers.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Before examining these new Rust-based tools, it's worth understanding the curren
7979

8080
- Created by the author of the popular Python LSP tool `jedi`
8181
- Aims for high-degree of compatibility with `mypy` to make adoption in large existing codebases seamless.
82-
- Not FOSS[^2], will require a license for codebases above 1.5 MB (~50,000 lines of code)[^3].
82+
- ~~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.
8383
- Currently maintained by a single author seems a potential risk to long-term sustainability as Python typing does not stand still.
8484

8585
**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.
@@ -108,7 +108,7 @@ __Generated 29/08/2025__
108108
> 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.
109109
110110
| Type Checker | Total Test Case Passes | Total Test Case Partial | Total False Positives | Total False Negatives |
111-
| :---------------------------------------------: | :--------------------: | :---------------------: | :-------------------: | :-------------------: |
111+
| :---------------------------------------------: | :--------------------: | :---------------------: | :-------------------: | :-------------------: |
112112
| zuban 0.0.20 | 97 | 42 | 152 | 89 |
113113
| ty 0.0.1-alpha.19 (e9cb838b3 2025-08-19) | 20 | 119 | 371 | 603 |
114114
| Local:ty ruff/0.12.11+27 (0bf5d2a20 2025-08-29) | 20 | 119 | 370 | 590 |
@@ -165,7 +165,7 @@ For teams evaluating these type checkers, the conformance scores provide valuabl
165165

166166
[^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.
167167
[^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.
168-
[^3]: Full pricing information at: <https://zubanls.com/pricing/>
168+
[^3]: ~~Full pricing information at: https://zubanls.com/pricing/~~ **Update (September 2025):** Pricing is now available on request for the non-AGPL license.
169169
[^4]: This is just for this blog post, no plans to seek merging this.
170170

171171
<!-- Reference links --->

content/pages/boardgames.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
+++
2+
title = "Boardgames"
3+
date = 2025-08-27
4+
template = "boardgames.html"
5+
sort_by = "none"
6+
path = "boardgames"
7+
+++

sass/boardgames.sass

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
.boardgames-container
2+
max-width: 1200px
3+
margin: 0 auto
4+
padding: 20px
5+
6+
.games-grid
7+
display: grid
8+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))
9+
gap: 20px
10+
margin-top: 20px
11+
12+
.game-card
13+
border: 1px solid #404040
14+
border-radius: 8px
15+
padding: 16px
16+
background: var(--bg-primary)
17+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3)
18+
transition: transform 0.2s, box-shadow 0.2s
19+
20+
&:hover
21+
transform: translateY(-2px)
22+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4)
23+
border-color: var(--note-color)
24+
25+
.game-header
26+
display: flex
27+
gap: 12px
28+
margin-bottom: 12px
29+
30+
.game-thumbnail
31+
width: 85px
32+
height: 110px
33+
object-fit: cover
34+
border-radius: 4px
35+
border: 1px solid #404040
36+
37+
.game-info
38+
flex: 1
39+
40+
.game-title
41+
font-weight: bold
42+
margin: 0 0 4px 0
43+
color: #ffffff
44+
45+
.game-year
46+
color: #cccccc
47+
font-size: 0.9em
48+
margin: 0
49+
50+
.game-stats
51+
display: flex
52+
gap: 16px
53+
margin-bottom: 8px
54+
font-size: 0.9em
55+
color: #aaaaaa
56+
57+
.game-rating
58+
font-weight: bold
59+
color: var(--note-color)
60+
61+
&.rating-high
62+
color: #27ae60
63+
64+
&.rating-medium
65+
color: #f39c12
66+
67+
&.rating-low
68+
color: #e74c3c
69+
70+
&.rating-unrated
71+
color: #999999
72+
73+
.game-comment
74+
color: #cccccc
75+
font-size: 0.9em
76+
margin-top: 8px
77+
font-style: italic

scripts/boardgames.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env -S uv run
2+
# /// script
3+
# requires-python = ">=3.13"
4+
# dependencies = [
5+
# "requests",
6+
# ]
7+
# ///
8+
9+
import requests
10+
import xml.etree.ElementTree as ET
11+
import json
12+
import time
13+
import logging
14+
from typing import List, Dict, Any
15+
16+
logging.basicConfig(
17+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
18+
)
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def fetch_boardgame_collection(username: str) -> str:
23+
url = f"https://boardgamegeek.com/xmlapi2/collection?username={username}&own=1&stats=1&excludesubtype=boardgameexpansion"
24+
25+
max_retries = 5
26+
retry_delay = 2
27+
28+
for attempt in range(max_retries):
29+
response = requests.get(url)
30+
31+
if response.status_code == 202:
32+
logger.info(
33+
f"Collection still processing, retrying in {retry_delay} seconds... (attempt {attempt + 1}/{max_retries})"
34+
)
35+
time.sleep(retry_delay)
36+
retry_delay *= 2
37+
continue
38+
39+
response.raise_for_status()
40+
return response.text
41+
42+
raise Exception(f"Failed to fetch collection after {max_retries} attempts")
43+
44+
45+
def parse_collection_xml(xml_content: str) -> List[Dict[str, Any]]:
46+
root = ET.fromstring(xml_content)
47+
games = []
48+
49+
for item in root.findall("item"):
50+
game = {
51+
"objectid": item.get("objectid"),
52+
"name": None,
53+
"yearpublished": None,
54+
"my_rating": None,
55+
"stats": {},
56+
"comment": None,
57+
}
58+
59+
name_elem = item.find("name")
60+
if name_elem is not None:
61+
game["name"] = name_elem.text
62+
63+
year_elem = item.find("yearpublished")
64+
if year_elem is not None:
65+
game["yearpublished"] = year_elem.text
66+
67+
thumbnail_elem = item.find("image")
68+
if thumbnail_elem is not None:
69+
game["image"] = thumbnail_elem.text
70+
comment_elem = item.find("comment")
71+
if comment_elem is not None:
72+
game["comment"] = comment_elem.text
73+
74+
stats_elem = item.find("stats")
75+
if stats_elem is not None:
76+
rating_elem = stats_elem.find("rating")
77+
if rating_elem is not None:
78+
game["stats"] = {
79+
"minplayers": stats_elem.get("minplayers"),
80+
"maxplayers": stats_elem.get("maxplayers"),
81+
"playingtime": stats_elem.get("playingtime"),
82+
}
83+
if rating_elem.get("value") != "N/A":
84+
game["my_rating"] = float(rating_elem.get("value"))
85+
games.append(game)
86+
games = sorted(games, key=lambda x: x["my_rating"] or 0, reverse=True)
87+
return games
88+
89+
90+
def save_to_json(data: List[Dict[str, Any]], filename: str) -> None:
91+
with open(filename, "w", encoding="utf-8") as f:
92+
json.dump(data, f, indent=2, ensure_ascii=False)
93+
94+
95+
def main():
96+
username = "sinon88"
97+
output_file = "../static/boardgames_collection.json"
98+
99+
try:
100+
logger.info(f"Fetching board game collection for user: {username}")
101+
xml_content = fetch_boardgame_collection(username)
102+
103+
logger.info("Parsing XML content...")
104+
games_list = parse_collection_xml(xml_content)
105+
106+
logger.info(f"Found {len(games_list)} games in collection")
107+
108+
logger.info(f"Saving to {output_file}...")
109+
save_to_json(games_list, output_file)
110+
111+
logger.info(f"Successfully saved board game collection to {output_file}")
112+
113+
except requests.exceptions.RequestException as e:
114+
logger.error(f"Error fetching data: {e}")
115+
except ET.ParseError as e:
116+
logger.error(f"Error parsing XML: {e}")
117+
except Exception as e:
118+
logger.error(f"Unexpected error: {e}")
119+
120+
121+
if __name__ == "__main__":
122+
main()

0 commit comments

Comments
 (0)