xdfetch 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {xdfetch-0.1.0 → xdfetch-0.2.0}/PKG-INFO +33 -3
  2. {xdfetch-0.1.0 → xdfetch-0.2.0}/README.md +32 -2
  3. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/synthetic.py +61 -0
  4. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_adapters.py +11 -1
  5. xdfetch-0.2.0/tests/test_amuse.py +478 -0
  6. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_convert.py +14 -1
  7. xdfetch-0.2.0/tests/test_gamedist.py +307 -0
  8. xdfetch-0.2.0/tests/test_puzzlr.py +256 -0
  9. xdfetch-0.2.0/tests/test_puzzmo.py +602 -0
  10. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_registry.py +20 -0
  11. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/__init__.py +1 -1
  12. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/adapters/__init__.py +8 -0
  13. xdfetch-0.2.0/xdfetch/adapters/amuse.py +329 -0
  14. xdfetch-0.2.0/xdfetch/adapters/gamedist.py +73 -0
  15. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/adapters/nyt.py +5 -3
  16. xdfetch-0.2.0/xdfetch/adapters/puzzlr.py +78 -0
  17. xdfetch-0.2.0/xdfetch/adapters/puzzmo.py +187 -0
  18. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/__init__.py +5 -1
  19. xdfetch-0.2.0/xdfetch/convert/amuse.py +230 -0
  20. xdfetch-0.2.0/xdfetch/convert/ccjson.py +87 -0
  21. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/ccxml.py +33 -8
  22. xdfetch-0.2.0/xdfetch/convert/puzzlr.py +164 -0
  23. xdfetch-0.2.0/xdfetch/convert/puzzmo.py +336 -0
  24. xdfetch-0.2.0/xdfetch/outlets.tsv +35 -0
  25. xdfetch-0.1.0/xdfetch/outlets.tsv +0 -15
  26. {xdfetch-0.1.0 → xdfetch-0.2.0}/.gitattributes +0 -0
  27. {xdfetch-0.1.0 → xdfetch-0.2.0}/.github/workflows/release.yml +0 -0
  28. {xdfetch-0.1.0 → xdfetch-0.2.0}/.github/workflows/test.yml +0 -0
  29. {xdfetch-0.1.0 → xdfetch-0.2.0}/.gitignore +0 -0
  30. {xdfetch-0.1.0 → xdfetch-0.2.0}/LICENSE +0 -0
  31. {xdfetch-0.1.0 → xdfetch-0.2.0}/pyproject.toml +0 -0
  32. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/compat/store/downloads/2026/20261029-120000-www.zip +0 -0
  33. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/compat/store/downloads/2026/20261029-120000-www.zip.register.tsv +0 -0
  34. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/compat/store/downloads/2026/20261030-130000-www.zip +0 -0
  35. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/compat/store/downloads/2026/20261030-130000-www.zip.register.tsv +0 -0
  36. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/compat/store/ledger.tsv +0 -0
  37. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/compat/store/reports/2026/20261029-120000-report.md +0 -0
  38. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/compat/store/reports/2026/20261030-130000-report.md +0 -0
  39. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/conftest.py +0 -0
  40. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_archive.py +0 -0
  41. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_availability.py +0 -0
  42. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_ccxml_puz.py +0 -0
  43. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_cli.py +0 -0
  44. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_compat.py +0 -0
  45. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_conde.py +0 -0
  46. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_conde_adapter.py +0 -0
  47. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_fetch.py +0 -0
  48. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_http.py +0 -0
  49. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_ipuz.py +0 -0
  50. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_ledger.py +0 -0
  51. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_nyt.py +0 -0
  52. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_nyt_adapter.py +0 -0
  53. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_page.py +0 -0
  54. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_plugins.py +0 -0
  55. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_princetonian.py +0 -0
  56. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_princetonian_adapter.py +0 -0
  57. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_store.py +0 -0
  58. {xdfetch-0.1.0 → xdfetch-0.2.0}/tests/test_wapo.py +0 -0
  59. {xdfetch-0.1.0 → xdfetch-0.2.0}/uv.lock +0 -0
  60. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/__main__.py +0 -0
  61. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/adapters/base.py +0 -0
  62. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/adapters/conde.py +0 -0
  63. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/adapters/page.py +0 -0
  64. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/adapters/princetonian.py +0 -0
  65. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/adapters/url.py +0 -0
  66. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/archive.py +0 -0
  67. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/availability.py +0 -0
  68. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/cli.py +0 -0
  69. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/conde.py +0 -0
  70. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/ipuz.py +0 -0
  71. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/nytjson.py +0 -0
  72. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/princetonian.py +0 -0
  73. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/puz.py +0 -0
  74. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/uclick.py +0 -0
  75. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/convert/wapo.py +0 -0
  76. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/fetch.py +0 -0
  77. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/http.py +0 -0
  78. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/ledger.py +0 -0
  79. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/registry.py +0 -0
  80. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/store.py +0 -0
  81. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/tsv.py +0 -0
  82. {xdfetch-0.1.0 → xdfetch-0.2.0}/xdfetch/xd.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xdfetch
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Download crossword puzzles and convert them to .xd
5
5
  Project-URL: Homepage, https://github.com/century-arcade/xdfetch
