xkcdkit 0.1.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.
xkcdkit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: xkcdkit
3
+ Version: 0.1.0
4
+ Summary: xkcd comics as a Python library, a CLI and a TUI, with a local cache and explainxkcd explanations.
5
+ Keywords: xkcd,comics,cli,tui,textual
6
+ Author: Eivind Teig
7
+ Author-email: Eivind Teig <eivind@dchub.no>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
+ Classifier: Topic :: Terminals
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: httpx2>=2.16
21
+ Requires-Dist: pydantic>=2.11
22
+ Requires-Dist: typer>=0.17
23
+ Requires-Dist: textual>=6.0
24
+ Requires-Dist: textual-image>=0.14
25
+ Requires-Dist: rapidfuzz>=3.13
26
+ Requires-Dist: platformdirs>=4.3
27
+ Requires-Python: >=3.12
28
+ Project-URL: Homepage, https://github.com/eivl/xkcdkit
29
+ Project-URL: Repository, https://github.com/eivl/xkcdkit
30
+ Project-URL: Changelog, https://github.com/eivl/xkcdkit/blob/main/CHANGELOG.md
31
+ Description-Content-Type: text/markdown
32
+
33
+ # xkcdkit
34
+
35
+ xkcd comics as a Python library, a command line tool and a terminal browser.
36
+ Everything you look at is cached in a local SQLite database, together with the
37
+ explanation from [explainxkcd](https://www.explainxkcd.com/), so repeat lookups
38
+ and searches never touch the network.
39
+
40
+ ```
41
+ $ xkcd show 614
42
+ ╭─ #614 Woodpecker ───────────────────────────────────────────────╮
43
+ │ If you don't have an extension cord I can get that too. Because │
44
+ │ we're friends! Right? │
45
+ │ https://xkcd.com/614/ │
46
+ ╰──────────────────────────────────────────────────── 2009-07-24 ──╯
47
+ ```
48
+
49
+ On a terminal that supports the kitty graphics protocol or sixel, the comic
50
+ image is drawn above the card.
51
+
52
+ ## Install
53
+
54
+ ### As a library
55
+
56
+ ```sh
57
+ pip install xkcdkit
58
+ # or
59
+ uv add xkcdkit
60
+ ```
61
+
62
+ ### As a command line tool and TUI
63
+
64
+ Install it in its own isolated environment so it never conflicts with your
65
+ projects:
66
+
67
+ ```sh
68
+ pipx install xkcdkit
69
+ # or
70
+ uv tool install xkcdkit
71
+ ```
72
+
73
+ Either way you get the `xkcd` command.
74
+
75
+ ### Linux and macOS
76
+
77
+ The commands above work as written. If you prefer not to install anything
78
+ permanently:
79
+
80
+ ```sh
81
+ uvx xkcdkit show 614
82
+ ```
83
+
84
+ ### Windows
85
+
86
+ On Windows the Python launcher is called `py`, and `python` is often not on
87
+ your PATH (or opens the Microsoft Store). Use the launcher for everything that
88
+ starts with `python`:
89
+
90
+ ```powershell
91
+ py -m pip install --user pipx
92
+ py -m pipx ensurepath
93
+ pipx install xkcdkit
94
+ ```
95
+
96
+ or, with uv installed:
97
+
98
+ ```powershell
99
+ uv tool install xkcdkit
100
+ ```
101
+
102
+ Images need Windows Terminal 1.22 or newer, which supports sixel. The old
103
+ console host (`conhost.exe`) cannot draw images, and xkcdkit falls back to a
104
+ text-only card there.
105
+
106
+ ## Command line
107
+
108
+ ```
109
+ xkcd show 614 # one comic by number
110
+ xkcd show 614 --explain # with the explainxkcd explanation
111
+ xkcd latest # the newest comic
112
+ xkcd random # a random comic
113
+ xkcd search "woodpecker" # fuzzy search over cached titles, alt text, transcripts and explanations
114
+ xkcd explain 614 # just the explanation, as plain text
115
+ xkcd sync # refresh the archive index and the latest comic
116
+ xkcd sync --full # download every comic and explanation (about an hour, see below)
117
+ xkcd resync # re-fetch comics older than 30 days or still missing an explanation
118
+ xkcd tui # open the browser
119
+ ```
120
+
121
+ Add `--json` to `show`, `latest`, `random`, `search` or `explain` for
122
+ machine-readable output. Nothing but the JSON goes to stdout; errors go to
123
+ stderr with exit code 1, so pipelines stay clean:
124
+
125
+ ```sh
126
+ xkcd show 614 --json | jq .alt
127
+ xkcd search "sandwich" --json | jq '.[].num'
128
+ ```
129
+
130
+ `--no-image` skips the picture. Images are also skipped automatically when
131
+ stdout is not a terminal, so pipes always get clean text.
132
+
133
+ ### The browser
134
+
135
+ `xkcd tui` opens a two-pane browser in the spirit of lazygit: the archive on
136
+ the left, the comic on the right with tabs for alt text, transcript and
137
+ explanation.
138
+
139
+ | Key | Action |
140
+ | ------- | -------------------------------- |
141
+ | `j`/`k` | move through the archive |
142
+ | `/` | filter the archive by title |
143
+ | `Enter` | open the selected comic |
144
+ | `r` | random comic |
145
+ | `l` | latest comic |
146
+ | `i` | show or hide the image pane |
147
+ | `s` | sync the archive index |
148
+ | `?` | key help |
149
+ | `q` | quit |
150
+
151
+ ## Library
152
+
153
+ The async client is the primary API. It is built for I/O bound programs such
154
+ as bots and web services.
155
+
156
+ ```python
157
+ from xkcdkit import AsyncXKCD
158
+
159
+ async with AsyncXKCD() as xk:
160
+ comic = await xk.get(614)
161
+ print(comic.title, comic.alt, comic.img)
162
+ print(comic.explanation_text[:200])
163
+
164
+ latest = await xk.latest()
165
+ hits = await xk.search("sandwich", limit=5)
166
+ await xk.sync() # archive index + latest comic
167
+ ```
168
+
169
+ A blocking twin exists for scripts and the REPL. It runs each call on its own
170
+ event loop, so it cannot be used from inside async code, and it opens and
171
+ closes its connections per call (pass `client_factory=` rather than a client
172
+ if you need to customise HTTP).
173
+
174
+ ```python
175
+ from xkcdkit import XKCD
176
+
177
+ with XKCD() as xk:
178
+ print(xk.random().title)
179
+ ```
180
+
181
+ In a long-running program such as a Discord bot, keep one `AsyncXKCD` open
182
+ for the lifetime of the process: it holds the HTTP connection pool and the
183
+ database connection.
184
+
185
+ ```python
186
+ from xkcdkit import AsyncXKCD, ComicNotFound
187
+
188
+ xk = AsyncXKCD() # created once; call `await xk.close()` on shutdown
189
+
190
+
191
+ @bot.command()
192
+ async def xkcd(ctx, num: int):
193
+ try:
194
+ comic = await xk.get(num)
195
+ except ComicNotFound:
196
+ await ctx.send(f"There is no xkcd {num}.")
197
+ return
198
+ await ctx.send(f"**{comic.title}**\n{comic.img}\n*{comic.alt}*")
199
+ ```
200
+
201
+ ### The `Comic` model
202
+
203
+ `Comic` is a pydantic model with every field from the xkcd JSON (`num`,
204
+ `title`, `safe_title`, `alt`, `img`, `transcript`, `link`, `news`, `year`,
205
+ `month`, `day`) plus:
206
+
207
+ | Field | Meaning |
208
+ | --------------------- | ----------------------------------------------------------- |
209
+ | `explanation` | full explainxkcd page wikitext, or `None` |
210
+ | `explanation_revid` | the wiki revision that was fetched |
211
+ | `explanation_missing` | `True` when explainxkcd had no page at sync time |
212
+ | `synced_at` | when the row was written (UTC) |
213
+ | `explanation_text` | property: the Explanation section as plain text |
214
+ | `date`, `url` | properties: publication date and the comic's page |
215
+
216
+ `comic.model_dump_json()` gives you the same JSON the CLI prints.
217
+
218
+ ## Data, caching and freshness
219
+
220
+ - The database lives in your platform's user data directory
221
+ (`~/.local/share/xkcdkit/xkcd.sqlite3` on Linux, `~/Library/Application
222
+ Support/xkcdkit/` on macOS, `%LOCALAPPDATA%\xkcdkit\` on Windows). Set
223
+ `XKCDKIT_DB` or pass `--db` / `db=` to put it elsewhere.
224
+ - Comics are immutable once stored. A cached comic is served as is, except
225
+ when its explanation was missing more than a day ago; then explainxkcd is
226
+ asked again on the next access.
227
+ - `resync` re-fetches rows older than `--max-age` days (default 30) and every
228
+ row still flagged `explanation_missing`. Recently synced data is trusted.
229
+ - Roughly 1 to 2 percent of comics have no explainxkcd page yet. Those rows
230
+ carry `explanation_missing = True` so your code can decide what to do.
231
+
232
+ ### Being polite to explainxkcd
233
+
234
+ explainxkcd is a volunteer-run wiki. Its robots policy asks for one request
235
+ per second, so a full sync fetches one explanation per second and sends a
236
+ User-Agent that names this project and its repository. A complete first
237
+ `sync --full` therefore takes about an hour. On-demand lookups are not
238
+ delayed.
239
+
240
+ ## Terminal image support
241
+
242
+ | Terminal | Protocol | Status |
243
+ | ---------------------------- | -------- | ------------------------- |
244
+ | kitty, Ghostty | kitty | works |
245
+ | WezTerm, foot, iTerm2 | sixel | works |
246
+ | Windows Terminal 1.22+ | sixel | works |
247
+ | VS Code terminal | sixel | enable `terminal.integrated.enableImages` |
248
+ | tmux | – | falls back to half-block unicode |
249
+ | GNOME Terminal, conhost | – | falls back to half-block unicode |
250
+
251
+ Detection and rendering are done by [textual-image](https://github.com/lnqs/textual-image).
252
+
253
+ ## Development
254
+
255
+ ```sh
256
+ git clone https://github.com/eivl/xkcdkit
257
+ cd xkcdkit
258
+ uv sync --all-groups
259
+ uv run pre-commit install
260
+ uv run pytest
261
+ uv run tox # all supported Python versions
262
+ ```
263
+
264
+ The code base is deliberately small: seven modules, short functions, a
265
+ docstring on everything. ruff (with pydocstyle), mypy strict, flake8 and
266
+ zizmor run in pre-commit and in CI.
267
+
268
+ ### Releasing
269
+
270
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
271
+ On every push to `main`, python-semantic-release reads the commit messages,
272
+ bumps the version in `pyproject.toml`, updates `CHANGELOG.md`, tags the
273
+ release and publishes to PyPI through trusted publishing. `feat:` bumps the
274
+ minor version, `fix:` the patch version, and a `BREAKING CHANGE:` footer the
275
+ major version. The release commit also re-locks `uv.lock` so that
276
+ `uv sync --locked` keeps working after the bump.
277
+
278
+ Trusted publishing has to be registered once on PyPI under
279
+ *Publishing → Add a new pending publisher* with these values:
280
+
281
+ | Field | Value |
282
+ | ---------------- | -------------- |
283
+ | PyPI project | `xkcdkit` |
284
+ | Owner | `eivl` |
285
+ | Repository | `xkcdkit` |
286
+ | Workflow | `release.yml` |
287
+ | Environment | `pypi` |
288
+
289
+ and a GitHub environment named `pypi` must exist in the repository settings.
290
+
291
+ ## License
292
+
293
+ MIT
@@ -0,0 +1,261 @@
1
+ # xkcdkit
2
+
3
+ xkcd comics as a Python library, a command line tool and a terminal browser.
4
+ Everything you look at is cached in a local SQLite database, together with the
5
+ explanation from [explainxkcd](https://www.explainxkcd.com/), so repeat lookups
6
+ and searches never touch the network.
7
+
8
+ ```
9
+ $ xkcd show 614
10
+ ╭─ #614 Woodpecker ───────────────────────────────────────────────╮
11
+ │ If you don't have an extension cord I can get that too. Because │
12
+ │ we're friends! Right? │
13
+ │ https://xkcd.com/614/ │
14
+ ╰──────────────────────────────────────────────────── 2009-07-24 ──╯
15
+ ```
16
+
17
+ On a terminal that supports the kitty graphics protocol or sixel, the comic
18
+ image is drawn above the card.
19
+
20
+ ## Install
21
+
22
+ ### As a library
23
+
24
+ ```sh
25
+ pip install xkcdkit
26
+ # or
27
+ uv add xkcdkit
28
+ ```
29
+
30
+ ### As a command line tool and TUI
31
+
32
+ Install it in its own isolated environment so it never conflicts with your
33
+ projects:
34
+
35
+ ```sh
36
+ pipx install xkcdkit
37
+ # or
38
+ uv tool install xkcdkit
39
+ ```
40
+
41
+ Either way you get the `xkcd` command.
42
+
43
+ ### Linux and macOS
44
+
45
+ The commands above work as written. If you prefer not to install anything
46
+ permanently:
47
+
48
+ ```sh
49
+ uvx xkcdkit show 614
50
+ ```
51
+
52
+ ### Windows
53
+
54
+ On Windows the Python launcher is called `py`, and `python` is often not on
55
+ your PATH (or opens the Microsoft Store). Use the launcher for everything that
56
+ starts with `python`:
57
+
58
+ ```powershell
59
+ py -m pip install --user pipx
60
+ py -m pipx ensurepath
61
+ pipx install xkcdkit
62
+ ```
63
+
64
+ or, with uv installed:
65
+
66
+ ```powershell
67
+ uv tool install xkcdkit
68
+ ```
69
+
70
+ Images need Windows Terminal 1.22 or newer, which supports sixel. The old
71
+ console host (`conhost.exe`) cannot draw images, and xkcdkit falls back to a
72
+ text-only card there.
73
+
74
+ ## Command line
75
+
76
+ ```
77
+ xkcd show 614 # one comic by number
78
+ xkcd show 614 --explain # with the explainxkcd explanation
79
+ xkcd latest # the newest comic
80
+ xkcd random # a random comic
81
+ xkcd search "woodpecker" # fuzzy search over cached titles, alt text, transcripts and explanations
82
+ xkcd explain 614 # just the explanation, as plain text
83
+ xkcd sync # refresh the archive index and the latest comic
84
+ xkcd sync --full # download every comic and explanation (about an hour, see below)
85
+ xkcd resync # re-fetch comics older than 30 days or still missing an explanation
86
+ xkcd tui # open the browser
87
+ ```
88
+
89
+ Add `--json` to `show`, `latest`, `random`, `search` or `explain` for
90
+ machine-readable output. Nothing but the JSON goes to stdout; errors go to
91
+ stderr with exit code 1, so pipelines stay clean:
92
+
93
+ ```sh
94
+ xkcd show 614 --json | jq .alt
95
+ xkcd search "sandwich" --json | jq '.[].num'
96
+ ```
97
+
98
+ `--no-image` skips the picture. Images are also skipped automatically when
99
+ stdout is not a terminal, so pipes always get clean text.
100
+
101
+ ### The browser
102
+
103
+ `xkcd tui` opens a two-pane browser in the spirit of lazygit: the archive on
104
+ the left, the comic on the right with tabs for alt text, transcript and
105
+ explanation.
106
+
107
+ | Key | Action |
108
+ | ------- | -------------------------------- |
109
+ | `j`/`k` | move through the archive |
110
+ | `/` | filter the archive by title |
111
+ | `Enter` | open the selected comic |
112
+ | `r` | random comic |
113
+ | `l` | latest comic |
114
+ | `i` | show or hide the image pane |
115
+ | `s` | sync the archive index |
116
+ | `?` | key help |
117
+ | `q` | quit |
118
+
119
+ ## Library
120
+
121
+ The async client is the primary API. It is built for I/O bound programs such
122
+ as bots and web services.
123
+
124
+ ```python
125
+ from xkcdkit import AsyncXKCD
126
+
127
+ async with AsyncXKCD() as xk:
128
+ comic = await xk.get(614)
129
+ print(comic.title, comic.alt, comic.img)
130
+ print(comic.explanation_text[:200])
131
+
132
+ latest = await xk.latest()
133
+ hits = await xk.search("sandwich", limit=5)
134
+ await xk.sync() # archive index + latest comic
135
+ ```
136
+
137
+ A blocking twin exists for scripts and the REPL. It runs each call on its own
138
+ event loop, so it cannot be used from inside async code, and it opens and
139
+ closes its connections per call (pass `client_factory=` rather than a client
140
+ if you need to customise HTTP).
141
+
142
+ ```python
143
+ from xkcdkit import XKCD
144
+
145
+ with XKCD() as xk:
146
+ print(xk.random().title)
147
+ ```
148
+
149
+ In a long-running program such as a Discord bot, keep one `AsyncXKCD` open
150
+ for the lifetime of the process: it holds the HTTP connection pool and the
151
+ database connection.
152
+
153
+ ```python
154
+ from xkcdkit import AsyncXKCD, ComicNotFound
155
+
156
+ xk = AsyncXKCD() # created once; call `await xk.close()` on shutdown
157
+
158
+
159
+ @bot.command()
160
+ async def xkcd(ctx, num: int):
161
+ try:
162
+ comic = await xk.get(num)
163
+ except ComicNotFound:
164
+ await ctx.send(f"There is no xkcd {num}.")
165
+ return
166
+ await ctx.send(f"**{comic.title}**\n{comic.img}\n*{comic.alt}*")
167
+ ```
168
+
169
+ ### The `Comic` model
170
+
171
+ `Comic` is a pydantic model with every field from the xkcd JSON (`num`,
172
+ `title`, `safe_title`, `alt`, `img`, `transcript`, `link`, `news`, `year`,
173
+ `month`, `day`) plus:
174
+
175
+ | Field | Meaning |
176
+ | --------------------- | ----------------------------------------------------------- |
177
+ | `explanation` | full explainxkcd page wikitext, or `None` |
178
+ | `explanation_revid` | the wiki revision that was fetched |
179
+ | `explanation_missing` | `True` when explainxkcd had no page at sync time |
180
+ | `synced_at` | when the row was written (UTC) |
181
+ | `explanation_text` | property: the Explanation section as plain text |
182
+ | `date`, `url` | properties: publication date and the comic's page |
183
+
184
+ `comic.model_dump_json()` gives you the same JSON the CLI prints.
185
+
186
+ ## Data, caching and freshness
187
+
188
+ - The database lives in your platform's user data directory
189
+ (`~/.local/share/xkcdkit/xkcd.sqlite3` on Linux, `~/Library/Application
190
+ Support/xkcdkit/` on macOS, `%LOCALAPPDATA%\xkcdkit\` on Windows). Set
191
+ `XKCDKIT_DB` or pass `--db` / `db=` to put it elsewhere.
192
+ - Comics are immutable once stored. A cached comic is served as is, except
193
+ when its explanation was missing more than a day ago; then explainxkcd is
194
+ asked again on the next access.
195
+ - `resync` re-fetches rows older than `--max-age` days (default 30) and every
196
+ row still flagged `explanation_missing`. Recently synced data is trusted.
197
+ - Roughly 1 to 2 percent of comics have no explainxkcd page yet. Those rows
198
+ carry `explanation_missing = True` so your code can decide what to do.
199
+
200
+ ### Being polite to explainxkcd
201
+
202
+ explainxkcd is a volunteer-run wiki. Its robots policy asks for one request
203
+ per second, so a full sync fetches one explanation per second and sends a
204
+ User-Agent that names this project and its repository. A complete first
205
+ `sync --full` therefore takes about an hour. On-demand lookups are not
206
+ delayed.
207
+
208
+ ## Terminal image support
209
+
210
+ | Terminal | Protocol | Status |
211
+ | ---------------------------- | -------- | ------------------------- |
212
+ | kitty, Ghostty | kitty | works |
213
+ | WezTerm, foot, iTerm2 | sixel | works |
214
+ | Windows Terminal 1.22+ | sixel | works |
215
+ | VS Code terminal | sixel | enable `terminal.integrated.enableImages` |
216
+ | tmux | – | falls back to half-block unicode |
217
+ | GNOME Terminal, conhost | – | falls back to half-block unicode |
218
+
219
+ Detection and rendering are done by [textual-image](https://github.com/lnqs/textual-image).
220
+
221
+ ## Development
222
+
223
+ ```sh
224
+ git clone https://github.com/eivl/xkcdkit
225
+ cd xkcdkit
226
+ uv sync --all-groups
227
+ uv run pre-commit install
228
+ uv run pytest
229
+ uv run tox # all supported Python versions
230
+ ```
231
+
232
+ The code base is deliberately small: seven modules, short functions, a
233
+ docstring on everything. ruff (with pydocstyle), mypy strict, flake8 and
234
+ zizmor run in pre-commit and in CI.
235
+
236
+ ### Releasing
237
+
238
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
239
+ On every push to `main`, python-semantic-release reads the commit messages,
240
+ bumps the version in `pyproject.toml`, updates `CHANGELOG.md`, tags the
241
+ release and publishes to PyPI through trusted publishing. `feat:` bumps the
242
+ minor version, `fix:` the patch version, and a `BREAKING CHANGE:` footer the
243
+ major version. The release commit also re-locks `uv.lock` so that
244
+ `uv sync --locked` keeps working after the bump.
245
+
246
+ Trusted publishing has to be registered once on PyPI under
247
+ *Publishing → Add a new pending publisher* with these values:
248
+
249
+ | Field | Value |
250
+ | ---------------- | -------------- |
251
+ | PyPI project | `xkcdkit` |
252
+ | Owner | `eivl` |
253
+ | Repository | `xkcdkit` |
254
+ | Workflow | `release.yml` |
255
+ | Environment | `pypi` |
256
+
257
+ and a GitHub environment named `pypi` must exist in the repository settings.
258
+
259
+ ## License
260
+
261
+ MIT
@@ -0,0 +1,158 @@
1
+ [project]
2
+ name = "xkcdkit"
3
+ version = "0.1.0"
4
+ description = "xkcd comics as a Python library, a CLI and a TUI, with a local cache and explainxkcd explanations."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ keywords = [
9
+ "xkcd",
10
+ "comics",
11
+ "cli",
12
+ "tui",
13
+ "textual",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Programming Language :: Python :: 3.15",
25
+ "Topic :: Terminals",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "httpx2>=2.16",
30
+ "pydantic>=2.11",
31
+ "typer>=0.17",
32
+ "textual>=6.0",
33
+ "textual-image>=0.14",
34
+ "rapidfuzz>=3.13",
35
+ "platformdirs>=4.3",
36
+ ]
37
+
38
+ [[project.authors]]
39
+ name = "Eivind Teig"
40
+ email = "eivind@dchub.no"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/eivl/xkcdkit"
44
+ Repository = "https://github.com/eivl/xkcdkit"
45
+ Changelog = "https://github.com/eivl/xkcdkit/blob/main/CHANGELOG.md"
46
+
47
+ [project.scripts]
48
+ xkcd = "xkcdkit.cli:app"
49
+
50
+ [dependency-groups]
51
+ dev = [
52
+ "pytest>=9.0",
53
+ "pytest-asyncio>=1.2",
54
+ "coverage[toml]>=7.10",
55
+ "ruff>=0.14",
56
+ "mypy>=1.18",
57
+ "flake8>=7.3",
58
+ "tox>=4.30",
59
+ "tox-uv>=1.28",
60
+ "pre-commit>=4.3",
61
+ "zizmor>=1.20",
62
+ ]
63
+
64
+ [build-system]
65
+ requires = ["uv_build>=0.13.4,<0.14.0"]
66
+ build-backend = "uv_build"
67
+
68
+ [tool.pytest.ini_options]
69
+ asyncio_mode = "auto"
70
+ asyncio_default_fixture_loop_scope = "function"
71
+ testpaths = ["tests"]
72
+
73
+ [tool.ruff]
74
+ line-length = 110
75
+ target-version = "py312"
76
+ src = [
77
+ "src",
78
+ "tests",
79
+ ]
80
+
81
+ [tool.ruff.lint]
82
+ select = [
83
+ "E",
84
+ "F",
85
+ "W",
86
+ "I",
87
+ "UP",
88
+ "B",
89
+ "D",
90
+ "N",
91
+ "SIM",
92
+ "RUF",
93
+ "ASYNC",
94
+ "PT",
95
+ "S",
96
+ ]
97
+ ignore = [
98
+ "D203",
99
+ "D213",
100
+ "S101",
101
+ "S311",
102
+ ]
103
+
104
+ [tool.ruff.lint.pydocstyle]
105
+ convention = "google"
106
+
107
+ [tool.ruff.lint.per-file-ignores]
108
+ "tests/*" = [
109
+ "S105",
110
+ "S106",
111
+ ]
112
+
113
+ [tool.mypy]
114
+ python_version = "3.12"
115
+ strict = true
116
+ files = [
117
+ "src",
118
+ "tests",
119
+ ]
120
+ plugins = ["pydantic.mypy"]
121
+
122
+ [[tool.mypy.overrides]]
123
+ module = [
124
+ "textual_image.*",
125
+ "rapidfuzz.*",
126
+ ]
127
+ ignore_missing_imports = true
128
+
129
+ [tool.coverage.run]
130
+ source = ["xkcdkit"]
131
+ branch = true
132
+
133
+ [tool.coverage.report]
134
+ fail_under = 95
135
+ show_missing = true
136
+ exclude_also = [
137
+ "if TYPE_CHECKING:",
138
+ "pragma: no cover",
139
+ ]
140
+
141
+ [tool.semantic_release]
142
+ version_toml = ["pyproject.toml:project.version"]
143
+ commit_parser = "conventional"
144
+ tag_format = "v{version}"
145
+ allow_zero_version = true
146
+ major_on_zero = false
147
+ build_command = "pip install uv && uv lock"
148
+ assets = ["uv.lock"]
149
+ commit_message = "chore(release): {version} [skip ci]"
150
+
151
+ [tool.semantic_release.changelog]
152
+ mode = "update"
153
+
154
+ [tool.semantic_release.changelog.default_templates]
155
+ changelog_file = "CHANGELOG.md"
156
+
157
+ [tool.semantic_release.branches.main]
158
+ match = "main"