h2hdb-opds 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.
- h2hdb_opds-0.1.0/.github/workflows/publish.yml +119 -0
- h2hdb_opds-0.1.0/.gitignore +13 -0
- h2hdb_opds-0.1.0/AGENTS.md +98 -0
- h2hdb_opds-0.1.0/CLAUDE.md +95 -0
- h2hdb_opds-0.1.0/LICENSE +674 -0
- h2hdb_opds-0.1.0/PKG-INFO +896 -0
- h2hdb_opds-0.1.0/README.md +192 -0
- h2hdb_opds-0.1.0/mypy.ini +18 -0
- h2hdb_opds-0.1.0/pyproject.toml +62 -0
- h2hdb_opds-0.1.0/scripts/hooks/finalize-markdown.sh +28 -0
- h2hdb_opds-0.1.0/scripts/hooks/finalize-python.sh +26 -0
- h2hdb_opds-0.1.0/scripts/rebuild-env.sh +15 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/__init__.py +20 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/__main__.py +40 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/acquisition.py +372 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/app.py +283 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/auth.py +129 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/config.py +201 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/language.py +132 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/py.typed +1 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/serialization.py +324 -0
- h2hdb_opds-0.1.0/src/h2hdb_opds/urls.py +21 -0
- h2hdb_opds-0.1.0/tests/__init__.py +0 -0
- h2hdb_opds-0.1.0/tests/conftest.py +20 -0
- h2hdb_opds-0.1.0/tests/fakes.py +240 -0
- h2hdb_opds-0.1.0/tests/http_client.py +19 -0
- h2hdb_opds-0.1.0/tests/test_acquisition_http.py +329 -0
- h2hdb_opds-0.1.0/tests/test_catalog_http.py +376 -0
- h2hdb_opds-0.1.0/tests/test_config_and_startup.py +225 -0
- h2hdb_opds-0.1.0/tests/test_package.py +5 -0
- h2hdb_opds-0.1.0/tests/test_sqlite_vertical_slice.py +127 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- pyproject.toml
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
validate:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
outputs:
|
|
16
|
+
bumped: ${{ steps.version.outputs.bumped }}
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 2
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.14"
|
|
25
|
+
|
|
26
|
+
- name: Install packaging tools
|
|
27
|
+
run: python -m pip install --upgrade packaging build
|
|
28
|
+
|
|
29
|
+
- name: Check version bump
|
|
30
|
+
id: version
|
|
31
|
+
run: |
|
|
32
|
+
python - <<'PY'
|
|
33
|
+
import os
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
import subprocess
|
|
36
|
+
import tomllib
|
|
37
|
+
|
|
38
|
+
from packaging.version import Version
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def project_version(document: str) -> Version:
|
|
42
|
+
return Version(str(tomllib.loads(document)["project"]["version"]))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
current = project_version(
|
|
46
|
+
Path("pyproject.toml").read_text(encoding="utf-8")
|
|
47
|
+
)
|
|
48
|
+
previous_document = subprocess.run(
|
|
49
|
+
["git", "show", "HEAD~1:pyproject.toml"],
|
|
50
|
+
check=False,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
)
|
|
54
|
+
previous = (
|
|
55
|
+
project_version(previous_document.stdout)
|
|
56
|
+
if previous_document.returncode == 0
|
|
57
|
+
else None
|
|
58
|
+
)
|
|
59
|
+
if previous is not None and current < previous:
|
|
60
|
+
raise SystemExit(
|
|
61
|
+
f"project.version decreased from {previous} to {current}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
bumped = previous is None or current > previous
|
|
65
|
+
output_path = Path(os.environ["GITHUB_OUTPUT"])
|
|
66
|
+
with output_path.open("a", encoding="utf-8") as output:
|
|
67
|
+
print(f"current={current}", file=output)
|
|
68
|
+
print(f"previous={previous or ''}", file=output)
|
|
69
|
+
print(f"bumped={'true' if bumped else 'false'}", file=output)
|
|
70
|
+
PY
|
|
71
|
+
|
|
72
|
+
- name: Install project and development dependencies
|
|
73
|
+
run: python -m pip install -e ".[dev]"
|
|
74
|
+
|
|
75
|
+
- name: Check formatting
|
|
76
|
+
run: python -m black --check .
|
|
77
|
+
|
|
78
|
+
- name: Lint
|
|
79
|
+
run: python -m ruff check .
|
|
80
|
+
|
|
81
|
+
- name: Type-check
|
|
82
|
+
run: python -m mypy src tests
|
|
83
|
+
|
|
84
|
+
- name: Test
|
|
85
|
+
run: python -m pytest
|
|
86
|
+
|
|
87
|
+
- name: Build distributions
|
|
88
|
+
run: python -m build
|
|
89
|
+
|
|
90
|
+
- name: Smoke-test wheel
|
|
91
|
+
run: |
|
|
92
|
+
WHEEL="$(find dist -maxdepth 1 -name '*.whl' -print -quit)"
|
|
93
|
+
test -n "$WHEEL"
|
|
94
|
+
python -m pip install --force-reinstall --no-deps "$WHEEL"
|
|
95
|
+
python -c "import h2hdb_opds"
|
|
96
|
+
|
|
97
|
+
- name: Store distributions
|
|
98
|
+
if: steps.version.outputs.bumped == 'true'
|
|
99
|
+
uses: actions/upload-artifact@v4
|
|
100
|
+
with:
|
|
101
|
+
name: distributions
|
|
102
|
+
path: dist/
|
|
103
|
+
if-no-files-found: error
|
|
104
|
+
|
|
105
|
+
publish:
|
|
106
|
+
needs: validate
|
|
107
|
+
if: needs.validate.outputs.bumped == 'true'
|
|
108
|
+
runs-on: ubuntu-latest
|
|
109
|
+
environment: pypi
|
|
110
|
+
permissions:
|
|
111
|
+
contents: read
|
|
112
|
+
id-token: write
|
|
113
|
+
steps:
|
|
114
|
+
- uses: actions/download-artifact@v4
|
|
115
|
+
with:
|
|
116
|
+
name: distributions
|
|
117
|
+
path: dist/
|
|
118
|
+
|
|
119
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Agent Instructions
|
|
2
|
+
|
|
3
|
+
## Project
|
|
4
|
+
|
|
5
|
+
`h2hdb-opds` is the OPDS 2.0 HTTP service for H2HDB catalogs. Python must be
|
|
6
|
+
run through `uv run --no-sync`, using the repository-local virtual environment.
|
|
7
|
+
The supported Python version is defined by `requires-python` in
|
|
8
|
+
`pyproject.toml`.
|
|
9
|
+
|
|
10
|
+
## Ownership Boundary
|
|
11
|
+
|
|
12
|
+
This repository owns:
|
|
13
|
+
|
|
14
|
+
- FastAPI and ASGI integration;
|
|
15
|
+
- OPDS 2.0 models and serialization;
|
|
16
|
+
- navigation, publications, search, and pagination;
|
|
17
|
+
- authentication;
|
|
18
|
+
- acquisition, Range, and conditional HTTP responses.
|
|
19
|
+
|
|
20
|
+
The `h2hdb` core package exclusively owns connectors, transactions, database
|
|
21
|
+
schema and migrations, durable queues, coordination fencing, and catalog
|
|
22
|
+
repositories. Depend only on core public interfaces. Do not import connector or
|
|
23
|
+
repository internals and do not create or migrate schema here. Database access
|
|
24
|
+
is always forced to read-only, and the service exposes only published catalog
|
|
25
|
+
revisions. Production startup uses `open_database()` for its compatibility
|
|
26
|
+
check. Injected readers are checked through their public `check_compatibility()`
|
|
27
|
+
method. No startup path may call `migrate()`.
|
|
28
|
+
|
|
29
|
+
## Module Layout and HTTP Invariants
|
|
30
|
+
|
|
31
|
+
- `config.py` owns the frozen OPDS, server, authentication, and nested core
|
|
32
|
+
configuration models.
|
|
33
|
+
- `app.py` wires the FastAPI lifespan and routes to a compatible public
|
|
34
|
+
`CatalogReader`.
|
|
35
|
+
- `serialization.py` owns OPDS feed, publication, navigation, search template,
|
|
36
|
+
and pagination serialization.
|
|
37
|
+
- `auth.py` owns Basic authentication and the public OPDS authentication
|
|
38
|
+
document. Credential comparisons must use `secrets.compare_digest`.
|
|
39
|
+
- `acquisition.py` owns artifact file responses and HTTP validators/ranges.
|
|
40
|
+
|
|
41
|
+
Feed responses use `application/opds+json`; standalone publications use
|
|
42
|
+
`application/opds-publication+json`. Acquisition ETags are strong SHA-256
|
|
43
|
+
validators from the verified artifact. Apply the RFC precondition order from
|
|
44
|
+
`If-Match` through `If-Modified-Since`. Support only one byte range; invalid,
|
|
45
|
+
multiple, and unsatisfiable byte ranges return 416 with `Content-Range`, unknown
|
|
46
|
+
range units are ignored, and an `If-Range` mismatch returns the complete 200
|
|
47
|
+
representation. An `If-Range` date comparison is exact.
|
|
48
|
+
|
|
49
|
+
All acquisition paths must remain under the configured real artifact root and
|
|
50
|
+
must be opened without following symlinks. Verify size and SHA-256 before sending
|
|
51
|
+
a strong validator. Absolute links come only from the canonical public base URL,
|
|
52
|
+
never the request Host header. Basic authentication requires an effective HTTPS
|
|
53
|
+
request and either local TLS or an explicitly trusted TLS-terminating proxy.
|
|
54
|
+
Catalog listing and counts use core's pinned `require_artifact` filter so every
|
|
55
|
+
serialized OPDS Publication has an acquisition link.
|
|
56
|
+
|
|
57
|
+
All feed, publication, pagination, and acquisition links carry their selected
|
|
58
|
+
revision. Resolve an omitted revision to the current snapshot and an explicit
|
|
59
|
+
revision through core's public revision-history lookup. Map a missing revision
|
|
60
|
+
to an explicit 404, and pass the resolved `CatalogRevision` into every list,
|
|
61
|
+
publication, and artifact read. Never fall back from a missing requested
|
|
62
|
+
revision to current data or mix snapshots across pagination.
|
|
63
|
+
|
|
64
|
+
## Environment and Commands
|
|
65
|
+
|
|
66
|
+
This is a standalone repository, not a uv workspace. `uv.lock` is ignored and
|
|
67
|
+
must not become a build, test, or runtime input.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv venv --python 3.14
|
|
71
|
+
uv pip install -e ".[dev]"
|
|
72
|
+
uv run --no-sync ruff check .
|
|
73
|
+
uv run --no-sync black --check .
|
|
74
|
+
uv run --no-sync mypy src tests
|
|
75
|
+
uv run --no-sync pytest
|
|
76
|
+
uv run --no-sync python -m build
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If the environment is damaged, run `scripts/rebuild-env.sh`.
|
|
80
|
+
|
|
81
|
+
## Shared Finalization
|
|
82
|
+
|
|
83
|
+
The repository keeps provider-neutral hooks in `scripts/hooks/`.
|
|
84
|
+
|
|
85
|
+
- After changing Python, run `bash scripts/hooks/finalize-python.sh`.
|
|
86
|
+
- After changing Markdown, run `bash scripts/hooks/finalize-markdown.sh`.
|
|
87
|
+
|
|
88
|
+
Do not create agent-specific copies of these implementations.
|
|
89
|
+
|
|
90
|
+
## Design and Compatibility
|
|
91
|
+
|
|
92
|
+
The project is pre-1.0. Prefer clear responsibility boundaries and the cleanest
|
|
93
|
+
end state over compatibility shims or deprecated aliases. Follow SOLID
|
|
94
|
+
principles. Keep authentication, file I/O, and response streaming separate from
|
|
95
|
+
catalog query construction and never perform schema mutation at service startup.
|
|
96
|
+
|
|
97
|
+
`CLAUDE.md` documents the same repository rules. Keep both files synchronized
|
|
98
|
+
when changing workflow, ownership, testing, or tooling conventions.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides repository guidance to Claude Code and other coding agents.
|
|
4
|
+
|
|
5
|
+
## Communication
|
|
6
|
+
|
|
7
|
+
- Claude 必須以繁體中文回答所有對話內容;程式碼、指令、檔名與專有名詞維持原文。
|
|
8
|
+
|
|
9
|
+
## Project
|
|
10
|
+
|
|
11
|
+
`h2hdb-opds` exposes an H2HDB catalog through an OPDS 2.0 HTTP service. It owns
|
|
12
|
+
FastAPI and ASGI integration, OPDS models and serialization, navigation,
|
|
13
|
+
publications, search, pagination, authentication, and acquisition responses
|
|
14
|
+
including Range and conditional HTTP behavior.
|
|
15
|
+
|
|
16
|
+
The `h2hdb` core package exclusively owns database connectors, transactions,
|
|
17
|
+
schema migrations, durable queues, coordination fencing, and catalog
|
|
18
|
+
repositories. Use only core public interfaces. Never import connector or
|
|
19
|
+
repository internals, create or migrate schema, or expose unpublished catalog
|
|
20
|
+
state. Database access is always forced to read-only. Production startup calls
|
|
21
|
+
the public `open_database()` compatibility path; injected readers call only
|
|
22
|
+
`check_compatibility()`. No startup path may call `migrate()`.
|
|
23
|
+
|
|
24
|
+
## Module Layout and HTTP Invariants
|
|
25
|
+
|
|
26
|
+
- `config.py` owns the frozen OPDS, server, authentication, and nested core
|
|
27
|
+
configuration models.
|
|
28
|
+
- `app.py` wires the FastAPI lifespan and routes to a compatible public
|
|
29
|
+
`CatalogReader`.
|
|
30
|
+
- `serialization.py` owns OPDS feed, publication, navigation, search template,
|
|
31
|
+
and pagination serialization.
|
|
32
|
+
- `auth.py` owns Basic authentication and the public OPDS authentication
|
|
33
|
+
document. Credential comparisons must use `secrets.compare_digest`.
|
|
34
|
+
- `acquisition.py` owns artifact file responses and HTTP validators/ranges.
|
|
35
|
+
|
|
36
|
+
Feed responses use `application/opds+json`; standalone publications use
|
|
37
|
+
`application/opds-publication+json`. Acquisition ETags are strong SHA-256
|
|
38
|
+
validators from the verified artifact. Apply the RFC precondition order from
|
|
39
|
+
`If-Match` through `If-Modified-Since`. Support only one byte range; invalid,
|
|
40
|
+
multiple, and unsatisfiable byte ranges return 416 with `Content-Range`, unknown
|
|
41
|
+
range units are ignored, and an `If-Range` mismatch returns the complete 200
|
|
42
|
+
representation. An `If-Range` date comparison is exact.
|
|
43
|
+
|
|
44
|
+
All acquisition paths must remain under the configured real artifact root and
|
|
45
|
+
must be opened without following symlinks. Verify size and SHA-256 before sending
|
|
46
|
+
a strong validator. Absolute links come only from the canonical public base URL,
|
|
47
|
+
never the request Host header. Basic authentication requires an effective HTTPS
|
|
48
|
+
request and either local TLS or an explicitly trusted TLS-terminating proxy.
|
|
49
|
+
Catalog listing and counts use core's pinned `require_artifact` filter so every
|
|
50
|
+
serialized OPDS Publication has an acquisition link.
|
|
51
|
+
|
|
52
|
+
All feed, publication, pagination, and acquisition links carry their selected
|
|
53
|
+
revision. Resolve an omitted revision to the current snapshot and an explicit
|
|
54
|
+
revision through core's public revision-history lookup. Map a missing revision
|
|
55
|
+
to an explicit 404, and pass the resolved `CatalogRevision` into every list,
|
|
56
|
+
publication, and artifact read. Never fall back from a missing requested
|
|
57
|
+
revision to current data or mix snapshots across pagination.
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
This is an independent repository and not part of a uv workspace. `uv.lock` is
|
|
62
|
+
ignored and must not be used by build, test, or runtime workflows. Always run
|
|
63
|
+
Python through `uv run --no-sync` after installing the editable environment.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv venv --python 3.14
|
|
67
|
+
uv pip install -e ".[dev]"
|
|
68
|
+
uv run --no-sync ruff check .
|
|
69
|
+
uv run --no-sync black --check .
|
|
70
|
+
uv run --no-sync mypy src tests
|
|
71
|
+
uv run --no-sync pytest
|
|
72
|
+
uv run --no-sync python -m build
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use `scripts/rebuild-env.sh` to recreate a damaged environment.
|
|
76
|
+
|
|
77
|
+
## Shared Finalization
|
|
78
|
+
|
|
79
|
+
Provider-neutral hooks live in `scripts/hooks/` and are shared by humans and
|
|
80
|
+
all coding agents.
|
|
81
|
+
|
|
82
|
+
- After Python changes, run `bash scripts/hooks/finalize-python.sh`.
|
|
83
|
+
- After Markdown changes, run `bash scripts/hooks/finalize-markdown.sh`.
|
|
84
|
+
|
|
85
|
+
Do not duplicate their implementation under an agent-specific directory.
|
|
86
|
+
|
|
87
|
+
## Design
|
|
88
|
+
|
|
89
|
+
This project is pre-1.0. Prefer the cleanest architecture over compatibility
|
|
90
|
+
shims and deprecated aliases. Follow SOLID principles. Keep authentication,
|
|
91
|
+
file I/O, and response streaming separate from catalog query construction, and
|
|
92
|
+
never perform schema mutation during service startup.
|
|
93
|
+
|
|
94
|
+
Keep this document synchronized with `AGENTS.md` whenever ownership, workflow,
|
|
95
|
+
testing, or tooling conventions change.
|