6
6
  Project-URL: Repository, https://github.com/century-arcade/xdfetch
@@ -170,6 +170,10 @@ if any fetch ended in `error`; a `missing` is not an error.
170
170
  | Condé Nast puzzle record | The New Yorker |
171
171
  | Washington Post games JSON | The Washington Post |
172
172
  | Daily Princetonian crossword API | The Daily Princetonian |
173
+ | Amuse Labs (PuzzleMe) puzzle JSON, unscrambled from the player page | the LA Times, the Atlantic, Newsday, Vox, many others |
174
+ | Crossword Compiler as JSON (xml2js) | King Features, on GameDistribution's feeds |
175
+ | PZLR crossword level | The Wall Street Journal |
176
+ | Puzzmo's xd dialect, in its GraphQL answers | Puzzmo, the Boston Globe |
173
177
 
174
178
  Other packages can add formats; see "Adding to xdfetch from another package" below.
175
179
 
@@ -180,7 +184,12 @@ The series xdfetch knows how to fetch are rows of
180
184
  `publisher.series`: `nytimes.daily`, `nytimes.mini`, `nytimes.midi`, `newyorker.daily`,
181
185
  `newyorker.mini`, `universal.daily`, `universal.sunday`, `usatoday.daily`,
182
186
  `washingtonpost.sunday`, `arkadium.mini`, `arkadium.two-minute-mini`,
183
- `brendanemmettquigley.blog`, `dailyprincetonian.daily`, `dailyprincetonian.mini`.
187
+ `brendanemmettquigley.blog`, `dailyprincetonian.daily`, `dailyprincetonian.mini`,
188
+ `latimes.daily`, `latimes.midi`, `atlantic.daily`, `newsday.daily`, `dailybeast.daily`,
189
+ `crosswordclub.daily`, `crosswordclub.themed`, `vox.daily`, `vulture.daily`,
190
+ `walrus.weekly`, `defector.weekly`, `kingfeatures.thomas-joseph`,
191
+ `kingfeatures.eugene-sheffer`, `kingfeatures.premier-sunday`, `wsj.daily`,
192
+ `bostonglobe.sunday`, `bostonglobe.mini`, `puzzmo.daily`, `puzzmo.mini`, `puzzmo.big`.
184
193
  `xdfetch list` prints them all.
185
194
 
186
195
  A row's columns, of which only `id` and `adapter` are required:
@@ -188,7 +197,7 @@ A row's columns, of which only `id` and `adapter` are required:
188
197
  | Column | Meaning | Blank means |
189
198
  |---|---|---|
190
199
  | `id` | the series' id | |
