xdfetch 0.0.1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """xdfetch: fetch crossword puzzles as their outlets serve them, and convert them to .xd."""
2
+
3
+ __version__ = "0.0.1"
4
+
5
+ USER_AGENT = f"xdfetch/{__version__} (+https://github.com/century-arcade/xdfetch)"
xdfetch/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,5 @@
1
+ """Adapters: one per transport (a plain URL, an embed's API, ...).
2
+
3
+ An adapter answers two questions for an outlet and a date: where is the file, and what
4
+ did the host send. It returns the bytes as served; it never converts.
5
+ """
xdfetch/cli.py ADDED
@@ -0,0 +1,51 @@
1
+ """xdfetch command line.
2
+
3
+ list the outlets xdfetch knows
4
+ get fetch one outlet's puzzle and write it as .xd (not written yet)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+
12
+ from . import __version__, registry
13
+
14
+
15
+ def cmd_list(args: argparse.Namespace) -> int:
16
+ outlets = registry.load()
17
+ for o in sorted(outlets.values(), key=lambda o: o.id):
18
+ print(f"{o.id}\t{o.days}\t{o.homepage}")
19
+ return 0
20
+
21
+
22
+ def cmd_get(args: argparse.Namespace) -> int:
23
+ print("xdfetch get: not written yet", file=sys.stderr)
24
+ return 2
25
+
26
+
27
+ def build_parser() -> argparse.ArgumentParser:
28
+ p = argparse.ArgumentParser(
29
+ prog="xdfetch", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
30
+ )
31
+ p.add_argument("--version", action="version", version=f"xdfetch {__version__}")
32
+ sub = p.add_subparsers(dest="command", required=True)
33
+
34
+ sp = sub.add_parser("list", help="the outlets xdfetch knows")
35
+ sp.set_defaults(func=cmd_list)
36
+
37
+ sp = sub.add_parser("get", help="fetch one outlet's puzzle and write it as .xd")
38
+ sp.add_argument("outlet")
39
+ sp.add_argument("--date", help="YYYY-MM-DD (default: the outlet's latest)")
40
+ sp.set_defaults(func=cmd_get)
41
+
42
+ return p
43
+
44
+
45
+ def main(argv: list[str] | None = None) -> int:
46
+ args = build_parser().parse_args(argv)
47
+ return args.func(args)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ raise SystemExit(main())
@@ -0,0 +1,5 @@
1
+ """Converters: one per format, chosen by sniffing the bytes, never by file extension.
2
+
3
+ A converter takes the bytes an outlet served and builds a Puzzle. Whatever the source has
4
+ that .xd can't carry is listed on the Puzzle, not dropped silently.
5
+ """
xdfetch/outlets.tsv ADDED
@@ -0,0 +1 @@
1
+ id adapter params days tz homepage notes
xdfetch/registry.py ADDED
@@ -0,0 +1,41 @@
1
+ """The outlet registry: outlets.tsv, one row per outlet.
2
+
3
+ Columns:
4
+ id the outlet's id, following the xd corpus's publication ids
5
+ adapter which adapter fetches it (see xdfetch.adapters)
6
+ params the adapter's parameters, as space-separated key=value
7
+ days publication days: daily, mon-sat, sun, ...
8
+ tz the outlet's time zone, for "today"
9
+ homepage the outlet's own page for its puzzles
10
+ notes anything worth knowing
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import csv
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ OUTLETS_TSV = Path(__file__).with_name("outlets.tsv")
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Outlet:
24
+ id: str
25
+ adapter: str
26
+ params: str = ""
27
+ days: str = "daily"
28
+ tz: str = "America/New_York"
29
+ homepage: str = ""
30
+ notes: str = ""
31
+
32
+
33
+ def load(path: Path = OUTLETS_TSV) -> dict[str, Outlet]:
34
+ """Read a registry file. Later rows with the same id replace earlier ones."""
35
+ outlets: dict[str, Outlet] = {}
36
+ with path.open(encoding="utf-8", newline="") as f:
37
+ for row in csv.DictReader(f, delimiter="\t"):
38
+ fields = {k: (v or "") for k, v in row.items() if k in Outlet.__dataclass_fields__}
39
+ outlet = Outlet(**fields)
40
+ outlets[outlet.id] = outlet
41
+ return outlets
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.5
2
+ Name: xdfetch
3
+ Version: 0.0.1
4
+ Summary: Fetch crossword puzzles from online outlets 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 :: 1 - Planning
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.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # xdfetch
22
+
23
+ Fetches crossword puzzles from the outlets that publish them online and converts them to
24
+ [.xd](https://github.com/century-arcade/xdformat), the plain-text crossword format of the
25
+ [xd corpus](https://github.com/century-arcade/xd).
26
+
27
+ **Status: just started.** The package installs and the command runs, but it can't fetch
28
+ anything yet. [DESIGN.md](DESIGN.md) has the plan.
29
+
30
+ ## What it will do
31
+
32
+ pip install xdfetch
33
+ xdfetch list # the outlets it knows
34
+ xdfetch get lat # today's LA Times -> lat2026-11-09.xd
35
+ xdfetch get nyt --date 2026-11-01 # with your own subscription cookie in NYT_S
36
+ xdfetch run --store ./archive --start 2026-10-01 --end 2026-10-31 # keep an archive
37
+
38
+ Three rules shape it:
39
+
40
+ 1. **Fetch keeps the bytes as served.** The file the outlet sent is kept as it was, and
41
+ conversion reads from that.
42
+ 2. **Convert is separate:** one converter per *format*, not per outlet, chosen by looking
43
+ at the content. Anything a source has that .xd can't carry is reported, never dropped
44
+ silently.
45
+ 3. **Most outlets are a row, not code.** `outlets.tsv` says where each outlet's file is and
46
+ when it publishes; code is only needed for a new kind of host.
47
+
48
+ ## Development
49
+
50
+ uv sync --group dev
51
+ uv run pytest
52
+ uv run ruff check
53
+
54
+ ## License
55
+
56
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ xdfetch/__init__.py,sha256=2cLAjkCeyDyPOLzXlrJp5hBHc_0ctKDR3T68nvH0uz0,200
2
+ xdfetch/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ xdfetch/cli.py,sha256=rdtVqvZ5Nd41PoSvOJWzLeVxX6ZNn5w4MTMGc52GPpc,1407
4
+ xdfetch/outlets.tsv,sha256=hWuU-x3VI0YmGPT-ZneM6NTsJE82ilm2iSqf3bZtwIA,41
5
+ xdfetch/registry.py,sha256=yljLJuJEfQJ98X0v_PdeKP6yTz9ZyL4jE00pu-uLpsU,1351
6
+ xdfetch/adapters/__init__.py,sha256=Xvw5nWqetcO2qQ00Qr8dvROL_TlPLgcK0HgNg5EQ15s,229
7
+ xdfetch/convert/__init__.py,sha256=YlLc6ni2HpPR-vqRDATYOYrsDF1fat0ER65HHbLy7Yc,249
8
+ xdfetch-0.0.1.dist-info/METADATA,sha256=fOq4T18l-5GsOsfgFXC_dzAkNygdPaMqpikB5QqL_-0,2155
9
+ xdfetch-0.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ xdfetch-0.0.1.dist-info/entry_points.txt,sha256=nbVgi63O7_e4cyH4D5SZSMcmjec_0Flsl2PMKe0QQG8,45
11
+ xdfetch-0.0.1.dist-info/licenses/LICENSE,sha256=4XmtFFPTUKdPrypVToYNo6_d4Zd_uc2uaZV6OQ8D_ok,1081
12
+ xdfetch-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ xdfetch = xdfetch.cli:main
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 century-arcade
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.