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.
Files changed (80) hide show
  1. crawl4tools-1.0.0b1/.dockerignore +28 -0
  2. crawl4tools-1.0.0b1/.github/workflows/ci.yml +122 -0
  3. crawl4tools-1.0.0b1/.github/workflows/release.yml +340 -0
  4. crawl4tools-1.0.0b1/.gitignore +36 -0
  5. crawl4tools-1.0.0b1/.python-version +1 -0
  6. crawl4tools-1.0.0b1/Changelog.md +92 -0
  7. crawl4tools-1.0.0b1/Dockerfile +65 -0
  8. crawl4tools-1.0.0b1/LICENSE +202 -0
  9. crawl4tools-1.0.0b1/NOTICE +4 -0
  10. crawl4tools-1.0.0b1/PKG-INFO +327 -0
  11. crawl4tools-1.0.0b1/README.md +295 -0
  12. crawl4tools-1.0.0b1/README_ja.md +295 -0
  13. crawl4tools-1.0.0b1/compose.yaml +43 -0
  14. crawl4tools-1.0.0b1/pyproject.toml +85 -0
  15. crawl4tools-1.0.0b1/src/crawl4tools/__init__.py +10 -0
  16. crawl4tools-1.0.0b1/src/crawl4tools/cli/__init__.py +1 -0
  17. crawl4tools-1.0.0b1/src/crawl4tools/cli/main.py +394 -0
  18. crawl4tools-1.0.0b1/src/crawl4tools/cli/output.py +71 -0
  19. crawl4tools-1.0.0b1/src/crawl4tools/cli/report.py +48 -0
  20. crawl4tools-1.0.0b1/src/crawl4tools/engine/__init__.py +100 -0
  21. crawl4tools-1.0.0b1/src/crawl4tools/engine/classify.py +152 -0
  22. crawl4tools-1.0.0b1/src/crawl4tools/engine/errors.py +266 -0
  23. crawl4tools-1.0.0b1/src/crawl4tools/engine/fetcher.py +620 -0
  24. crawl4tools-1.0.0b1/src/crawl4tools/engine/interception.py +216 -0
  25. crawl4tools-1.0.0b1/src/crawl4tools/engine/models.py +116 -0
  26. crawl4tools-1.0.0b1/src/crawl4tools/engine/naming.py +197 -0
  27. crawl4tools-1.0.0b1/src/crawl4tools/engine/pdf.py +53 -0
  28. crawl4tools-1.0.0b1/src/crawl4tools/engine/probe.py +149 -0
  29. crawl4tools-1.0.0b1/src/crawl4tools/engine/proxy.py +123 -0
  30. crawl4tools-1.0.0b1/src/crawl4tools/i18n.py +176 -0
  31. crawl4tools-1.0.0b1/src/crawl4tools/locale/ja/LC_MESSAGES/crawl4tools.mo +0 -0
  32. crawl4tools-1.0.0b1/src/crawl4tools/locale/ja/LC_MESSAGES/crawl4tools.po +536 -0
  33. crawl4tools-1.0.0b1/src/crawl4tools/py.typed +0 -0
  34. crawl4tools-1.0.0b1/src/crawl4tools/server/__init__.py +28 -0
  35. crawl4tools-1.0.0b1/src/crawl4tools/server/cli_options.py +262 -0
  36. crawl4tools-1.0.0b1/src/crawl4tools/server/config.py +91 -0
  37. crawl4tools-1.0.0b1/src/crawl4tools/server/host.py +284 -0
  38. crawl4tools-1.0.0b1/src/crawl4tools/server/loader.py +239 -0
  39. crawl4tools-1.0.0b1/src/crawl4tools/server/mcp_main.py +230 -0
  40. crawl4tools-1.0.0b1/src/crawl4tools/server/mcp_server.py +445 -0
  41. crawl4tools-1.0.0b1/src/crawl4tools/server/results.py +258 -0
  42. crawl4tools-1.0.0b1/src/crawl4tools/server/server_main.py +360 -0
  43. crawl4tools-1.0.0b1/src/crawl4tools/server/settings.py +126 -0
  44. crawl4tools-1.0.0b1/tests/conftest.py +195 -0
  45. crawl4tools-1.0.0b1/tests/fixtures/make_sample_pdf.py +41 -0
  46. crawl4tools-1.0.0b1/tests/fixtures/sample.pdf +32 -0
  47. crawl4tools-1.0.0b1/tests/integration/test_cli_live.py +106 -0
  48. crawl4tools-1.0.0b1/tests/integration/test_mcp_live.py +121 -0
  49. crawl4tools-1.0.0b1/tests/integration/test_server_live.py +176 -0
  50. crawl4tools-1.0.0b1/tests/unit/test_classify.py +227 -0
  51. crawl4tools-1.0.0b1/tests/unit/test_cli_main.py +579 -0
  52. crawl4tools-1.0.0b1/tests/unit/test_cli_options.py +284 -0
  53. crawl4tools-1.0.0b1/tests/unit/test_cli_output.py +183 -0
  54. crawl4tools-1.0.0b1/tests/unit/test_cli_report.py +127 -0
  55. crawl4tools-1.0.0b1/tests/unit/test_errors.py +142 -0
  56. crawl4tools-1.0.0b1/tests/unit/test_errors_i18n.py +495 -0
  57. crawl4tools-1.0.0b1/tests/unit/test_fetcher.py +1289 -0
  58. crawl4tools-1.0.0b1/tests/unit/test_host.py +531 -0
  59. crawl4tools-1.0.0b1/tests/unit/test_i18n.py +419 -0
  60. crawl4tools-1.0.0b1/tests/unit/test_interception.py +320 -0
  61. crawl4tools-1.0.0b1/tests/unit/test_loader.py +462 -0
  62. crawl4tools-1.0.0b1/tests/unit/test_mcp_main.py +567 -0
  63. crawl4tools-1.0.0b1/tests/unit/test_mcp_server.py +945 -0
  64. crawl4tools-1.0.0b1/tests/unit/test_models.py +94 -0
  65. crawl4tools-1.0.0b1/tests/unit/test_naming.py +213 -0
  66. crawl4tools-1.0.0b1/tests/unit/test_pdf.py +37 -0
  67. crawl4tools-1.0.0b1/tests/unit/test_probe.py +230 -0
  68. crawl4tools-1.0.0b1/tests/unit/test_proxy.py +221 -0
  69. crawl4tools-1.0.0b1/tests/unit/test_server_config.py +186 -0
  70. crawl4tools-1.0.0b1/tests/unit/test_server_main.py +621 -0
  71. crawl4tools-1.0.0b1/tests/unit/test_server_results.py +691 -0
  72. crawl4tools-1.0.0b1/tests/unit/test_server_settings.py +185 -0
  73. crawl4tools-1.0.0b1/tests/unit/test_version.py +11 -0
  74. crawl4tools-1.0.0b1/tests/unit/test_workflows.py +427 -0
  75. crawl4tools-1.0.0b1/tools/check.sh +50 -0
  76. crawl4tools-1.0.0b1/tools/docker_smoke.sh +176 -0
  77. crawl4tools-1.0.0b1/tools/i18n.sh +62 -0
  78. crawl4tools-1.0.0b1/tools/release_check.sh +353 -0
  79. crawl4tools-1.0.0b1/tools/wheel_check.sh +67 -0
  80. 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"]