191
- | `adapter` | which adapter fetches it: `url`, `page`, `nyt`, `conde`, `princetonian`, or one a package adds | |
200
+ | `adapter` | which adapter fetches it: `url`, `page`, `nyt`, `conde`, `princetonian`, `amuse`, `gamedist`, `puzzlr`, `puzzmo`, or one a package adds | |
192
201
  | `params` | the adapter's parameters, as `key=value` pairs separated by spaces (quote a value that holds spaces) | none |
193
202
  | `days` | the days a puzzle runs: `daily`, `mon-sat`, `sun`, `mon,thu` | daily |
194
203
  | `since` | the first date to expect a puzzle, `YYYY-MM-DD` | any date |
@@ -206,6 +215,27 @@ optional `filename` param names the file to keep it under. `page` fetches a date
206
215
  (`page`, also with strftime fields) and then every file it links whose address matches
207
216
  the `link` regex.
208
217
 
218
+ The `amuse` adapter fetches from an Amuse Labs (PuzzleMe) player: `host` is the player's
219
+ base address and `set` the series on it. The puzzle id for a date comes from an `id`
220
+ strftime template when the id encodes the date; or, with `page`, from the player embedded
221
+ in the outlet's own page for the date (`fallback` names an undated page to try when that
222
+ one is missing); else from the player's date picker, which lists only recent puzzles.
223
+ Its converter reads `editor`, `title_drop`, `copyright=year`, `publish_offset` and
224
+ `date_title=weekday` from the
225
+ row too; `xdfetch/adapters/amuse.py` and `xdfetch/convert/amuse.py` describe them all.
226
+
227
+ Three more fetch from platforms that also carry several outlets' puzzles:
228
+
229
+ - `gamedist`, GameDistribution's crossword feeds (King Features' puzzles): `feed` is the
230
+ feed's slug. The rows also set how the puzzle is written: `author` (a fixed byline),
231
+ `dash` and `endash` (what to write for an em or en dash in a clue) and `quotes=repair`
232
+ (a space after a clue's opening quote mark removed, unbalanced quote marks noted).
233
+ - `puzzlr`, PZLR: `tenant` is the publisher's slug, and `offset` the days added to the
234
+ date asked for to get the date PZLR files the puzzle under. `copyright` is written when
235
+ the puzzle carries none.
236
+ - `puzzmo`, Puzzmo: `team` and `queue` name a publisher's queue, or `finder` names one of
237
+ Puzzmo's own series (`crossword`, `crossword/mini`, `crossword/big`).
238
+
209
239
  Adapters return each file as the outlet served it; converting is a separate step.
210
240
 
211
241
  ## Adding to xdfetch from another package
@@ -145,6 +145,10 @@ if any fetch ended in `error`; a `missing` is not an error.
145
145
  | Condé Nast puzzle record | The New Yorker |
146
146
  | Washington Post games JSON | The Washington Post |
147
147
  | Daily Princetonian crossword API | The Daily Princetonian |
148
+ | Amuse Labs (PuzzleMe) puzzle JSON, unscrambled from the player page | the LA Times, the Atlantic, Newsday, Vox, many others |
149
+ | Crossword Compiler as JSON (xml2js) | King Features, on GameDistribution's feeds |
150
+ | PZLR crossword level | The Wall Street Journal |
151
+ | Puzzmo's xd dialect, in its GraphQL answers | Puzzmo, the Boston Globe |
148
152
 
149
153
  Other packages can add formats; see "Adding to xdfetch from another package" below.
150
154
 
@@ -155,7 +159,12 @@ The series xdfetch knows how to fetch are rows of
155
159
  `publisher.series`: `nytimes.daily`, `nytimes.mini`, `nytimes.midi`, `newyorker.daily`,
156
160
  `newyorker.mini`, `universal.daily`, `universal.sunday`, `usatoday.daily`,
157
161
  `washingtonpost.sunday`, `arkadium.mini`, `arkadium.two-minute-mini`,
158
- `brendanemmettquigley.blog`, `dailyprincetonian.daily`, `dailyprincetonian.mini`.
162
+ `brendanemmettquigley.blog`, `dailyprincetonian.daily`, `dailyprincetonian.mini`,
163
+ `latimes.daily`, `latimes.midi`, `atlantic.daily`, `newsday.daily`, `dailybeast.daily`,
164
+ `crosswordclub.daily`, `crosswordclub.themed`, `vox.daily`, `vulture.daily`,
165
+ `walrus.weekly`, `defector.weekly`, `kingfeatures.thomas-joseph`,
166
+ `kingfeatures.eugene-sheffer`, `kingfeatures.premier-sunday`, `wsj.daily`,
167
+ `bostonglobe.sunday`, `bostonglobe.mini`, `puzzmo.daily`, `puzzmo.mini`, `puzzmo.big`.
159
168
  `xdfetch list` prints them all.
160
169
 
161
170
  A row's columns, of which only `id` and `adapter` are required:
@@ -163,7 +172,7 @@ A row's columns, of which only `id` and `adapter` are required:
163
172
  | Column | Meaning | Blank means |
164
173
  |---|---|---|
165
174
  | `id` | the series' id | |
166
- | `adapter` | which adapter fetches it: `url`, `page`, `nyt`, `conde`, `princetonian`, or one a package adds | |
175
+ | `adapter` | which adapter fetches it: `url`, `page`, `nyt`, `conde`, `princetonian`, `amuse`, `gamedist`, `puzzlr`, `puzzmo`, or one a package adds | |
167
176
  | `params` | the adapter's parameters, as `key=value` pairs separated by spaces (quote a value that holds spaces) | none |
168
177
  | `days` | the days a puzzle runs: `daily`, `mon-sat`, `sun`, `mon,thu` | daily |
169
178
  | `since` | the first date to expect a puzzle, `YYYY-MM-DD` | any date |
@@ -181,6 +190,27 @@ optional `filename` param names the file to keep it under. `page` fetches a date
181
190
  (`page`, also with strftime fields) and then every file it links whose address matches
182
191
  the `link` regex.
183
192
 
193
+ The `amuse` adapter fetches from an Amuse Labs (PuzzleMe) player: `host` is the player's
194
+ base address and `set` the series on it. The puzzle id for a date comes from an `id`
195
+ strftime template when the id encodes the date; or, with `page`, from the player embedded
196
+ in the outlet's own page for the date (`fallback` names an undated page to try when that
197
+ one is missing); else from the player's date picker, which lists only recent puzzles.
198
+ Its converter reads `editor`, `title_drop`, `copyright=year`, `publish_offset` and
199
+ `date_title=weekday` from the
200
+ row too; `xdfetch/adapters/amuse.py` and `xdfetch/convert/amuse.py` describe them all.
201
+
202
+ Three more fetch from platforms that also carry several outlets' puzzles:
203
+
204
+ - `gamedist`, GameDistribution's crossword feeds (King Features' puzzles): `feed` is the
205
+ feed's slug. The rows also set how the puzzle is written: `author` (a fixed byline),
206
+ `dash` and `endash` (what to write for an em or en dash in a clue) and `quotes=repair`
207
+ (a space after a clue's opening quote mark removed, unbalanced quote marks noted).
208
+ - `puzzlr`, PZLR: `tenant` is the publisher's slug, and `offset` the days added to the
209
+ date asked for to get the date PZLR files the puzzle under. `copyright` is written when
210
+ the puzzle carries none.
211
+ - `puzzmo`, Puzzmo: `team` and `queue` name a publisher's queue, or `finder` names one of
212
+ Puzzmo's own series (`crossword`, `crossword/mini`, `crossword/big`).
213
+
184
214
  Adapters return each file as the outlet served it; converting is a separate step.
185
215
 
186
216
  ## Adding to xdfetch from another package
@@ -20,6 +20,7 @@ FOUR, a 4x4 with blocks in two corners:
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
+ import datetime as dt
23
24
  import json
24
25
  import struct
25
26
 
@@ -327,6 +328,66 @@ Made up for the tests.
327
328
  """
328
329
  )
329
330
 
331
+ # --- Amuse Labs (PuzzleMe) player JSON, as the amuse adapter keeps it ---
332
+
333
+
334
+ def amuse_ms(day: dt.date) -> int:
335
+ """publishTime for noon UTC on the day, in milliseconds."""
336
+ return int(dt.datetime(day.year, day.month, day.day, 12, 0, tzinfo=dt.UTC).timestamp() * 1000)
337
+
338
+
339
+ def amuse() -> dict:
340
+ """FIVE as the player's JSON: box is by column (box[x][y]), "\\x00" a block."""
341
+ box = [["\x00" if FIVE[r][c] == "#" else five_value(r, c) for r in range(5)] for c in range(5)]
342
+ words = [
343
+ ("<i>Male</i> duck, with &quot;quotes&quot;", True, 1, 0, 0),
344
+ ("Fossil resin — café colour", True, 4, 0, 2),
345
+ ("Handbills", True, 5, 0, 4),
346
+ ("Small fairy-tale miner", False, 1, 0, 0),
347
+ ("Monastery", False, 2, 2, 0),
348
+ ("Mistakes…", False, 3, 4, 0),
349
+ ]
350
+ return {
351
+ "title": "Bird Words",
352
+ "subtitle": "",
353
+ "description": "Made up for the tests.",
354
+ "author": "Ann Setter & Bo Builder / Ed. Cy Editor",
355
+ "copyright": "© 2026 Example Syndicate",
356
+ "id": "tca261028",
357
+ "puzzleType": "CROSSWORD",
358
+ "unlinkGridAndClues": False,
359
+ "publishTime": amuse_ms(dt.date(2026, 10, 28)),
360
+ "publishTimeZone": "America/New_York",
361
+ "w": 5,
362
+ "h": 5,
363
+ "box": box,
364
+ "cellInfos": [{"x": FIVE_CIRCLE[1], "y": FIVE_CIRCLE[0], "isCircled": True, "rightWall": False}],
365
+ "placedWords": [
366
+ {"x": x, "y": y, "acrossNotDown": across, "clueNum": str(n), "nBoxes": 5, "clue": {"clue": text}}
367
+ for text, across, n, x, y in words
368
+ ],
369
+ }
370
+
371
+
372
+ AMUSE_XD = (
373
+ """\
374
+ Title: Bird Words
375
+ Author: Ann Setter and Bo Builder
376
+ Editor: Cy Editor
377
+ Copyright: © 2026 Example Syndicate
378
+ Date: 2026-10-28
379
+ Special: circle
380
+ Rebus: 1=RS
381
+
382
+
383
+ """
384
+ + FIVE_XD_GRID_AND_CLUES
385
+ + """
386
+
387
+ Made up for the tests.
388
+ """
389
+ )
390
+
330
391
  # --- The Daily Princetonian (FOUR, with the A of SAGE circled) ---
331
392
 
332
393
  AUTHOR_1 = "00000000-0000-4000-8000-0000000000a1"
@@ -61,7 +61,17 @@ def test_fetch_returns_bytes_in_hand_without_a_request(registry, http):
61
61
 
62
62
 
63
63
  def test_builtins():
64
- assert set(adapters.ADAPTERS) == {"url", "nyt", "conde", "page", "princetonian"}
64
+ assert set(adapters.ADAPTERS) == {
65
+ "url",
66
+ "nyt",
67
+ "conde",
68
+ "page",
69
+ "princetonian",
70
+ "amuse",
71
+ "gamedist",
72
+ "puzzlr",
73
+ "puzzmo",
74
+ }
65
75
  assert adapters.get("url") is adapters.ADAPTERS["url"]
66
76
 
67
77