crawl4tools 1.0.0b1__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.
- crawl4tools-1.0.0b1/.dockerignore +28 -0
- crawl4tools-1.0.0b1/.github/workflows/ci.yml +122 -0
- crawl4tools-1.0.0b1/.github/workflows/release.yml +340 -0
- crawl4tools-1.0.0b1/.gitignore +36 -0
- crawl4tools-1.0.0b1/.python-version +1 -0
- crawl4tools-1.0.0b1/Changelog.md +92 -0
- crawl4tools-1.0.0b1/Dockerfile +65 -0
- crawl4tools-1.0.0b1/LICENSE +202 -0
- crawl4tools-1.0.0b1/NOTICE +4 -0
- crawl4tools-1.0.0b1/PKG-INFO +327 -0
- crawl4tools-1.0.0b1/README.md +295 -0
- crawl4tools-1.0.0b1/README_ja.md +295 -0
- crawl4tools-1.0.0b1/compose.yaml +43 -0
- crawl4tools-1.0.0b1/pyproject.toml +85 -0
- crawl4tools-1.0.0b1/src/crawl4tools/__init__.py +10 -0
- crawl4tools-1.0.0b1/src/crawl4tools/cli/__init__.py +1 -0
- crawl4tools-1.0.0b1/src/crawl4tools/cli/main.py +394 -0
- crawl4tools-1.0.0b1/src/crawl4tools/cli/output.py +71 -0
- crawl4tools-1.0.0b1/src/crawl4tools/cli/report.py +48 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/__init__.py +100 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/classify.py +152 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/errors.py +266 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/fetcher.py +620 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/interception.py +216 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/models.py +116 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/naming.py +197 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/pdf.py +53 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/probe.py +149 -0
- crawl4tools-1.0.0b1/src/crawl4tools/engine/proxy.py +123 -0
- crawl4tools-1.0.0b1/src/crawl4tools/i18n.py +176 -0
- crawl4tools-1.0.0b1/src/crawl4tools/locale/ja/LC_MESSAGES/crawl4tools.mo +0 -0
- crawl4tools-1.0.0b1/src/crawl4tools/locale/ja/LC_MESSAGES/crawl4tools.po +536 -0
- crawl4tools-1.0.0b1/src/crawl4tools/py.typed +0 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/__init__.py +28 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/cli_options.py +262 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/config.py +91 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/host.py +284 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/loader.py +239 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/mcp_main.py +230 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/mcp_server.py +445 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/results.py +258 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/server_main.py +360 -0
- crawl4tools-1.0.0b1/src/crawl4tools/server/settings.py +126 -0
- crawl4tools-1.0.0b1/tests/conftest.py +195 -0
- crawl4tools-1.0.0b1/tests/fixtures/make_sample_pdf.py +41 -0
- crawl4tools-1.0.0b1/tests/fixtures/sample.pdf +32 -0
- crawl4tools-1.0.0b1/tests/integration/test_cli_live.py +106 -0
- crawl4tools-1.0.0b1/tests/integration/test_mcp_live.py +121 -0
- crawl4tools-1.0.0b1/tests/integration/test_server_live.py +176 -0
- crawl4tools-1.0.0b1/tests/unit/test_classify.py +227 -0
- crawl4tools-1.0.0b1/tests/unit/test_cli_main.py +579 -0
- crawl4tools-1.0.0b1/tests/unit/test_cli_options.py +284 -0
- crawl4tools-1.0.0b1/tests/unit/test_cli_output.py +183 -0
- crawl4tools-1.0.0b1/tests/unit/test_cli_report.py +127 -0
- crawl4tools-1.0.0b1/tests/unit/test_errors.py +142 -0
- crawl4tools-1.0.0b1/tests/unit/test_errors_i18n.py +495 -0
- crawl4tools-1.0.0b1/tests/unit/test_fetcher.py +1289 -0
- crawl4tools-1.0.0b1/tests/unit/test_host.py +531 -0
- crawl4tools-1.0.0b1/tests/unit/test_i18n.py +419 -0
- crawl4tools-1.0.0b1/tests/unit/test_interception.py +320 -0
- crawl4tools-1.0.0b1/tests/unit/test_loader.py +462 -0
- crawl4tools-1.0.0b1/tests/unit/test_mcp_main.py +567 -0
- crawl4tools-1.0.0b1/tests/unit/test_mcp_server.py +945 -0
- crawl4tools-1.0.0b1/tests/unit/test_models.py +94 -0
- crawl4tools-1.0.0b1/tests/unit/test_naming.py +213 -0
- crawl4tools-1.0.0b1/tests/unit/test_pdf.py +37 -0
- crawl4tools-1.0.0b1/tests/unit/test_probe.py +230 -0
- crawl4tools-1.0.0b1/tests/unit/test_proxy.py +221 -0
- crawl4tools-1.0.0b1/tests/unit/test_server_config.py +186 -0
- crawl4tools-1.0.0b1/tests/unit/test_server_main.py +621 -0
- crawl4tools-1.0.0b1/tests/unit/test_server_results.py +691 -0
- crawl4tools-1.0.0b1/tests/unit/test_server_settings.py +185 -0
- crawl4tools-1.0.0b1/tests/unit/test_version.py +11 -0
- crawl4tools-1.0.0b1/tests/unit/test_workflows.py +427 -0
- crawl4tools-1.0.0b1/tools/check.sh +50 -0
- crawl4tools-1.0.0b1/tools/docker_smoke.sh +176 -0
- crawl4tools-1.0.0b1/tools/i18n.sh +62 -0
- crawl4tools-1.0.0b1/tools/release_check.sh +353 -0
- crawl4tools-1.0.0b1/tools/wheel_check.sh +67 -0
- crawl4tools-1.0.0b1/uv.lock +4037 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Keep the build context small and avoid leaking local/dev-only files into
|
|
2
|
+
# the image. README.md, LICENSE, NOTICE, pyproject.toml, uv.lock and src/
|
|
3
|
+
# are intentionally NOT listed here: hatchling's wheel build needs them.
|
|
4
|
+
|
|
5
|
+
.git
|
|
6
|
+
.gitignore
|
|
7
|
+
.venv
|
|
8
|
+
venv
|
|
9
|
+
**/__pycache__
|
|
10
|
+
**/*.py[cod]
|
|
11
|
+
.mypy_cache
|
|
12
|
+
.pytest_cache
|
|
13
|
+
.ruff_cache
|
|
14
|
+
.crawl4ai
|
|
15
|
+
dist
|
|
16
|
+
build
|
|
17
|
+
*.egg-info
|
|
18
|
+
tests
|
|
19
|
+
tools
|
|
20
|
+
downloads
|
|
21
|
+
.env
|
|
22
|
+
.env.*
|
|
23
|
+
PROJECT.md
|
|
24
|
+
開発資料
|
|
25
|
+
.patent-checker
|
|
26
|
+
.claude
|
|
27
|
+
Dockerfile
|
|
28
|
+
compose.yaml
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, "dev/**"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
defaults:
|
|
17
|
+
run:
|
|
18
|
+
# Explicit bash runs every script with -e and -o pipefail.
|
|
19
|
+
shell: bash
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
check:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 20
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
|
+
# setup-uv publishes full version tags only (no floating v10 tag).
|
|
28
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
29
|
+
with:
|
|
30
|
+
version: "0.12.15"
|
|
31
|
+
enable-cache: true
|
|
32
|
+
python-version: "3.12"
|
|
33
|
+
- name: Lock, shellcheck, lint, format, type check and unit tests
|
|
34
|
+
run: tools/check.sh
|
|
35
|
+
|
|
36
|
+
py311:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
timeout-minutes: 15
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v7
|
|
41
|
+
# python-version sets UV_PYTHON for the job, which beats .python-version (3.12).
|
|
42
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
43
|
+
with:
|
|
44
|
+
version: "0.12.15"
|
|
45
|
+
enable-cache: true
|
|
46
|
+
python-version: "3.11"
|
|
47
|
+
- name: Show the interpreter (must be 3.11)
|
|
48
|
+
run: |
|
|
49
|
+
uv run --python 3.11 python --version
|
|
50
|
+
uv run --python 3.11 python -c 'import sys; assert sys.version_info[:2] == (3, 11), sys.version'
|
|
51
|
+
# Unit tests only: mypy targets 3.12 and runs in the check job.
|
|
52
|
+
- name: Unit tests on Python 3.11
|
|
53
|
+
run: uv run --python 3.11 pytest -q tests/unit
|
|
54
|
+
|
|
55
|
+
package:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
timeout-minutes: 20
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v7
|
|
60
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
61
|
+
with:
|
|
62
|
+
version: "0.12.15"
|
|
63
|
+
enable-cache: true
|
|
64
|
+
python-version: "3.12"
|
|
65
|
+
- name: Build the sdist and wheel
|
|
66
|
+
run: uv build
|
|
67
|
+
- name: Check the wheel's contents
|
|
68
|
+
run: tools/wheel_check.sh dist/crawl4tools-*.whl
|
|
69
|
+
- name: Install the wheel as a uv tool
|
|
70
|
+
run: |
|
|
71
|
+
uv tool install --with-executables-from playwright ./dist/crawl4tools-*.whl
|
|
72
|
+
# Put the tool's commands on PATH for the next steps.
|
|
73
|
+
uv tool dir --bin >> "$GITHUB_PATH"
|
|
74
|
+
- name: Check each command's --version line
|
|
75
|
+
run: |
|
|
76
|
+
version="$(uv version --short)"
|
|
77
|
+
status=0
|
|
78
|
+
for cmd in crawl4cli crawl4mcp crawl4server; do
|
|
79
|
+
if ! output="$("$cmd" --version)"; then
|
|
80
|
+
echo "::error::$cmd --version failed"
|
|
81
|
+
status=1
|
|
82
|
+
continue
|
|
83
|
+
fi
|
|
84
|
+
first="${output%%$'\n'*}"
|
|
85
|
+
if [[ "$first" == "$cmd $version "* ]]; then
|
|
86
|
+
echo "ok: $first"
|
|
87
|
+
else
|
|
88
|
+
echo "::error::$cmd --version printed '$first'; expected it to start with '$cmd $version '"
|
|
89
|
+
status=1
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
exit "$status"
|
|
93
|
+
|
|
94
|
+
docker:
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
timeout-minutes: 30
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v7
|
|
99
|
+
- uses: docker/setup-buildx-action@v4
|
|
100
|
+
- name: Build the image for linux/amd64
|
|
101
|
+
uses: docker/build-push-action@v7
|
|
102
|
+
with:
|
|
103
|
+
context: .
|
|
104
|
+
platforms: linux/amd64
|
|
105
|
+
# Load into the local daemon so the smoke test can run it.
|
|
106
|
+
load: true
|
|
107
|
+
tags: crawl4tools:ci
|
|
108
|
+
cache-from: type=gha
|
|
109
|
+
cache-to: type=gha,mode=max
|
|
110
|
+
- name: Validate compose.yaml
|
|
111
|
+
run: docker compose config -q
|
|
112
|
+
- name: Smoke-test the container
|
|
113
|
+
run: tools/docker_smoke.sh crawl4tools:ci
|
|
114
|
+
|
|
115
|
+
actionlint:
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
timeout-minutes: 10
|
|
118
|
+
steps:
|
|
119
|
+
- uses: actions/checkout@v7
|
|
120
|
+
# The image bundles shellcheck, so run: scripts are checked as well.
|
|
121
|
+
- name: Lint the workflows
|
|
122
|
+
run: docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:1.7.12 -color
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing a GitHub Release whose tag is v<version in pyproject.toml> puts that
|
|
4
|
+
# version on PyPI (Trusted Publishing), on Docker Hub as xhighhongo41/crawl4tools
|
|
5
|
+
# (linux/amd64 + linux/arm64, each built on its own native runner) and attaches
|
|
6
|
+
# the sdist and wheel to the release.
|
|
7
|
+
#
|
|
8
|
+
# Re-running a failed job is safe: PyPI skips files it already has, images are
|
|
9
|
+
# pushed by digest, the manifest job re-points the same tags, and the assets are
|
|
10
|
+
# uploaded with --clobber. Re-run only the failed jobs with
|
|
11
|
+
# gh run rerun <run id> --failed
|
|
12
|
+
#
|
|
13
|
+
# A manual run is a dry run by default: it builds the packages and both images
|
|
14
|
+
# and logs in to Docker Hub, but publishes nothing. A manual run with
|
|
15
|
+
# dry_run=false publishes (to recover from a broken release run) and must run on
|
|
16
|
+
# the tag: gh workflow run release.yml --ref v<version> -f dry_run=false
|
|
17
|
+
|
|
18
|
+
on:
|
|
19
|
+
release:
|
|
20
|
+
types: [published]
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
inputs:
|
|
23
|
+
dry_run:
|
|
24
|
+
description: "Dry run: build the packages and images, but publish nothing"
|
|
25
|
+
type: boolean
|
|
26
|
+
default: true
|
|
27
|
+
|
|
28
|
+
concurrency:
|
|
29
|
+
group: release-${{ github.ref }}
|
|
30
|
+
cancel-in-progress: false
|
|
31
|
+
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
|
|
35
|
+
defaults:
|
|
36
|
+
run:
|
|
37
|
+
# Explicit bash runs every script with -e and -o pipefail.
|
|
38
|
+
shell: bash
|
|
39
|
+
|
|
40
|
+
env:
|
|
41
|
+
IMAGE: xhighhongo41/crawl4tools
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
verify:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
timeout-minutes: 10
|
|
47
|
+
outputs:
|
|
48
|
+
version: ${{ steps.check.outputs.version }}
|
|
49
|
+
tag: ${{ steps.check.outputs.tag }}
|
|
50
|
+
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
|
|
51
|
+
dry_run: ${{ steps.check.outputs.dry_run }}
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v7
|
|
54
|
+
# setup-uv publishes full version tags only (no floating v10 tag). No cache
|
|
55
|
+
# in this workflow: what gets published is built from fresh downloads only.
|
|
56
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
57
|
+
with:
|
|
58
|
+
version: "0.12.15"
|
|
59
|
+
enable-cache: false
|
|
60
|
+
python-version: "3.12"
|
|
61
|
+
- name: Check the version against the release
|
|
62
|
+
id: check
|
|
63
|
+
# Event data reaches the script through env only, never pasted into it.
|
|
64
|
+
env:
|
|
65
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
66
|
+
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
|
|
67
|
+
DRY_RUN_INPUT: ${{ inputs.dry_run }}
|
|
68
|
+
run: |
|
|
69
|
+
version="$(uv version --short)"
|
|
70
|
+
# The version must be in PEP 440 normal form, or the wheel's name
|
|
71
|
+
# (normalized by the build) would not match the tag.
|
|
72
|
+
is_prerelease="$(uv run --no-project --with packaging python -c '
|
|
73
|
+
import sys
|
|
74
|
+
|
|
75
|
+
from packaging.version import InvalidVersion, Version
|
|
76
|
+
|
|
77
|
+
text = sys.argv[1]
|
|
78
|
+
try:
|
|
79
|
+
version = Version(text)
|
|
80
|
+
except InvalidVersion:
|
|
81
|
+
sys.exit(f"::error::{text!r} in pyproject.toml is not a PEP 440 version")
|
|
82
|
+
if str(version) != text:
|
|
83
|
+
sys.exit(f"::error::Write the version in PEP 440 normal form: {version} (not {text})")
|
|
84
|
+
if version.local is not None or version.epoch != 0:
|
|
85
|
+
sys.exit(f"::error::{text} has a local part or an epoch, which a Docker tag cannot hold")
|
|
86
|
+
print("true" if version.is_prerelease else "false")
|
|
87
|
+
' "$version")"
|
|
88
|
+
tag="v$version"
|
|
89
|
+
|
|
90
|
+
dry_run=false
|
|
91
|
+
if [[ "$GITHUB_EVENT_NAME" == workflow_dispatch && "$DRY_RUN_INPUT" == true ]]; then
|
|
92
|
+
dry_run=true
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
case "$GITHUB_EVENT_NAME" in
|
|
96
|
+
release)
|
|
97
|
+
if [[ -z "$RELEASE_TAG" || ! "$RELEASE_PRERELEASE" =~ ^(true|false)$ ]]; then
|
|
98
|
+
echo "::error::The release event carries no tag_name or prerelease field (tag '$RELEASE_TAG', prerelease '$RELEASE_PRERELEASE')"
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
if [[ "$RELEASE_TAG" != "$tag" ]]; then
|
|
102
|
+
echo "::error::The release's tag is $RELEASE_TAG, but pyproject.toml's version needs the tag $tag"
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
if [[ "$is_prerelease" == true && "$RELEASE_PRERELEASE" != true ]]; then
|
|
106
|
+
# A re-run replays this same event, so it cannot pick up the edit.
|
|
107
|
+
echo "::error::$version is a pre-release; mark the GitHub Release as a pre-release, then run this workflow on $tag with dry_run=false"
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
;;
|
|
111
|
+
workflow_dispatch)
|
|
112
|
+
# A manual real publish runs on the tag, never on an arbitrary branch.
|
|
113
|
+
if [[ "$dry_run" == false && "$GITHUB_REF" != "refs/tags/$tag" ]]; then
|
|
114
|
+
echo "::error::A manual publish must run on the tag $tag (this run is on $GITHUB_REF); use --ref $tag, or run a dry run"
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
echo "::error::Unexpected event: $GITHUB_EVENT_NAME"
|
|
120
|
+
exit 1
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
echo "version=$version"
|
|
126
|
+
echo "tag=$tag"
|
|
127
|
+
echo "is_prerelease=$is_prerelease"
|
|
128
|
+
echo "dry_run=$dry_run"
|
|
129
|
+
} >>"$GITHUB_OUTPUT"
|
|
130
|
+
echo "version $version, tag $tag, pre-release $is_prerelease, dry run $dry_run"
|
|
131
|
+
|
|
132
|
+
build:
|
|
133
|
+
needs: verify
|
|
134
|
+
runs-on: ubuntu-latest
|
|
135
|
+
timeout-minutes: 15
|
|
136
|
+
steps:
|
|
137
|
+
- uses: actions/checkout@v7
|
|
138
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
139
|
+
with:
|
|
140
|
+
version: "0.12.15"
|
|
141
|
+
enable-cache: false
|
|
142
|
+
python-version: "3.12"
|
|
143
|
+
- name: Build the sdist and wheel
|
|
144
|
+
run: uv build
|
|
145
|
+
- name: Check the wheel's contents
|
|
146
|
+
run: tools/wheel_check.sh dist/crawl4tools-*.whl
|
|
147
|
+
- uses: actions/upload-artifact@v7
|
|
148
|
+
with:
|
|
149
|
+
name: dist
|
|
150
|
+
path: dist/
|
|
151
|
+
if-no-files-found: error
|
|
152
|
+
|
|
153
|
+
pypi:
|
|
154
|
+
needs: [verify, build]
|
|
155
|
+
if: needs.verify.outputs.dry_run != 'true'
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
timeout-minutes: 10
|
|
158
|
+
environment:
|
|
159
|
+
name: pypi
|
|
160
|
+
url: https://pypi.org/project/crawl4tools/${{ needs.verify.outputs.version }}/
|
|
161
|
+
# Trusted Publishing: the OIDC token is the only credential, and only this
|
|
162
|
+
# job may request one.
|
|
163
|
+
permissions:
|
|
164
|
+
id-token: write
|
|
165
|
+
steps:
|
|
166
|
+
- uses: actions/download-artifact@v8
|
|
167
|
+
with:
|
|
168
|
+
name: dist
|
|
169
|
+
path: dist/
|
|
170
|
+
# Generates and uploads PEP 740 attestations by default.
|
|
171
|
+
- name: Publish to PyPI
|
|
172
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
173
|
+
with:
|
|
174
|
+
skip-existing: true
|
|
175
|
+
|
|
176
|
+
docker:
|
|
177
|
+
needs: verify
|
|
178
|
+
strategy:
|
|
179
|
+
fail-fast: false
|
|
180
|
+
matrix:
|
|
181
|
+
include:
|
|
182
|
+
- platform: linux/amd64
|
|
183
|
+
runner: ubuntu-latest
|
|
184
|
+
- platform: linux/arm64
|
|
185
|
+
runner: ubuntu-24.04-arm
|
|
186
|
+
runs-on: ${{ matrix.runner }}
|
|
187
|
+
timeout-minutes: 45
|
|
188
|
+
steps:
|
|
189
|
+
- name: Name the platform for artifact names
|
|
190
|
+
env:
|
|
191
|
+
PLATFORM: ${{ matrix.platform }}
|
|
192
|
+
run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >>"$GITHUB_ENV"
|
|
193
|
+
- uses: actions/checkout@v7
|
|
194
|
+
- uses: docker/setup-buildx-action@v4
|
|
195
|
+
# Also in a dry run, to prove the credentials work before a real release.
|
|
196
|
+
- name: Log in to Docker Hub
|
|
197
|
+
uses: docker/login-action@v4
|
|
198
|
+
with:
|
|
199
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
200
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
201
|
+
# Labels only; the tags are set by the manifest job.
|
|
202
|
+
- name: Image labels
|
|
203
|
+
id: meta
|
|
204
|
+
uses: docker/metadata-action@v6
|
|
205
|
+
with:
|
|
206
|
+
images: ${{ env.IMAGE }}
|
|
207
|
+
labels: |
|
|
208
|
+
org.opencontainers.image.version=${{ needs.verify.outputs.version }}
|
|
209
|
+
org.opencontainers.image.licenses=Apache-2.0
|
|
210
|
+
# A push by digest and a plain build cannot share one step, so each run
|
|
211
|
+
# takes exactly one of the two steps below.
|
|
212
|
+
- name: Build and push by digest
|
|
213
|
+
id: build
|
|
214
|
+
if: needs.verify.outputs.dry_run != 'true'
|
|
215
|
+
uses: docker/build-push-action@v7
|
|
216
|
+
with:
|
|
217
|
+
context: .
|
|
218
|
+
platforms: ${{ matrix.platform }}
|
|
219
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
220
|
+
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
|
221
|
+
- name: Build without pushing (dry run)
|
|
222
|
+
if: needs.verify.outputs.dry_run == 'true'
|
|
223
|
+
uses: docker/build-push-action@v7
|
|
224
|
+
with:
|
|
225
|
+
context: .
|
|
226
|
+
platforms: ${{ matrix.platform }}
|
|
227
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
228
|
+
push: false
|
|
229
|
+
- name: Export the digest
|
|
230
|
+
if: needs.verify.outputs.dry_run != 'true'
|
|
231
|
+
env:
|
|
232
|
+
DIGEST: ${{ steps.build.outputs.digest }}
|
|
233
|
+
run: |
|
|
234
|
+
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
|
235
|
+
echo "::error::The build reported no image digest (got '$DIGEST')"
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
mkdir -p "$RUNNER_TEMP/digests"
|
|
239
|
+
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"
|
|
240
|
+
# One artifact per platform, so the two matrix jobs never collide.
|
|
241
|
+
- name: Upload the digest
|
|
242
|
+
if: needs.verify.outputs.dry_run != 'true'
|
|
243
|
+
uses: actions/upload-artifact@v7
|
|
244
|
+
with:
|
|
245
|
+
name: digests-${{ env.PLATFORM_PAIR }}
|
|
246
|
+
path: ${{ runner.temp }}/digests/*
|
|
247
|
+
if-no-files-found: error
|
|
248
|
+
retention-days: 1
|
|
249
|
+
|
|
250
|
+
manifest:
|
|
251
|
+
needs: [verify, docker]
|
|
252
|
+
if: needs.verify.outputs.dry_run != 'true'
|
|
253
|
+
runs-on: ubuntu-latest
|
|
254
|
+
timeout-minutes: 10
|
|
255
|
+
steps:
|
|
256
|
+
- name: Download the digests
|
|
257
|
+
uses: actions/download-artifact@v8
|
|
258
|
+
with:
|
|
259
|
+
path: ${{ runner.temp }}/digests
|
|
260
|
+
pattern: digests-*
|
|
261
|
+
merge-multiple: true
|
|
262
|
+
- uses: docker/setup-buildx-action@v4
|
|
263
|
+
- name: Log in to Docker Hub
|
|
264
|
+
uses: docker/login-action@v4
|
|
265
|
+
with:
|
|
266
|
+
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
267
|
+
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
268
|
+
# The version tag always; latest only for a final (not pre-release) version.
|
|
269
|
+
# flavor latest=false keeps the action from adding latest on its own.
|
|
270
|
+
- name: Image tags
|
|
271
|
+
id: meta
|
|
272
|
+
uses: docker/metadata-action@v6
|
|
273
|
+
with:
|
|
274
|
+
images: ${{ env.IMAGE }}
|
|
275
|
+
flavor: latest=false
|
|
276
|
+
tags: |
|
|
277
|
+
type=raw,value=${{ needs.verify.outputs.version }}
|
|
278
|
+
type=raw,value=latest,enable=${{ needs.verify.outputs.is_prerelease == 'false' }}
|
|
279
|
+
- name: Create the multi-platform image
|
|
280
|
+
working-directory: ${{ runner.temp }}/digests
|
|
281
|
+
run: |
|
|
282
|
+
mapfile -t tags < <(jq -r '.tags[]' <<<"$DOCKER_METADATA_OUTPUT_JSON")
|
|
283
|
+
if ((${#tags[@]} == 0)); then
|
|
284
|
+
echo "::error::docker/metadata-action produced no tags"
|
|
285
|
+
exit 1
|
|
286
|
+
fi
|
|
287
|
+
shopt -s nullglob
|
|
288
|
+
digests=(*)
|
|
289
|
+
# One digest per platform of the docker job's matrix.
|
|
290
|
+
if ((${#digests[@]} != 2)); then
|
|
291
|
+
echo "::error::Expected 2 image digests (amd64, arm64), found ${#digests[@]}: ${digests[*]}"
|
|
292
|
+
exit 1
|
|
293
|
+
fi
|
|
294
|
+
args=()
|
|
295
|
+
for tag in "${tags[@]}"; do
|
|
296
|
+
args+=(--tag "$tag")
|
|
297
|
+
done
|
|
298
|
+
for digest in "${digests[@]}"; do
|
|
299
|
+
args+=("$IMAGE@sha256:$digest")
|
|
300
|
+
done
|
|
301
|
+
echo "docker buildx imagetools create ${args[*]}"
|
|
302
|
+
docker buildx imagetools create "${args[@]}"
|
|
303
|
+
- name: Check the image holds both platforms
|
|
304
|
+
env:
|
|
305
|
+
VERSION: ${{ needs.verify.outputs.version }}
|
|
306
|
+
run: |
|
|
307
|
+
ref="$IMAGE:$VERSION"
|
|
308
|
+
docker buildx imagetools inspect "$ref"
|
|
309
|
+
# Attestation manifests show up as unknown/unknown and are ignored.
|
|
310
|
+
platforms="$(docker buildx imagetools inspect "$ref" --format '{{json .Manifest}}' |
|
|
311
|
+
jq -r '.manifests[] | select(.platform != null) | "\(.platform.os)/\(.platform.architecture)"')"
|
|
312
|
+
status=0
|
|
313
|
+
for want in linux/amd64 linux/arm64; do
|
|
314
|
+
if grep -qxF "$want" <<<"$platforms"; then
|
|
315
|
+
echo "ok: $ref has $want"
|
|
316
|
+
else
|
|
317
|
+
echo "::error::$ref has no $want image"
|
|
318
|
+
status=1
|
|
319
|
+
fi
|
|
320
|
+
done
|
|
321
|
+
exit "$status"
|
|
322
|
+
|
|
323
|
+
assets:
|
|
324
|
+
needs: [verify, build]
|
|
325
|
+
if: needs.verify.outputs.dry_run != 'true'
|
|
326
|
+
runs-on: ubuntu-latest
|
|
327
|
+
timeout-minutes: 10
|
|
328
|
+
# gh release upload needs write access to the release; only this job has it.
|
|
329
|
+
permissions:
|
|
330
|
+
contents: write
|
|
331
|
+
steps:
|
|
332
|
+
- uses: actions/download-artifact@v8
|
|
333
|
+
with:
|
|
334
|
+
name: dist
|
|
335
|
+
path: dist/
|
|
336
|
+
- name: Attach the sdist and wheel to the release
|
|
337
|
+
env:
|
|
338
|
+
GH_TOKEN: ${{ github.token }}
|
|
339
|
+
TAG: ${{ needs.verify.outputs.tag }}
|
|
340
|
+
run: gh release upload "$TAG" dist/* --clobber --repo "$GITHUB_REPOSITORY"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# --- Project-internal files (never committed) ---
|
|
2
|
+
/PROJECT.md
|
|
3
|
+
/開発資料/
|
|
4
|
+
/.patent-checker/
|
|
5
|
+
/.claude/
|
|
6
|
+
|
|
7
|
+
# --- Python ---
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
.eggs/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
htmlcov/
|
|
23
|
+
|
|
24
|
+
# --- Environment / secrets ---
|
|
25
|
+
.env
|
|
26
|
+
.env.*
|
|
27
|
+
|
|
28
|
+
# --- Playwright / crawl4ai runtime ---
|
|
29
|
+
.crawl4ai/
|
|
30
|
+
|
|
31
|
+
# --- OS / editors ---
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
.idea/
|
|
35
|
+
.vscode/
|
|
36
|
+
*.swp
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
Pre-release versions are written in PEP 440 form (for example 1.0.0b1).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [1.0.0b1] - 2026-09-25
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- PyPI package `crawl4tools`
|
|
16
|
+
- Docker Hub image `xhighhongo41/crawl4tools` for linux/amd64 and linux/arm64, published with each GitHub Release (beta versions are not tagged `latest`)
|
|
17
|
+
- The direct-connection fallback now also covers a proxy that intercepts TLS: a TLS error, an error page generated by the proxy itself, or a bot challenge seen through the proxy; a refusal by the proxy itself is never bypassed
|
|
18
|
+
- GitHub Actions CI: lint, type checks, unit tests on Python 3.11 and 3.12, a package build check, and a Docker build and smoke test
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `crawl4server` now waits at most 5 seconds for requests in progress when stopped (`SIGINT`/`SIGTERM`, e.g. `docker stop`), then closes the remaining connections
|
|
23
|
+
- `compose.yaml` pulls the published `xhighhongo41/crawl4tools` image instead of building it
|
|
24
|
+
- The Docker base image is pinned to Debian 12 (bookworm)
|
|
25
|
+
- Development status is now Beta
|
|
26
|
+
|
|
27
|
+
## [0.4.0] - 2026-09-25
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- English and Japanese messages for `crawl4cli`, `crawl4mcp`, and `crawl4server`: `--help` text, error messages, notes and progress lines on stderr, the MCP tools' descriptions and result text, the servers' startup/warning lines, and the web loader's JSON error responses
|
|
32
|
+
- `--lang en|ja` option, `CRAWL4CLI_LANG`/`CRAWL4MCP_LANG`/`CRAWL4SERVER_LANG` environment variables, and a `lang` config file key (servers) to choose the message language, resolved as option > environment variable > config file
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- `crawl4cli` now follows the OS locale (`LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG`) to pick its default language; `crawl4mcp` and `crawl4server` still default to English regardless of the locale
|
|
37
|
+
|
|
38
|
+
## [0.3.0] - 2026-09-24
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `crawl4server` command: serves the Open WebUI web loader and MCP on two ports of one process, sharing one headless browser and one concurrency limit
|
|
43
|
+
- Open WebUI external web loader endpoint (`POST /crawl`, `GET /health`, optional bearer API key)
|
|
44
|
+
- `CRAWL4SERVER_*` environment variables and a YAML/JSON config file (`--config`)
|
|
45
|
+
- `Dockerfile` and `compose.yaml` for running `crawl4server` in a container
|
|
46
|
+
- Page title in fetch results (web loader `metadata.title`, MCP structured page data `title`)
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- `starlette` and `uvicorn` are now direct dependencies; the empty `server` extra was removed
|
|
51
|
+
|
|
52
|
+
## [0.2.0] - 2026-09-24
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `crawl4mcp` MCP server with `fetch` (return content directly) and `download` (save to files) tools
|
|
57
|
+
- stdio and Streamable HTTP transports
|
|
58
|
+
- Per-call URL limit (default 20) and server-wide concurrency limit (default 3), both configurable
|
|
59
|
+
- Configuration via command-line options, `CRAWL4MCP_*` environment variables, or a YAML/JSON config file (`--config`)
|
|
60
|
+
- `Fetcher.fetch`/`fetch_many` accept per-call options while sharing one engine
|
|
61
|
+
|
|
62
|
+
## [0.1.0] - 2026-09-24
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- `crawl4cli` command: download one or more URLs as Markdown, HTML, PDF, screenshot, MHTML, or raw source
|
|
67
|
+
- PDF transcription to Markdown; non-HTML resources such as images are saved as they are
|
|
68
|
+
- Error messages by cause (HTTP status, unknown host, refused connection, timeout, missing browser) and exit codes 0/1/2
|
|
69
|
+
- Proxy support (`--proxy`) with a single retry over a direct connection when the proxy fails (`--no-fallback` to disable)
|
|
70
|
+
- `--fit` to keep only the main content of a page, using crawl4ai's pruning content filter
|
|
71
|
+
- Concurrent downloads of several URLs into a directory (`-d`, `-j`)
|
|
72
|
+
- `CRAWL4CLI_*` environment variables for every option
|
|
73
|
+
- Shared fetch engine (`crawl4tools.engine`) for the planned servers
|
|
74
|
+
- `tools/check.sh` and `tools/release_check.sh` for development
|
|
75
|
+
|
|
76
|
+
## [0.0.1] - 2026-09-23
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- Project scaffolding (license, NOTICE, .gitignore, pyproject metadata)
|
|
81
|
+
- README in English and Japanese
|
|
82
|
+
- This Changelog
|
|
83
|
+
|
|
84
|
+
No code is included in this release.
|
|
85
|
+
|
|
86
|
+
[Unreleased]: https://github.com/xhighhongo41/crawl4tools/compare/v1.0.0b1...HEAD
|
|
87
|
+
[1.0.0b1]: https://github.com/xhighhongo41/crawl4tools/compare/v0.4.0...v1.0.0b1
|
|
88
|
+
[0.4.0]: https://github.com/xhighhongo41/crawl4tools/compare/v0.3.0...v0.4.0
|
|
89
|
+
[0.3.0]: https://github.com/xhighhongo41/crawl4tools/compare/v0.2.0...v0.3.0
|
|
90
|
+
[0.2.0]: https://github.com/xhighhongo41/crawl4tools/compare/v0.1.0...v0.2.0
|
|
91
|
+
[0.1.0]: https://github.com/xhighhongo41/crawl4tools/compare/v0.0.1...v0.1.0
|
|
92
|
+
[0.0.1]: https://github.com/xhighhongo41/crawl4tools/releases/tag/v0.0.1
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# crawl4tools server image
|
|
2
|
+
#
|
|
3
|
+
# Build: docker build -t xhighhongo41/crawl4tools:<version> .
|
|
4
|
+
# Run: docker compose up -d (pulls the published image unless built here)
|
|
5
|
+
#
|
|
6
|
+
# The image bundles Playwright's Chromium build (and its OS dependencies),
|
|
7
|
+
# so no separate browser install step is needed at runtime. The final
|
|
8
|
+
# process runs as the unprivileged "crawl" user, not root.
|
|
9
|
+
|
|
10
|
+
# The plain "3.12-slim" tag now points at Debian 13 (trixie); Playwright's
|
|
11
|
+
# `install --with-deps` has reports of misdetecting Debian 13, so pin to
|
|
12
|
+
# bookworm until that is confirmed to work.
|
|
13
|
+
FROM python:3.12-slim-bookworm
|
|
14
|
+
|
|
15
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
16
|
+
UV_COMPILE_BYTECODE=1 \
|
|
17
|
+
UV_LINK_MODE=copy \
|
|
18
|
+
UV_PYTHON_DOWNLOADS=never \
|
|
19
|
+
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
|
20
|
+
CRAWL4_AI_BASE_DIRECTORY=/data \
|
|
21
|
+
HOME=/data
|
|
22
|
+
|
|
23
|
+
# Pinned uv binary, copied straight from the official distroless image.
|
|
24
|
+
COPY --from=ghcr.io/astral-sh/uv:0.12.15 /uv /usr/local/bin/uv
|
|
25
|
+
|
|
26
|
+
WORKDIR /app
|
|
27
|
+
|
|
28
|
+
# Copy only what hatchling's wheel build and `uv sync` need, so source-only
|
|
29
|
+
# edits don't bust the dependency layer's cache.
|
|
30
|
+
COPY pyproject.toml uv.lock README.md LICENSE NOTICE ./
|
|
31
|
+
COPY src ./src
|
|
32
|
+
|
|
33
|
+
# Install the locked, non-dev dependency set plus the project itself
|
|
34
|
+
# (non-editable, so /app/src is not required at runtime) into /app/.venv,
|
|
35
|
+
# built against the image's own interpreter.
|
|
36
|
+
RUN UV_PYTHON=/usr/local/bin/python3 uv sync --frozen --no-dev --no-editable
|
|
37
|
+
|
|
38
|
+
# Install Chromium and the OS libraries it needs (requires root; apt-get is
|
|
39
|
+
# invoked by `--with-deps`). Drop the apt cache afterwards to keep the
|
|
40
|
+
# layer small.
|
|
41
|
+
RUN /app/.venv/bin/playwright install --with-deps chromium && \
|
|
42
|
+
rm -rf /var/lib/apt/lists/*
|
|
43
|
+
|
|
44
|
+
# Non-root runtime user; its home doubles as crawl4ai's cache/DB directory
|
|
45
|
+
# (CRAWL4_AI_BASE_DIRECTORY=/data above) and holds the download directory.
|
|
46
|
+
RUN useradd --create-home --home-dir /data --uid 1000 crawl && \
|
|
47
|
+
mkdir -p /data/downloads && \
|
|
48
|
+
chown -R crawl:crawl /data /ms-playwright
|
|
49
|
+
|
|
50
|
+
USER crawl
|
|
51
|
+
WORKDIR /data
|
|
52
|
+
|
|
53
|
+
ENV PATH=/app/.venv/bin:$PATH
|
|
54
|
+
|
|
55
|
+
# Open WebUI external web loader (8766) and MCP Streamable HTTP (8765).
|
|
56
|
+
EXPOSE 8766 8765
|
|
57
|
+
|
|
58
|
+
# Assumes the default loader port (8766); override the healthcheck if
|
|
59
|
+
# CRAWL4SERVER_LOADER_PORT is changed. No curl in the slim base image, so
|
|
60
|
+
# the check is done with Python's stdlib instead.
|
|
61
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
62
|
+
CMD ["python", "-c", "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8766/health', timeout=3).status == 200 else 1)"]
|
|
63
|
+
|
|
64
|
+
ENTRYPOINT ["crawl4server"]
|
|
65
|
+
CMD ["--host", "0.0.0.0", "--download-dir", "/data/downloads"]
|