xdfetch 0.0.1__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.
- xdfetch-0.2.0/.gitattributes +2 -0
- {xdfetch-0.0.1 → xdfetch-0.2.0}/.github/workflows/release.yml +1 -1
- xdfetch-0.2.0/.github/workflows/test.yml +32 -0
- xdfetch-0.2.0/PKG-INFO +287 -0
- xdfetch-0.2.0/README.md +262 -0
- {xdfetch-0.0.1 → xdfetch-0.2.0}/pyproject.toml +24 -7
- xdfetch-0.2.0/tests/compat/store/downloads/2026/20261029-120000-www.zip +0 -0
- xdfetch-0.2.0/tests/compat/store/downloads/2026/20261029-120000-www.zip.register.tsv +2 -0
- xdfetch-0.2.0/tests/compat/store/downloads/2026/20261030-130000-www.zip +0 -0
- xdfetch-0.2.0/tests/compat/store/downloads/2026/20261030-130000-www.zip.register.tsv +2 -0
- xdfetch-0.2.0/tests/compat/store/ledger.tsv +10 -0
- xdfetch-0.2.0/tests/compat/store/reports/2026/20261029-120000-report.md +17 -0
- xdfetch-0.2.0/tests/compat/store/reports/2026/20261030-130000-report.md +18 -0
- xdfetch-0.2.0/tests/conftest.py +136 -0
- xdfetch-0.2.0/tests/synthetic.py +594 -0
- xdfetch-0.2.0/tests/test_adapters.py +126 -0
- xdfetch-0.2.0/tests/test_amuse.py +478 -0
- xdfetch-0.2.0/tests/test_archive.py +108 -0
- xdfetch-0.2.0/tests/test_availability.py +86 -0
- xdfetch-0.2.0/tests/test_ccxml_puz.py +219 -0
- xdfetch-0.2.0/tests/test_cli.py +412 -0
- xdfetch-0.2.0/tests/test_compat.py +238 -0
- xdfetch-0.2.0/tests/test_conde.py +77 -0
- xdfetch-0.2.0/tests/test_conde_adapter.py +59 -0
- xdfetch-0.2.0/tests/test_convert.py +245 -0
- xdfetch-0.2.0/tests/test_fetch.py +233 -0
- xdfetch-0.2.0/tests/test_gamedist.py +307 -0
- xdfetch-0.2.0/tests/test_http.py +191 -0
- xdfetch-0.2.0/tests/test_ipuz.py +93 -0
- xdfetch-0.2.0/tests/test_ledger.py +61 -0
- xdfetch-0.2.0/tests/test_nyt.py +115 -0
- xdfetch-0.2.0/tests/test_nyt_adapter.py +88 -0
- xdfetch-0.2.0/tests/test_page.py +53 -0
- xdfetch-0.2.0/tests/test_plugins.py +180 -0
- xdfetch-0.2.0/tests/test_princetonian.py +80 -0
- xdfetch-0.2.0/tests/test_princetonian_adapter.py +85 -0
- xdfetch-0.2.0/tests/test_puzzlr.py +256 -0
- xdfetch-0.2.0/tests/test_puzzmo.py +602 -0
- xdfetch-0.2.0/tests/test_registry.py +225 -0
- xdfetch-0.2.0/tests/test_store.py +102 -0
- xdfetch-0.2.0/tests/test_wapo.py +87 -0
- xdfetch-0.2.0/uv.lock +622 -0
- {xdfetch-0.0.1 → xdfetch-0.2.0}/xdfetch/__init__.py +1 -1
- xdfetch-0.2.0/xdfetch/adapters/__init__.py +81 -0
- xdfetch-0.2.0/xdfetch/adapters/amuse.py +329 -0
- xdfetch-0.2.0/xdfetch/adapters/base.py +99 -0
- xdfetch-0.2.0/xdfetch/adapters/conde.py +63 -0
- xdfetch-0.2.0/xdfetch/adapters/gamedist.py +73 -0
- xdfetch-0.2.0/xdfetch/adapters/nyt.py +79 -0
- xdfetch-0.2.0/xdfetch/adapters/page.py +53 -0
- xdfetch-0.2.0/xdfetch/adapters/princetonian.py +98 -0
- xdfetch-0.2.0/xdfetch/adapters/puzzlr.py +78 -0
- xdfetch-0.2.0/xdfetch/adapters/puzzmo.py +187 -0
- xdfetch-0.2.0/xdfetch/adapters/url.py +37 -0
- xdfetch-0.2.0/xdfetch/archive.py +204 -0
- xdfetch-0.2.0/xdfetch/availability.py +111 -0
- xdfetch-0.2.0/xdfetch/cli.py +492 -0
- xdfetch-0.2.0/xdfetch/convert/__init__.py +177 -0
- xdfetch-0.2.0/xdfetch/convert/amuse.py +230 -0
- xdfetch-0.2.0/xdfetch/convert/ccjson.py +87 -0
- xdfetch-0.2.0/xdfetch/convert/ccxml.py +298 -0
- xdfetch-0.2.0/xdfetch/convert/conde.py +195 -0
- xdfetch-0.2.0/xdfetch/convert/ipuz.py +177 -0
- xdfetch-0.2.0/xdfetch/convert/nytjson.py +265 -0
- xdfetch-0.2.0/xdfetch/convert/princetonian.py +110 -0
- xdfetch-0.2.0/xdfetch/convert/puz.py +125 -0
- xdfetch-0.2.0/xdfetch/convert/puzzlr.py +164 -0
- xdfetch-0.2.0/xdfetch/convert/puzzmo.py +336 -0
- xdfetch-0.2.0/xdfetch/convert/uclick.py +108 -0
- xdfetch-0.2.0/xdfetch/convert/wapo.py +107 -0
- xdfetch-0.2.0/xdfetch/fetch.py +309 -0
- xdfetch-0.2.0/xdfetch/http.py +229 -0
- xdfetch-0.2.0/xdfetch/ledger.py +148 -0
- xdfetch-0.2.0/xdfetch/outlets.tsv +35 -0
- xdfetch-0.2.0/xdfetch/registry.py +308 -0
- xdfetch-0.2.0/xdfetch/store.py +117 -0
- xdfetch-0.2.0/xdfetch/tsv.py +41 -0
- xdfetch-0.2.0/xdfetch/xd.py +306 -0
- xdfetch-0.0.1/.github/workflows/test.yml +0 -18
- xdfetch-0.0.1/DESIGN.md +0 -74
- xdfetch-0.0.1/PKG-INFO +0 -56
- xdfetch-0.0.1/README.md +0 -36
- xdfetch-0.0.1/tests/test_cli.py +0 -24
- xdfetch-0.0.1/uv.lock +0 -202
- xdfetch-0.0.1/xdfetch/adapters/__init__.py +0 -5
- xdfetch-0.0.1/xdfetch/cli.py +0 -51
- xdfetch-0.0.1/xdfetch/convert/__init__.py +0 -5
- xdfetch-0.0.1/xdfetch/outlets.tsv +0 -1
- xdfetch-0.0.1/xdfetch/registry.py +0 -41
- {xdfetch-0.0.1 → xdfetch-0.2.0}/.gitignore +0 -0
- {xdfetch-0.0.1 → xdfetch-0.2.0}/LICENSE +0 -0
- {xdfetch-0.0.1 → xdfetch-0.2.0}/xdfetch/__main__.py +0 -0
|
@@ -50,7 +50,7 @@ jobs:
|
|
|
50
50
|
- run: twine check --strict dist/*
|
|
51
51
|
- name: run the tests against the built wheel
|
|
52
52
|
run: |
|
|
53
|
-
pip install "$(ls dist/*.whl)" pytest
|
|
53
|
+
pip install "$(ls dist/*.whl)" pytest responses
|
|
54
54
|
# run from outside the checkout so the tests import the installed wheel
|
|
55
55
|
cd "$RUNNER_TEMP"
|
|
56
56
|
pytest "$GITHUB_WORKSPACE/tests"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
lint:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v7
|
|
10
|
+
- uses: astral-sh/setup-uv@v7
|
|
11
|
+
- run: uv sync --locked
|
|
12
|
+
- run: uv run ruff check
|
|
13
|
+
- run: uv run ruff format --check
|
|
14
|
+
- run: uv run ty check
|
|
15
|
+
|
|
16
|
+
pytest:
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix:
|
|
21
|
+
os: [ubuntu-latest]
|
|
22
|
+
python-version: ["3.11", "3.14"]
|
|
23
|
+
include:
|
|
24
|
+
- os: windows-latest
|
|
25
|
+
python-version: "3.14"
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v7
|
|
28
|
+
- uses: astral-sh/setup-uv@v7
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
- run: uv sync --locked
|
|
32
|
+
- run: uv run pytest
|
xdfetch-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xdfetch
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Download crossword puzzles and convert them to .xd
|
|
5
|
+
Project-URL: Homepage, https://github.com/century-arcade/xdfetch
|
|
6
|
+
Project-URL: Repository, https://github.com/century-arcade/xdfetch
|
|
7
|
+
Project-URL: Issues, https://github.com/century-arcade/xdfetch/issues
|
|
8
|
+
Author: Alex DeJarnatt
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: crossword,downloader,puzzles,xd
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Games/Entertainment :: Puzzle Games
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: lxml>=5.0
|
|
20
|
+
Requires-Dist: requests>=2.32
|
|
21
|
+
Requires-Dist: tzdata>=2024.1; sys_platform == 'win32'
|
|
22
|
+
Provides-Extra: s3
|
|
23
|
+
Requires-Dist: boto3>=1.34; extra == 's3'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# xdfetch
|
|
27
|
+
|
|
28
|
+
Downloads crossword puzzles and converts them to
|
|
29
|
+
[.xd](https://github.com/century-arcade/xdformat), a plain-text crossword format.
|
|
30
|
+
|
|
31
|
+
**Early days.** It fetches puzzles from the outlets it knows (see Outlets below) or from a
|
|
32
|
+
URL, converts puzzle files (see Formats), and can keep an archive of every file it fetches
|
|
33
|
+
(see Keeping an archive).
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
pip install xdfetch # or: uv tool install xdfetch
|
|
38
|
+
|
|
39
|
+
xdfetch list # the outlets: id, days, homepage
|
|
40
|
+
xdfetch get nytimes.mini # the latest -> nytimes.mini-2026-11-08.xd
|
|
41
|
+
xdfetch get universal.daily --date 2026-11-02 # -> universal.daily-2026-11-02.xd
|
|
42
|
+
xdfetch get https://example.com/puzzle.puz # -> puzzle.xd
|
|
43
|
+
xdfetch get https://example.com/puzzle.puz --format both # also keep puzzle.puz as served
|
|
44
|
+
xdfetch get https://example.com/puzzle.puz --format raw # only the file as served
|
|
45
|
+
xdfetch convert *.puz *.ipuz # -> one .xd each
|
|
46
|
+
xdfetch convert old.puz -o - # to stdout
|
|
47
|
+
|
|
48
|
+
Output goes in the current directory unless `-o` names a file or a directory. An
|
|
49
|
+
existing file is left alone unless you add `--force`. `get` exits with 3 when the outlet
|
|
50
|
+
has no puzzle for the date. An outlet whose row names an `auth` variable reads its secret
|
|
51
|
+
from that environment variable.
|
|
52
|
+
|
|
53
|
+
The format is picked by looking at the file, not its extension. Text is written as UTF-8,
|
|
54
|
+
with curly quotes and ellipses made plain ASCII and HTML in clues turned into xd's markup.
|
|
55
|
+
|
|
56
|
+
## Keeping an archive
|
|
57
|
+
|
|
58
|
+
`get` fetches one puzzle. `run` is for fetching many, again and again: run it on a
|
|
59
|
+
schedule and it keeps a growing archive of every outlet's files, exactly as served, and a
|
|
60
|
+
ledger of what it has fetched so the next run doesn't repeat it.
|
|
61
|
+
|
|
62
|
+
xdfetch run --store ./archive # yesterday's puzzles from every enabled outlet
|
|
63
|
+
xdfetch run --store ./archive --start 2026-10-01 --id universal.daily usatoday.daily
|
|
64
|
+
xdfetch plan --store ./archive --date today # what run would fetch
|
|
65
|
+
xdfetch status --store ./archive # per outlet: last file, misses, errors
|
|
66
|
+
xdfetch availability --store ./archive # when each outlet's file is really up
|
|
67
|
+
xdfetch verify ./archive/downloads/2026/*.zip # check archives against their manifests
|
|
68
|
+
|
|
69
|
+
### The store
|
|
70
|
+
|
|
71
|
+
`--store` names a directory or `s3://bucket/prefix` (`pip install 'xdfetch[s3]'`); it can
|
|
72
|
+
also come from the `XDFETCH_STORE` environment variable. A run writes three things there:
|
|
73
|
+
|
|
74
|
+
ledger.tsv one row per fetch, added to by every run
|
|
75
|
+
downloads/2026/20261109-070000-www.zip the files this run fetched (none fetched, no zip)
|
|
76
|
+
reports/2026/20261109-070000-report.md this run's report
|
|
77
|
+
|
|
78
|
+
The archive holds files as the outlet served them, not `.xd`. Unzip one and
|
|
79
|
+
`xdfetch convert` its files when you want `.xd`.
|
|
80
|
+
|
|
81
|
+
### Which outlets
|
|
82
|
+
|
|
83
|
+
A run fetches every enabled outlet that `xdfetch list` shows, or only the ones named
|
|
84
|
+
after `--id`. `--registry FILE` (repeatable, on every command but `convert` and `verify`)
|
|
85
|
+
lays a file of your own rows over xdfetch's. It adds outlets, or replaces a built-in row
|
|
86
|
+
that has the same `id`; it does not narrow the run to the outlets in the file. A
|
|
87
|
+
tab-separated file with a header line, with only the columns you need (the columns are
|
|
88
|
+
listed under Outlets below):
|
|
89
|
+
|
|
90
|
+
id adapter params days homepage
|
|
91
|
+
example.weekly url url=https://example.com/puzzles/%Y-%m-%d.puz sat https://example.com/
|
|
92
|
+
|
|
93
|
+
To leave a built-in outlet out of every run, replace its row with a disabled one:
|
|
94
|
+
|
|
95
|
+
id adapter enabled
|
|
96
|
+
usatoday.daily url no
|
|
97
|
+
|
|
98
|
+
### Which dates
|
|
99
|
+
|
|
100
|
+
| Options | Dates |
|
|
101
|
+
|---|---|
|
|
102
|
+
| none | yesterday |
|
|
103
|
+
| `--date D` | that one date |
|
|
104
|
+
| `--start D` | from D to today |
|
|
105
|
+
| `--start D --end E` | from D to E |
|
|
106
|
+
|
|
107
|
+
A date is `YYYY-MM-DD`, `today` or `yesterday` (UTC). Within the range, an outlet is asked
|
|
108
|
+
only for the days it publishes (`days`, `since`) and only once the day's file should be up
|
|
109
|
+
(`available_after`).
|
|
110
|
+
|
|
111
|
+
A run looks only at the dates in its range. It does not work out what it missed since the
|
|
112
|
+
last run, so a job that skips a night, or that should retry misses and do re-fetches,
|
|
113
|
+
needs a range that reaches back. For example, with GNU `date`:
|
|
114
|
+
|
|
115
|
+
xdfetch run --store ./archive --start "$(date -d '8 days ago' +%F)"
|
|
116
|
+
|
|
117
|
+
Dates the ledger already has are skipped, so a wide range costs nothing extra.
|
|
118
|
+
|
|
119
|
+
### What is fetched
|
|
120
|
+
|
|
121
|
+
For each outlet and date in the range, the ledger decides (`plan` prints the reason):
|
|
122
|
+
|
|
123
|
+
| Reason | When |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `new` | the ledger has no row for it |
|
|
126
|
+
| `retry` | the last try was `missing` or `error`, and the date is at most 3 days old |
|
|
127
|
+
| `refetch` | the outlet's `refetch` column names days after publication to look again (`1,2,7`), and one is due |
|
|
128
|
+
| `forced` | `--force`: fetch it whatever the ledger says |
|
|
129
|
+
|
|
130
|
+
`--no-refetch` turns re-fetches off. Each try ends as one of:
|
|
131
|
+
|
|
132
|
+
| Status | Meaning |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `ok` | first copy of the file, archived |
|
|
135
|
+
| `same` | a re-fetch got identical bytes; nothing new is kept |
|
|
136
|
+
| `changed` | a re-fetch got different bytes; the new file is kept in this run's zip as `name.v2.ext`, and the first copy stays where it was |
|
|
137
|
+
| `missing` | the outlet has nothing for that date (a 404 or an empty page) |
|
|
138
|
+
| `error` | anything else; an outlet that errors 3 times in a row is skipped for the rest of the run |
|
|
139
|
+
|
|
140
|
+
An archive is never rewritten once stored. Each zip holds the fetched files under their
|
|
141
|
+
outlet's id, a `MANIFEST.tsv` (file name, sha256, size, URL and fetch time of each) and a
|
|
142
|
+
`NOTES.md` saying where each outlet's files came from.
|
|
143
|
+
|
|
144
|
+
Other options of `run`: `--dry-run` plans without fetching, `--interval SECONDS` sets the
|
|
145
|
+
pause between requests to one host (default 2), and `--summary FILE` writes the report to
|
|
146
|
+
a second place, such as a CI job summary. The report is also printed. `run` exits with 1
|
|
147
|
+
if any fetch ended in `error`; a `missing` is not an error.
|
|
148
|
+
|
|
149
|
+
### Checking on it
|
|
150
|
+
|
|
151
|
+
- `status` prints a line per outlet: the date of its last file, how many files, misses
|
|
152
|
+
and errors the ledger holds, and how many of its most recent dates in a row ended in a
|
|
153
|
+
miss or an error.
|
|
154
|
+
- `availability` compares each outlet's `available_after` with what the ledger saw (when
|
|
155
|
+
a file was first found, when it was still missing, and the server's Last-Modified), and
|
|
156
|
+
says whether the number should be raised or could be lower.
|
|
157
|
+
- `verify ZIP...` checks each zip against its own manifest: every listed file is there
|
|
158
|
+
with the same sha256 and size, and no file under an outlet's folder is unlisted. It
|
|
159
|
+
reads only the zips, and exits with 1 if any has a problem.
|
|
160
|
+
|
|
161
|
+
## Formats
|
|
162
|
+
|
|
163
|
+
| Format | Served by, for example |
|
|
164
|
+
|---|---|
|
|
165
|
+
| Across Lite `.puz` | many outlets and archives |
|
|
166
|
+
| ipuz (`.ipuz`, also as JSONP) | Crossword Compiler and other tools |
|
|
167
|
+
| Crossword Compiler XML (`.jpz`) | Arkadium's feeds, many others |
|
|
168
|
+
| uclick XML | Andrews McMeel (Universal, USA Today) |
|
|
169
|
+
| NYT puzzle JSON (v6) | The New York Times |
|
|
170
|
+
| Condé Nast puzzle record | The New Yorker |
|
|
171
|
+
| Washington Post games JSON | The Washington Post |
|
|
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 |
|
|
177
|
+
|
|
178
|
+
Other packages can add formats; see "Adding to xdfetch from another package" below.
|
|
179
|
+
|
|
180
|
+
## Outlets
|
|
181
|
+
|
|
182
|
+
The series xdfetch knows how to fetch are rows of
|
|
183
|
+
[`xdfetch/outlets.tsv`](xdfetch/outlets.tsv), each with an `id` of the form
|
|
184
|
+
`publisher.series`: `nytimes.daily`, `nytimes.mini`, `nytimes.midi`, `newyorker.daily`,
|
|
185
|
+
`newyorker.mini`, `universal.daily`, `universal.sunday`, `usatoday.daily`,
|
|
186
|
+
`washingtonpost.sunday`, `arkadium.mini`, `arkadium.two-minute-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`.
|
|
193
|
+
`xdfetch list` prints them all.
|
|
194
|
+
|
|
195
|
+
A row's columns, of which only `id` and `adapter` are required:
|
|
196
|
+
|
|
197
|
+
| Column | Meaning | Blank means |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `id` | the series' id | |
|
|
200
|
+
| `adapter` | which adapter fetches it: `url`, `page`, `nyt`, `conde`, `princetonian`, `amuse`, `gamedist`, `puzzlr`, `puzzmo`, or one a package adds | |
|
|
201
|
+
| `params` | the adapter's parameters, as `key=value` pairs separated by spaces (quote a value that holds spaces) | none |
|
|
202
|
+
| `days` | the days a puzzle runs: `daily`, `mon-sat`, `sun`, `mon,thu` | daily |
|
|
203
|
+
| `since` | the first date to expect a puzzle, `YYYY-MM-DD` | any date |
|
|
204
|
+
| `tz` | the outlet's timezone, an IANA name | `America/New_York` |
|
|
205
|
+
| `available_after` | hours after midnight in `tz` before the day's file is reliably up; negative for the evening before | 0 |
|
|
206
|
+
| `refetch` | days after publication to fetch again and compare, such as `1,2,7` | never |
|
|
207
|
+
| `homepage` | the outlet's own page for the series | |
|
|
208
|
+
| `auth` | the environment variable holding the secret the adapter needs | no secret |
|
|
209
|
+
| `enabled` | `yes` or `no` | yes |
|
|
210
|
+
| `notes` | what the file is, how it's named, quirks of the series | |
|
|
211
|
+
|
|
212
|
+
Two adapters need no code for a new outlet. `url` fetches a file at a predictable address:
|
|
213
|
+
its `url` param is the address with strftime fields for the date (`%Y-%m-%d`), and an
|
|
214
|
+
optional `filename` param names the file to keep it under. `page` fetches a dated page
|
|
215
|
+
(`page`, also with strftime fields) and then every file it links whose address matches
|
|
216
|
+
the `link` regex.
|
|
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
|
+
|
|
239
|
+
Adapters return each file as the outlet served it; converting is a separate step.
|
|
240
|
+
|
|
241
|
+
## Adding to xdfetch from another package
|
|
242
|
+
|
|
243
|
+
Another package can add adapters, outlet rows and formats without changing xdfetch,
|
|
244
|
+
through three entry-point groups in its `pyproject.toml`:
|
|
245
|
+
|
|
246
|
+
[project]
|
|
247
|
+
dependencies = ["xdfetch"]
|
|
248
|
+
|
|
249
|
+
# an adapter: the entry point's name is the adapter's name, its object an
|
|
250
|
+
# xdfetch.adapters.Adapter subclass
|
|
251
|
+
[project.entry-points."xdfetch.adapters"]
|
|
252
|
+
mysite = "mypackage.adapters:MySiteAdapter"
|
|
253
|
+
|
|
254
|
+
# outlet rows: a path to a TSV with the same columns as outlets.tsv (a str,
|
|
255
|
+
# os.PathLike or importlib.resources Traversable), or a function returning one
|
|
256
|
+
[project.entry-points."xdfetch.registries"]
|
|
257
|
+
mypackage = "mypackage:outlets_tsv"
|
|
258
|
+
|
|
259
|
+
# a converter: a (name, sniff, convert) tuple, or an object such as a module
|
|
260
|
+
# with sniff and convert functions
|
|
261
|
+
[project.entry-points."xdfetch.converters"]
|
|
262
|
+
myformat = "mypackage.myformat"
|
|
263
|
+
|
|
264
|
+
Converter plugins are sniffed before the built-in converters and may hand data back by
|
|
265
|
+
returning None; the docstring of `xdfetch.convert` gives the full shape.
|
|
266
|
+
|
|
267
|
+
`xdfetch.registry.effective()` reads xdfetch's own rows, then each package's TSV (in
|
|
268
|
+
order of entry-point name), then any files passed to it. A later row with the same
|
|
269
|
+
`id` replaces the earlier one whole, so a package can change a built-in outlet's row as
|
|
270
|
+
well as add its own. A program that keeps a registry of its own can load just its files
|
|
271
|
+
with `xdfetch.registry.from_files(paths)`; ids outside xdfetch's own rows can be any
|
|
272
|
+
string and are used as given. `xdfetch.adapters.get(name)` finds built-in adapters first,
|
|
273
|
+
then the packages'; a missing or broken one is an error that names it.
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
uv sync
|
|
278
|
+
uv run pytest
|
|
279
|
+
uv run ruff check && uv run ruff format --check
|
|
280
|
+
uv run ty check
|
|
281
|
+
|
|
282
|
+
Tests use tiny puzzles made up for the purpose (`tests/conftest.py`, `tests/synthetic.py`).
|
|
283
|
+
Please don't add real, published puzzles to the repo.
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT. See [LICENSE](LICENSE).
|
xdfetch-0.2.0/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# xdfetch
|
|
2
|
+
|
|
3
|
+
Downloads crossword puzzles and converts them to
|
|
4
|
+
[.xd](https://github.com/century-arcade/xdformat), a plain-text crossword format.
|
|
5
|
+
|
|
6
|
+
**Early days.** It fetches puzzles from the outlets it knows (see Outlets below) or from a
|
|
7
|
+
URL, converts puzzle files (see Formats), and can keep an archive of every file it fetches
|
|
8
|
+
(see Keeping an archive).
|
|
9
|
+
|
|
10
|
+
## Use
|
|
11
|
+
|
|
12
|
+
pip install xdfetch # or: uv tool install xdfetch
|
|
13
|
+
|
|
14
|
+
xdfetch list # the outlets: id, days, homepage
|
|
15
|
+
xdfetch get nytimes.mini # the latest -> nytimes.mini-2026-11-08.xd
|
|
16
|
+
xdfetch get universal.daily --date 2026-11-02 # -> universal.daily-2026-11-02.xd
|
|
17
|
+
xdfetch get https://example.com/puzzle.puz # -> puzzle.xd
|
|
18
|
+
xdfetch get https://example.com/puzzle.puz --format both # also keep puzzle.puz as served
|
|
19
|
+
xdfetch get https://example.com/puzzle.puz --format raw # only the file as served
|
|
20
|
+
xdfetch convert *.puz *.ipuz # -> one .xd each
|
|
21
|
+
xdfetch convert old.puz -o - # to stdout
|
|
22
|
+
|
|
23
|
+
Output goes in the current directory unless `-o` names a file or a directory. An
|
|
24
|
+
existing file is left alone unless you add `--force`. `get` exits with 3 when the outlet
|
|
25
|
+
has no puzzle for the date. An outlet whose row names an `auth` variable reads its secret
|
|
26
|
+
from that environment variable.
|
|
27
|
+
|
|
28
|
+
The format is picked by looking at the file, not its extension. Text is written as UTF-8,
|
|
29
|
+
with curly quotes and ellipses made plain ASCII and HTML in clues turned into xd's markup.
|
|
30
|
+
|
|
31
|
+
## Keeping an archive
|
|
32
|
+
|
|
33
|
+
`get` fetches one puzzle. `run` is for fetching many, again and again: run it on a
|
|
34
|
+
schedule and it keeps a growing archive of every outlet's files, exactly as served, and a
|
|
35
|
+
ledger of what it has fetched so the next run doesn't repeat it.
|
|
36
|
+
|
|
37
|
+
xdfetch run --store ./archive # yesterday's puzzles from every enabled outlet
|
|
38
|
+
xdfetch run --store ./archive --start 2026-10-01 --id universal.daily usatoday.daily
|
|
39
|
+
xdfetch plan --store ./archive --date today # what run would fetch
|
|
40
|
+
xdfetch status --store ./archive # per outlet: last file, misses, errors
|
|
41
|
+
xdfetch availability --store ./archive # when each outlet's file is really up
|
|
42
|
+
xdfetch verify ./archive/downloads/2026/*.zip # check archives against their manifests
|
|
43
|
+
|
|
44
|
+
### The store
|
|
45
|
+
|
|
46
|
+
`--store` names a directory or `s3://bucket/prefix` (`pip install 'xdfetch[s3]'`); it can
|
|
47
|
+
also come from the `XDFETCH_STORE` environment variable. A run writes three things there:
|
|
48
|
+
|
|
49
|
+
ledger.tsv one row per fetch, added to by every run
|
|
50
|
+
downloads/2026/20261109-070000-www.zip the files this run fetched (none fetched, no zip)
|
|
51
|
+
reports/2026/20261109-070000-report.md this run's report
|
|
52
|
+
|
|
53
|
+
The archive holds files as the outlet served them, not `.xd`. Unzip one and
|
|
54
|
+
`xdfetch convert` its files when you want `.xd`.
|
|
55
|
+
|
|
56
|
+
### Which outlets
|
|
57
|
+
|
|
58
|
+
A run fetches every enabled outlet that `xdfetch list` shows, or only the ones named
|
|
59
|
+
after `--id`. `--registry FILE` (repeatable, on every command but `convert` and `verify`)
|
|
60
|
+
lays a file of your own rows over xdfetch's. It adds outlets, or replaces a built-in row
|
|
61
|
+
that has the same `id`; it does not narrow the run to the outlets in the file. A
|
|
62
|
+
tab-separated file with a header line, with only the columns you need (the columns are
|
|
63
|
+
listed under Outlets below):
|
|
64
|
+
|
|
65
|
+
id adapter params days homepage
|
|
66
|
+
example.weekly url url=https://example.com/puzzles/%Y-%m-%d.puz sat https://example.com/
|
|
67
|
+
|
|
68
|
+
To leave a built-in outlet out of every run, replace its row with a disabled one:
|
|
69
|
+
|
|
70
|
+
id adapter enabled
|
|
71
|
+
usatoday.daily url no
|
|
72
|
+
|
|
73
|
+
### Which dates
|
|
74
|
+
|
|
75
|
+
| Options | Dates |
|
|
76
|
+
|---|---|
|
|
77
|
+
| none | yesterday |
|
|
78
|
+
| `--date D` | that one date |
|
|
79
|
+
| `--start D` | from D to today |
|
|
80
|
+
| `--start D --end E` | from D to E |
|
|
81
|
+
|
|
82
|
+
A date is `YYYY-MM-DD`, `today` or `yesterday` (UTC). Within the range, an outlet is asked
|
|
83
|
+
only for the days it publishes (`days`, `since`) and only once the day's file should be up
|
|
84
|
+
(`available_after`).
|
|
85
|
+
|
|
86
|
+
A run looks only at the dates in its range. It does not work out what it missed since the
|
|
87
|
+
last run, so a job that skips a night, or that should retry misses and do re-fetches,
|
|
88
|
+
needs a range that reaches back. For example, with GNU `date`:
|
|
89
|
+
|
|
90
|
+
xdfetch run --store ./archive --start "$(date -d '8 days ago' +%F)"
|
|
91
|
+
|
|
92
|
+
Dates the ledger already has are skipped, so a wide range costs nothing extra.
|
|
93
|
+
|
|
94
|
+
### What is fetched
|
|
95
|
+
|
|
96
|
+
For each outlet and date in the range, the ledger decides (`plan` prints the reason):
|
|
97
|
+
|
|
98
|
+
| Reason | When |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `new` | the ledger has no row for it |
|
|
101
|
+
| `retry` | the last try was `missing` or `error`, and the date is at most 3 days old |
|
|
102
|
+
| `refetch` | the outlet's `refetch` column names days after publication to look again (`1,2,7`), and one is due |
|
|
103
|
+
| `forced` | `--force`: fetch it whatever the ledger says |
|
|
104
|
+
|
|
105
|
+
`--no-refetch` turns re-fetches off. Each try ends as one of:
|
|
106
|
+
|
|
107
|
+
| Status | Meaning |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `ok` | first copy of the file, archived |
|
|
110
|
+
| `same` | a re-fetch got identical bytes; nothing new is kept |
|
|
111
|
+
| `changed` | a re-fetch got different bytes; the new file is kept in this run's zip as `name.v2.ext`, and the first copy stays where it was |
|
|
112
|
+
| `missing` | the outlet has nothing for that date (a 404 or an empty page) |
|
|
113
|
+
| `error` | anything else; an outlet that errors 3 times in a row is skipped for the rest of the run |
|
|
114
|
+
|
|
115
|
+
An archive is never rewritten once stored. Each zip holds the fetched files under their
|
|
116
|
+
outlet's id, a `MANIFEST.tsv` (file name, sha256, size, URL and fetch time of each) and a
|
|
117
|
+
`NOTES.md` saying where each outlet's files came from.
|
|
118
|
+
|
|
119
|
+
Other options of `run`: `--dry-run` plans without fetching, `--interval SECONDS` sets the
|
|
120
|
+
pause between requests to one host (default 2), and `--summary FILE` writes the report to
|
|
121
|
+
a second place, such as a CI job summary. The report is also printed. `run` exits with 1
|
|
122
|
+
if any fetch ended in `error`; a `missing` is not an error.
|
|
123
|
+
|
|
124
|
+
### Checking on it
|
|
125
|
+
|
|
126
|
+
- `status` prints a line per outlet: the date of its last file, how many files, misses
|
|
127
|
+
and errors the ledger holds, and how many of its most recent dates in a row ended in a
|
|
128
|
+
miss or an error.
|
|
129
|
+
- `availability` compares each outlet's `available_after` with what the ledger saw (when
|
|
130
|
+
a file was first found, when it was still missing, and the server's Last-Modified), and
|
|
131
|
+
says whether the number should be raised or could be lower.
|
|
132
|
+
- `verify ZIP...` checks each zip against its own manifest: every listed file is there
|
|
133
|
+
with the same sha256 and size, and no file under an outlet's folder is unlisted. It
|
|
134
|
+
reads only the zips, and exits with 1 if any has a problem.
|
|
135
|
+
|
|
136
|
+
## Formats
|
|
137
|
+
|
|
138
|
+
| Format | Served by, for example |
|
|
139
|
+
|---|---|
|
|
140
|
+
| Across Lite `.puz` | many outlets and archives |
|
|
141
|
+
| ipuz (`.ipuz`, also as JSONP) | Crossword Compiler and other tools |
|
|
142
|
+
| Crossword Compiler XML (`.jpz`) | Arkadium's feeds, many others |
|
|
143
|
+
| uclick XML | Andrews McMeel (Universal, USA Today) |
|
|
144
|
+
| NYT puzzle JSON (v6) | The New York Times |
|
|
145
|
+
| Condé Nast puzzle record | The New Yorker |
|
|
146
|
+
| Washington Post games JSON | The Washington Post |
|
|
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 |
|
|
152
|
+
|
|
153
|
+
Other packages can add formats; see "Adding to xdfetch from another package" below.
|
|
154
|
+
|
|
155
|
+
## Outlets
|
|
156
|
+
|
|
157
|
+
The series xdfetch knows how to fetch are rows of
|
|
158
|
+
[`xdfetch/outlets.tsv`](xdfetch/outlets.tsv), each with an `id` of the form
|
|
159
|
+
`publisher.series`: `nytimes.daily`, `nytimes.mini`, `nytimes.midi`, `newyorker.daily`,
|
|
160
|
+
`newyorker.mini`, `universal.daily`, `universal.sunday`, `usatoday.daily`,
|
|
161
|
+
`washingtonpost.sunday`, `arkadium.mini`, `arkadium.two-minute-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`.
|
|
168
|
+
`xdfetch list` prints them all.
|
|
169
|
+
|
|
170
|
+
A row's columns, of which only `id` and `adapter` are required:
|
|
171
|
+
|
|
172
|
+
| Column | Meaning | Blank means |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `id` | the series' id | |
|
|
175
|
+
| `adapter` | which adapter fetches it: `url`, `page`, `nyt`, `conde`, `princetonian`, `amuse`, `gamedist`, `puzzlr`, `puzzmo`, or one a package adds | |
|
|
176
|
+
| `params` | the adapter's parameters, as `key=value` pairs separated by spaces (quote a value that holds spaces) | none |
|
|
177
|
+
| `days` | the days a puzzle runs: `daily`, `mon-sat`, `sun`, `mon,thu` | daily |
|
|
178
|
+
| `since` | the first date to expect a puzzle, `YYYY-MM-DD` | any date |
|
|
179
|
+
| `tz` | the outlet's timezone, an IANA name | `America/New_York` |
|
|
180
|
+
| `available_after` | hours after midnight in `tz` before the day's file is reliably up; negative for the evening before | 0 |
|
|
181
|
+
| `refetch` | days after publication to fetch again and compare, such as `1,2,7` | never |
|
|
182
|
+
| `homepage` | the outlet's own page for the series | |
|
|
183
|
+
| `auth` | the environment variable holding the secret the adapter needs | no secret |
|
|
184
|
+
| `enabled` | `yes` or `no` | yes |
|
|
185
|
+
| `notes` | what the file is, how it's named, quirks of the series | |
|
|
186
|
+
|
|
187
|
+
Two adapters need no code for a new outlet. `url` fetches a file at a predictable address:
|
|
188
|
+
its `url` param is the address with strftime fields for the date (`%Y-%m-%d`), and an
|
|
189
|
+
optional `filename` param names the file to keep it under. `page` fetches a dated page
|
|
190
|
+
(`page`, also with strftime fields) and then every file it links whose address matches
|
|
191
|
+
the `link` regex.
|
|
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
|
+
|
|
214
|
+
Adapters return each file as the outlet served it; converting is a separate step.
|
|
215
|
+
|
|
216
|
+
## Adding to xdfetch from another package
|
|
217
|
+
|
|
218
|
+
Another package can add adapters, outlet rows and formats without changing xdfetch,
|
|
219
|
+
through three entry-point groups in its `pyproject.toml`:
|
|
220
|
+
|
|
221
|
+
[project]
|
|
222
|
+
dependencies = ["xdfetch"]
|
|
223
|
+
|
|
224
|
+
# an adapter: the entry point's name is the adapter's name, its object an
|
|
225
|
+
# xdfetch.adapters.Adapter subclass
|
|
226
|
+
[project.entry-points."xdfetch.adapters"]
|
|
227
|
+
mysite = "mypackage.adapters:MySiteAdapter"
|
|
228
|
+
|
|
229
|
+
# outlet rows: a path to a TSV with the same columns as outlets.tsv (a str,
|
|
230
|
+
# os.PathLike or importlib.resources Traversable), or a function returning one
|
|
231
|
+
[project.entry-points."xdfetch.registries"]
|
|
232
|
+
mypackage = "mypackage:outlets_tsv"
|
|
233
|
+
|
|
234
|
+
# a converter: a (name, sniff, convert) tuple, or an object such as a module
|
|
235
|
+
# with sniff and convert functions
|
|
236
|
+
[project.entry-points."xdfetch.converters"]
|
|
237
|
+
myformat = "mypackage.myformat"
|
|
238
|
+
|
|
239
|
+
Converter plugins are sniffed before the built-in converters and may hand data back by
|
|
240
|
+
returning None; the docstring of `xdfetch.convert` gives the full shape.
|
|
241
|
+
|
|
242
|
+
`xdfetch.registry.effective()` reads xdfetch's own rows, then each package's TSV (in
|
|
243
|
+
order of entry-point name), then any files passed to it. A later row with the same
|
|
244
|
+
`id` replaces the earlier one whole, so a package can change a built-in outlet's row as
|
|
245
|
+
well as add its own. A program that keeps a registry of its own can load just its files
|
|
246
|
+
with `xdfetch.registry.from_files(paths)`; ids outside xdfetch's own rows can be any
|
|
247
|
+
string and are used as given. `xdfetch.adapters.get(name)` finds built-in adapters first,
|
|
248
|
+
then the packages'; a missing or broken one is an error that names it.
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
uv sync
|
|
253
|
+
uv run pytest
|
|
254
|
+
uv run ruff check && uv run ruff format --check
|
|
255
|
+
uv run ty check
|
|
256
|
+
|
|
257
|
+
Tests use tiny puzzles made up for the purpose (`tests/conftest.py`, `tests/synthetic.py`).
|
|
258
|
+
Please don't add real, published puzzles to the repo.
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -5,22 +5,29 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "xdfetch"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "
|
|
8
|
+
description = "Download crossword puzzles and convert them to .xd"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
13
|
authors = [{ name = "Alex DeJarnatt" }]
|
|
14
14
|
keywords = ["crossword", "puzzles", "xd", "downloader"]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
17
17
|
"Environment :: Console",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
20
|
"Programming Language :: Python :: 3 :: Only",
|
|
21
21
|
"Topic :: Games/Entertainment :: Puzzle Games",
|
|
22
22
|
]
|
|
23
|
-
dependencies = [
|
|
23
|
+
dependencies = [
|
|
24
|
+
"lxml>=5.0",
|
|
25
|
+
"requests>=2.32",
|
|
26
|
+
"tzdata>=2024.1; sys_platform == 'win32'",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
s3 = ["boto3>=1.34"] # s3:// stores
|
|
24
31
|
|
|
25
32
|
[project.urls]
|
|
26
33
|
Homepage = "https://github.com/century-arcade/xdfetch"
|
|
@@ -31,7 +38,12 @@ Issues = "https://github.com/century-arcade/xdfetch/issues"
|
|
|
31
38
|
xdfetch = "xdfetch.cli:main"
|
|
32
39
|
|
|
33
40
|
[dependency-groups]
|
|
34
|
-
dev = [
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=8",
|
|
43
|
+
"responses>=0.26.3",
|
|
44
|
+
"ruff>=0.8",
|
|
45
|
+
"ty>=0.0.100",
|
|
46
|
+
]
|
|
35
47
|
|
|
36
48
|
[tool.hatch.version]
|
|
37
49
|
path = "xdfetch/__init__.py"
|
|
@@ -41,10 +53,15 @@ packages = ["xdfetch"]
|
|
|
41
53
|
|
|
42
54
|
[tool.ruff]
|
|
43
55
|
line-length = 120
|
|
44
|
-
target-version = "
|
|
56
|
+
target-version = "py311"
|
|
45
57
|
|
|
46
58
|
[tool.ruff.lint]
|
|
47
|
-
select = ["E", "F", "I", "UP", "B", "
|
|
59
|
+
select = ["E", "W", "F", "I", "UP", "B", "SIM", "RUF", "PT"]
|
|
60
|
+
ignore = ["RUF001", "RUF003"] # puzzle text is full of curly quotes, dashes and the like
|
|
61
|
+
|
|
62
|
+
[tool.ty.environment]
|
|
63
|
+
python-version = "3.11"
|
|
48
64
|
|
|
49
65
|
[tool.pytest.ini_options]
|
|
50
66
|
testpaths = ["tests"]
|
|
67
|
+
addopts = ["--strict-markers"]
|
|
Binary file
|
|
Binary file
|