pydj 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.
- pydj-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +107 -0
- pydj-0.1.0/.github/ISSUE_TEMPLATE/config.yml +11 -0
- pydj-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +68 -0
- pydj-0.1.0/.github/pull_request_template.md +27 -0
- pydj-0.1.0/.github/workflows/ci.yml +263 -0
- pydj-0.1.0/.github/workflows/release.yml +146 -0
- pydj-0.1.0/.gitignore +70 -0
- pydj-0.1.0/CHANGELOG.md +94 -0
- pydj-0.1.0/CONTRIBUTING.md +119 -0
- pydj-0.1.0/LICENSE +384 -0
- pydj-0.1.0/PKG-INFO +694 -0
- pydj-0.1.0/README.md +656 -0
- pydj-0.1.0/README.zh.md +627 -0
- pydj-0.1.0/SECURITY.md +60 -0
- pydj-0.1.0/examples/01_basic_scan.py +32 -0
- pydj-0.1.0/examples/02_structured_output.py +55 -0
- pydj-0.1.0/examples/03_fine_grained.py +87 -0
- pydj-0.1.0/examples/04_custom_transport.py +204 -0
- pydj-0.1.0/examples/05_custom_plugin.py +109 -0
- pydj-0.1.0/examples/06_batch_scan.py +95 -0
- pydj-0.1.0/examples/README.md +110 -0
- pydj-0.1.0/examples/site_fixture.py +178 -0
- pydj-0.1.0/pyproject.toml +116 -0
- pydj-0.1.0/src/pydj/__init__.py +127 -0
- pydj-0.1.0/src/pydj/__main__.py +6 -0
- pydj-0.1.0/src/pydj/api.py +333 -0
- pydj-0.1.0/src/pydj/cli.py +379 -0
- pydj-0.1.0/src/pydj/decode.py +110 -0
- pydj-0.1.0/src/pydj/fetcher/__init__.py +33 -0
- pydj-0.1.0/src/pydj/fetcher/cache.py +313 -0
- pydj-0.1.0/src/pydj/fetcher/fetcher.py +519 -0
- pydj-0.1.0/src/pydj/fetcher/transport.py +401 -0
- pydj-0.1.0/src/pydj/knowledge.py +91 -0
- pydj-0.1.0/src/pydj/operations.py +628 -0
- pydj-0.1.0/src/pydj/output.py +123 -0
- pydj-0.1.0/src/pydj/pipeline.py +1379 -0
- pydj-0.1.0/src/pydj/plugins/__init__.py +117 -0
- pydj-0.1.0/src/pydj/plugins/base.py +137 -0
- pydj-0.1.0/src/pydj/plugins/dynamic_import.py +45 -0
- pydj-0.1.0/src/pydj/plugins/emp.py +101 -0
- pydj-0.1.0/src/pydj/plugins/esm_import.py +53 -0
- pydj-0.1.0/src/pydj/plugins/helmicro.py +159 -0
- pydj-0.1.0/src/pydj/plugins/html_pivot.py +133 -0
- pydj-0.1.0/src/pydj/plugins/html_script.py +134 -0
- pydj-0.1.0/src/pydj/plugins/microapp.py +203 -0
- pydj-0.1.0/src/pydj/plugins/microapp_helpers.py +94 -0
- pydj-0.1.0/src/pydj/plugins/modernjs.py +110 -0
- pydj-0.1.0/src/pydj/plugins/module_federation.py +125 -0
- pydj-0.1.0/src/pydj/plugins/module_federation_manifest.py +249 -0
- pydj-0.1.0/src/pydj/plugins/nextjs.py +432 -0
- pydj-0.1.0/src/pydj/plugins/nuxt.py +39 -0
- pydj-0.1.0/src/pydj/plugins/requirejs.py +94 -0
- pydj-0.1.0/src/pydj/plugins/script_create.py +53 -0
- pydj-0.1.0/src/pydj/plugins/sourcemap.py +108 -0
- pydj-0.1.0/src/pydj/plugins/sveltekit.py +46 -0
- pydj-0.1.0/src/pydj/plugins/trunk.py +118 -0
- pydj-0.1.0/src/pydj/plugins/umijs.py +107 -0
- pydj-0.1.0/src/pydj/plugins/universal_url.py +216 -0
- pydj-0.1.0/src/pydj/plugins/urlpattern.py +68 -0
- pydj-0.1.0/src/pydj/plugins/vite.py +129 -0
- pydj-0.1.0/src/pydj/plugins/webpack.py +715 -0
- pydj-0.1.0/src/pydj/sourcemap/__init__.py +41 -0
- pydj-0.1.0/src/pydj/sourcemap/paths.py +137 -0
- pydj-0.1.0/src/pydj/sourcemap/sourcemap.py +256 -0
- pydj-0.1.0/src/pydj/sourcemap/vlq.py +129 -0
- pydj-0.1.0/src/pydj/transport_protocol.py +219 -0
- pydj-0.1.0/src/pydj/types.py +356 -0
- pydj-0.1.0/src/pydj/urls.py +186 -0
- pydj-0.1.0/tests/conftest.py +94 -0
- pydj-0.1.0/tests/test_cli_api.py +541 -0
- pydj-0.1.0/tests/test_core.py +341 -0
- pydj-0.1.0/tests/test_differential.py +336 -0
- pydj-0.1.0/tests/test_fetcher.py +424 -0
- pydj-0.1.0/tests/test_injectable_transport.py +482 -0
- pydj-0.1.0/tests/test_operations.py +451 -0
- pydj-0.1.0/tests/test_pipeline.py +789 -0
- pydj-0.1.0/tests/test_plugins.py +918 -0
- pydj-0.1.0/tests/test_sourcemap.py +268 -0
- pydj-0.1.0/uv.lock +472 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something is broken — a crash, wrong output, or a site not extracted correctly
|
|
3
|
+
title: "[Bug]: "
|
|
4
|
+
labels: ["bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for the report. The single most useful thing you can include is
|
|
10
|
+
**the URL**, if a specific site is involved.
|
|
11
|
+
|
|
12
|
+
- type: dropdown
|
|
13
|
+
id: interface
|
|
14
|
+
attributes:
|
|
15
|
+
label: How are you using pydj?
|
|
16
|
+
options:
|
|
17
|
+
- CLI (pydj / dj)
|
|
18
|
+
- Library (from pydj import scan)
|
|
19
|
+
- Both
|
|
20
|
+
validations:
|
|
21
|
+
required: true
|
|
22
|
+
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: what-happened
|
|
25
|
+
attributes:
|
|
26
|
+
label: What happened?
|
|
27
|
+
description: The actual behaviour, including any error output.
|
|
28
|
+
placeholder: |
|
|
29
|
+
Running `pydj https://example.com` reports 0 JS files, but the page
|
|
30
|
+
clearly loads several.
|
|
31
|
+
validations:
|
|
32
|
+
required: true
|
|
33
|
+
|
|
34
|
+
- type: textarea
|
|
35
|
+
id: expected
|
|
36
|
+
attributes:
|
|
37
|
+
label: What did you expect?
|
|
38
|
+
placeholder: It should find the scripts referenced by the page.
|
|
39
|
+
validations:
|
|
40
|
+
required: true
|
|
41
|
+
|
|
42
|
+
- type: textarea
|
|
43
|
+
id: reproduce
|
|
44
|
+
attributes:
|
|
45
|
+
label: Steps to reproduce
|
|
46
|
+
description: The exact command or code, so it can be run verbatim.
|
|
47
|
+
render: shell
|
|
48
|
+
placeholder: |
|
|
49
|
+
pipx install 'pydj[tls]'
|
|
50
|
+
pydj -f json --no-cache https://example.com
|
|
51
|
+
validations:
|
|
52
|
+
required: true
|
|
53
|
+
|
|
54
|
+
- type: input
|
|
55
|
+
id: version
|
|
56
|
+
attributes:
|
|
57
|
+
label: pydj version
|
|
58
|
+
description: Output of `pydj --version`.
|
|
59
|
+
placeholder: pydj 0.1.0
|
|
60
|
+
validations:
|
|
61
|
+
required: true
|
|
62
|
+
|
|
63
|
+
- type: input
|
|
64
|
+
id: python
|
|
65
|
+
attributes:
|
|
66
|
+
label: Python version and OS
|
|
67
|
+
description: Output of `python -V`, plus your platform.
|
|
68
|
+
placeholder: Python 3.12.4 on macOS 15.1 (arm64)
|
|
69
|
+
validations:
|
|
70
|
+
required: true
|
|
71
|
+
|
|
72
|
+
- type: checkboxes
|
|
73
|
+
id: tls
|
|
74
|
+
attributes:
|
|
75
|
+
label: TLS impersonation
|
|
76
|
+
description: |
|
|
77
|
+
Whether browser TLS fingerprinting is active changes the outcome on
|
|
78
|
+
sites behind Cloudflare and similar.
|
|
79
|
+
options:
|
|
80
|
+
- label: I installed the `tls` extra (so `curl-cffi` is present)
|
|
81
|
+
- label: "`pydj --debug` reports TLS fingerprint impersonation is disabled"
|
|
82
|
+
|
|
83
|
+
- type: dropdown
|
|
84
|
+
id: go-comparison
|
|
85
|
+
attributes:
|
|
86
|
+
label: Does the Go implementation behave the same way?
|
|
87
|
+
description: |
|
|
88
|
+
pydj is a port, so this instantly distinguishes "the port is wrong" from
|
|
89
|
+
"the original does this too". Skip if you have not tried it.
|
|
90
|
+
options:
|
|
91
|
+
- "Not tried"
|
|
92
|
+
- "Go finds it correctly — pydj is missing it"
|
|
93
|
+
- "Go behaves the same way"
|
|
94
|
+
- "Go misses it too"
|
|
95
|
+
validations:
|
|
96
|
+
required: true
|
|
97
|
+
|
|
98
|
+
- type: textarea
|
|
99
|
+
id: debug
|
|
100
|
+
attributes:
|
|
101
|
+
label: Debug output
|
|
102
|
+
description: |
|
|
103
|
+
If you can, attach the tail of `pydj --debug -f json <url>` (it is
|
|
104
|
+
verbose; the last ~100 lines usually suffice).
|
|
105
|
+
render: shell
|
|
106
|
+
validations:
|
|
107
|
+
required: false
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Security vulnerability
|
|
4
|
+
url: https://github.com/ejfkdev/pydj/security/advisories/new
|
|
5
|
+
about: Report privately, not as a public issue. See SECURITY.md.
|
|
6
|
+
- name: Question or usage help
|
|
7
|
+
url: https://github.com/ejfkdev/pydj/discussions
|
|
8
|
+
about: For "how do I…" questions. The README and examples/ answer most of them.
|
|
9
|
+
- name: The original Go implementation
|
|
10
|
+
url: https://github.com/ejfkdev/dj/issues
|
|
11
|
+
about: If the Go tool behaves the same way, the issue may belong upstream.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest a bundler, framework or loading pattern pydj should recognise
|
|
3
|
+
title: "[Feature]: "
|
|
4
|
+
labels: ["enhancement"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Most feature requests here are about **discovery coverage** — a bundler
|
|
10
|
+
whose chunk URL scheme pydj does not recognise. Those are the easiest to
|
|
11
|
+
action, and a URL plus the relevant code snippet usually makes it a
|
|
12
|
+
small, well-defined change.
|
|
13
|
+
|
|
14
|
+
- type: dropdown
|
|
15
|
+
id: kind
|
|
16
|
+
attributes:
|
|
17
|
+
label: What kind of request is this?
|
|
18
|
+
options:
|
|
19
|
+
- A site or bundler whose JS is not extracted (coverage)
|
|
20
|
+
- A new library API or option
|
|
21
|
+
- A new CLI option
|
|
22
|
+
- Documentation
|
|
23
|
+
- Other
|
|
24
|
+
validations:
|
|
25
|
+
required: true
|
|
26
|
+
|
|
27
|
+
- type: textarea
|
|
28
|
+
id: problem
|
|
29
|
+
attributes:
|
|
30
|
+
label: What problem are you hitting?
|
|
31
|
+
description: Describe the situation, not the solution.
|
|
32
|
+
placeholder: |
|
|
33
|
+
A site built with X loads its chunks via `X.load("name")` and pydj finds
|
|
34
|
+
only the entry bundle.
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
|
|
38
|
+
- type: textarea
|
|
39
|
+
id: evidence
|
|
40
|
+
attributes:
|
|
41
|
+
label: Evidence from the site
|
|
42
|
+
description: |
|
|
43
|
+
If this is a coverage request, the URL and the code that builds the
|
|
44
|
+
chunk URLs. Grep the bundle for the chunk path pattern — the surrounding
|
|
45
|
+
function is what a plugin has to match.
|
|
46
|
+
render: shell
|
|
47
|
+
placeholder: |
|
|
48
|
+
# from the site's bundle
|
|
49
|
+
l.u = function (e) { return "static/js/" + e + "." + {10: "abc123"}[e] + ".js" }
|
|
50
|
+
validations:
|
|
51
|
+
required: false
|
|
52
|
+
|
|
53
|
+
- type: textarea
|
|
54
|
+
id: proposal
|
|
55
|
+
attributes:
|
|
56
|
+
label: What would you like to see?
|
|
57
|
+
validations:
|
|
58
|
+
required: true
|
|
59
|
+
|
|
60
|
+
- type: checkboxes
|
|
61
|
+
id: contribute
|
|
62
|
+
attributes:
|
|
63
|
+
label: Would you like to work on it?
|
|
64
|
+
options:
|
|
65
|
+
- label: I am willing to open a pull request
|
|
66
|
+
required: false
|
|
67
|
+
- label: I have read CONTRIBUTING.md
|
|
68
|
+
required: false
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## What this changes
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences. What was wrong or missing, and what the change does. -->
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
<!-- The reason, especially if the change looks like it does the wrong thing.
|
|
8
|
+
Behaviour that deliberately differs from the Go original needs a note here,
|
|
9
|
+
or a future reader will "fix" it back. -->
|
|
10
|
+
|
|
11
|
+
## How it was verified
|
|
12
|
+
|
|
13
|
+
<!-- Delete what does not apply. -->
|
|
14
|
+
|
|
15
|
+
- [ ] `ruff check src tests examples` passes
|
|
16
|
+
- [ ] `python -m pytest` passes
|
|
17
|
+
- [ ] Checked against the Go implementation on a real site
|
|
18
|
+
(`DJ_REFERENCE_BIN=/tmp/dj-ref python -m pytest tests/test_differential.py`)
|
|
19
|
+
- [ ] New or changed behaviour has a test
|
|
20
|
+
|
|
21
|
+
## Notes for review
|
|
22
|
+
|
|
23
|
+
<!-- Anything you want a reviewer to look at first, or are unsure about.
|
|
24
|
+
|
|
25
|
+
If this changes discovery, say whether it can *lose* a URL the previous
|
|
26
|
+
version found -- a port that silently stops finding a file is worse than one
|
|
27
|
+
that wastes a request. -->
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }})
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
# 3.10 is the floor declared in pyproject.toml; the rest guard against
|
|
24
|
+
# regressions on newer interpreters (a decompression bug once appeared
|
|
25
|
+
# only on 3.14).
|
|
26
|
+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
|
27
|
+
os: [ubuntu-latest]
|
|
28
|
+
include:
|
|
29
|
+
- { os: macos-latest, python-version: '3.13' }
|
|
30
|
+
- { os: windows-latest, python-version: '3.13' }
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- uses: actions/setup-python@v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
|
|
39
|
+
- name: Install
|
|
40
|
+
run: |
|
|
41
|
+
python -m pip install --upgrade pip
|
|
42
|
+
pip install -e '.[dev]'
|
|
43
|
+
|
|
44
|
+
- name: Show resolved HTTP stack
|
|
45
|
+
# curl-cffi is what enables TLS impersonation; knowing which version a
|
|
46
|
+
# failing run used makes a platform-specific failure much easier to read.
|
|
47
|
+
run: |
|
|
48
|
+
python -c "import curl_cffi, httpx, sys; print('python', sys.version.split()[0]); print('curl_cffi', curl_cffi.__version__); print('httpx', httpx.__version__)"
|
|
49
|
+
|
|
50
|
+
- name: Test
|
|
51
|
+
run: python -m pytest -q
|
|
52
|
+
|
|
53
|
+
verify-cli:
|
|
54
|
+
name: Verify CLI end to end
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
|
|
59
|
+
- uses: actions/setup-python@v5
|
|
60
|
+
with:
|
|
61
|
+
python-version: '3.13'
|
|
62
|
+
|
|
63
|
+
- name: Install
|
|
64
|
+
run: |
|
|
65
|
+
python -m pip install --upgrade pip
|
|
66
|
+
pip install -e '.[dev]'
|
|
67
|
+
|
|
68
|
+
- name: Build a fixture site and scan it
|
|
69
|
+
# The unit tests cover the pipeline in-process; this checks the packaged
|
|
70
|
+
# entry point actually finds JS over real HTTP, which is the thing users
|
|
71
|
+
# invoke. Catches anything that only breaks in the console script.
|
|
72
|
+
run: |
|
|
73
|
+
python - <<'PY'
|
|
74
|
+
import json, pathlib, threading, subprocess, sys
|
|
75
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
76
|
+
|
|
77
|
+
root = pathlib.Path('fixture_site')
|
|
78
|
+
(root / 'js').mkdir(parents=True, exist_ok=True)
|
|
79
|
+
(root / 'index.html').write_text(
|
|
80
|
+
'<!doctype html><html><head>'
|
|
81
|
+
'<link rel="modulepreload" href="/js/vendor.js">'
|
|
82
|
+
'</head><body>'
|
|
83
|
+
'<script src="/js/app.js"></script>'
|
|
84
|
+
'<script>import("/js/lazy.js")</script>'
|
|
85
|
+
'</body></html>'
|
|
86
|
+
)
|
|
87
|
+
(root / 'js/app.js').write_text(
|
|
88
|
+
'import("./chunk.js");\n//# sourceMappingURL=app.js.map\n'
|
|
89
|
+
)
|
|
90
|
+
for name in ('vendor.js', 'lazy.js', 'chunk.js'):
|
|
91
|
+
(root / 'js' / name).write_text('var x = 1;\n')
|
|
92
|
+
(root / 'js/app.js.map').write_text(json.dumps({
|
|
93
|
+
'version': 3, 'sources': ['src/App.tsx'],
|
|
94
|
+
'sourcesContent': ['export const App = 1;\n'], 'mappings': 'AAAA',
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
class Handler(BaseHTTPRequestHandler):
|
|
98
|
+
def do_GET(self):
|
|
99
|
+
target = root / self.path.lstrip('/')
|
|
100
|
+
if target.is_dir():
|
|
101
|
+
target = target / 'index.html'
|
|
102
|
+
if not target.is_file():
|
|
103
|
+
self.send_response(404); self.send_header('Content-Length','0'); self.end_headers(); return
|
|
104
|
+
body = target.read_bytes()
|
|
105
|
+
ctype = 'application/json' if target.suffix == '.map' else (
|
|
106
|
+
'text/html' if target.suffix == '.html' else 'application/javascript')
|
|
107
|
+
self.send_response(200)
|
|
108
|
+
self.send_header('Content-Type', ctype)
|
|
109
|
+
self.send_header('Content-Length', str(len(body)))
|
|
110
|
+
self.end_headers()
|
|
111
|
+
self.wfile.write(body)
|
|
112
|
+
def do_HEAD(self):
|
|
113
|
+
self.do_GET()
|
|
114
|
+
def log_message(self, *a): pass
|
|
115
|
+
|
|
116
|
+
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
|
117
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
118
|
+
url = f'http://127.0.0.1:{server.server_port}/'
|
|
119
|
+
|
|
120
|
+
proc = subprocess.run(
|
|
121
|
+
[sys.executable, '-m', 'pydj', '-f', 'json', '--no-cache', url],
|
|
122
|
+
capture_output=True, text=True, timeout=180,
|
|
123
|
+
)
|
|
124
|
+
server.shutdown(); server.server_close()
|
|
125
|
+
|
|
126
|
+
if proc.returncode != 0:
|
|
127
|
+
sys.exit(f'pydj exited {proc.returncode}\n{proc.stderr}')
|
|
128
|
+
|
|
129
|
+
data = json.loads(proc.stdout)
|
|
130
|
+
found = {u.rsplit('/', 1)[-1] for u in data['jsURLs']}
|
|
131
|
+
expected = {'app.js', 'vendor.js', 'lazy.js', 'chunk.js'}
|
|
132
|
+
missing = expected - found
|
|
133
|
+
if missing:
|
|
134
|
+
sys.exit(f'CLI missed {sorted(missing)}; found {sorted(found)}')
|
|
135
|
+
|
|
136
|
+
summary = data['summary']
|
|
137
|
+
assert summary['jsCount'] == 4, summary
|
|
138
|
+
assert summary.get('sourceMapCount') == 1, summary
|
|
139
|
+
assert summary.get('sourceCount') == 1, summary
|
|
140
|
+
print('CLI found all four JS files, the source map, and restored 1 source')
|
|
141
|
+
PY
|
|
142
|
+
|
|
143
|
+
differential:
|
|
144
|
+
name: Differential test against the Go implementation
|
|
145
|
+
runs-on: ubuntu-latest
|
|
146
|
+
steps:
|
|
147
|
+
- uses: actions/checkout@v4
|
|
148
|
+
|
|
149
|
+
- uses: actions/setup-python@v5
|
|
150
|
+
with:
|
|
151
|
+
python-version: '3.13'
|
|
152
|
+
|
|
153
|
+
- uses: actions/setup-go@v5
|
|
154
|
+
with:
|
|
155
|
+
# The Go project declares 1.26; `stable` keeps this working when the
|
|
156
|
+
# action's cache has not seen that version yet.
|
|
157
|
+
go-version: 'stable'
|
|
158
|
+
|
|
159
|
+
- name: Build the reference implementation
|
|
160
|
+
# This is the oracle the port is measured against. The suite skips
|
|
161
|
+
# cleanly without it, which is why it needs its own job -- otherwise the
|
|
162
|
+
# strongest test in the repo never actually runs in CI.
|
|
163
|
+
#
|
|
164
|
+
# `continue-on-error` because this job depends on an external repository
|
|
165
|
+
# and Go toolchain: if the clone fails, the upstream module moves, or the
|
|
166
|
+
# Go version policy changes, that is not a defect in pydj and must not
|
|
167
|
+
# fail the build. The next step reports clearly whether the suite
|
|
168
|
+
# actually ran.
|
|
169
|
+
continue-on-error: true
|
|
170
|
+
run: |
|
|
171
|
+
git clone --depth 1 https://github.com/ejfkdev/dj /tmp/dj-src
|
|
172
|
+
cd /tmp/dj-src
|
|
173
|
+
go build -o /tmp/dj-ref .
|
|
174
|
+
/tmp/dj-ref -h | head -3
|
|
175
|
+
|
|
176
|
+
- name: Confirm the oracle is usable
|
|
177
|
+
# Without this, a failed clone would silently turn the differential suite
|
|
178
|
+
# into a no-op that still reports success -- the one outcome worse than
|
|
179
|
+
# not running it.
|
|
180
|
+
run: |
|
|
181
|
+
if [ ! -x /tmp/dj-ref ]; then
|
|
182
|
+
echo "::warning::reference binary unavailable; differential suite will skip"
|
|
183
|
+
exit 0
|
|
184
|
+
fi
|
|
185
|
+
python -c "import subprocess; out = subprocess.run(['/tmp/dj-ref','-h'], capture_output=True, text=True).stdout; assert 'Dynamic JS File Extractor' in out, out[:200]; print('oracle ok')"
|
|
186
|
+
|
|
187
|
+
- name: Install pydj
|
|
188
|
+
run: |
|
|
189
|
+
python -m pip install --upgrade pip
|
|
190
|
+
pip install -e '.[dev]'
|
|
191
|
+
|
|
192
|
+
- name: Run the differential suite
|
|
193
|
+
env:
|
|
194
|
+
DJ_REFERENCE_BIN: /tmp/dj-ref
|
|
195
|
+
run: python -m pytest tests/test_differential.py -v
|
|
196
|
+
|
|
197
|
+
- name: Fail if the suite silently skipped
|
|
198
|
+
# A skipped differential run looks identical to a passing one in the
|
|
199
|
+
# summary line, which is the failure mode this guards against.
|
|
200
|
+
run: |
|
|
201
|
+
if [ ! -x /tmp/dj-ref ]; then
|
|
202
|
+
echo "Differential suite did not run: no reference binary."
|
|
203
|
+
echo "This job is advisory; see the warning above."
|
|
204
|
+
exit 0
|
|
205
|
+
fi
|
|
206
|
+
python -m pytest tests/test_differential.py -q --no-header 2>&1 | tail -3
|
|
207
|
+
|
|
208
|
+
lint:
|
|
209
|
+
name: Lint
|
|
210
|
+
runs-on: ubuntu-latest
|
|
211
|
+
steps:
|
|
212
|
+
- uses: actions/checkout@v4
|
|
213
|
+
|
|
214
|
+
- uses: actions/setup-python@v5
|
|
215
|
+
with:
|
|
216
|
+
python-version: '3.13'
|
|
217
|
+
|
|
218
|
+
- name: Install
|
|
219
|
+
run: |
|
|
220
|
+
python -m pip install --upgrade pip
|
|
221
|
+
pip install -e '.[dev]' ruff
|
|
222
|
+
|
|
223
|
+
- name: Ruff
|
|
224
|
+
run: ruff check src tests
|
|
225
|
+
|
|
226
|
+
- name: Check the wheel builds and has the expected metadata
|
|
227
|
+
run: |
|
|
228
|
+
pip install build
|
|
229
|
+
python -m build --wheel
|
|
230
|
+
python - <<'PY'
|
|
231
|
+
import glob, zipfile
|
|
232
|
+
wheel = glob.glob('dist/*.whl')[0]
|
|
233
|
+
archive = zipfile.ZipFile(wheel)
|
|
234
|
+
# Locate the dist-info directory rather than reconstructing its name:
|
|
235
|
+
# the wheel filename carries build tags (py3-none-any) that the
|
|
236
|
+
# dist-info directory does not.
|
|
237
|
+
dist_info = next(n for n in archive.namelist() if n.endswith('.dist-info/METADATA'))
|
|
238
|
+
meta = archive.read(dist_info).decode()
|
|
239
|
+
|
|
240
|
+
# curl-cffi must be an unconditional dependency: without it, TLS
|
|
241
|
+
# impersonation silently stops working, which is the whole product.
|
|
242
|
+
assert 'Requires-Dist: curl-cffi' in meta, 'curl-cffi must be a hard dependency'
|
|
243
|
+
assert 'Requires-Dist: httpx' in meta
|
|
244
|
+
assert 'Homepage, https://github.com/ejfkdev/pydj' in meta, 'wrong project URL'
|
|
245
|
+
assert 'Project-URL: Repository, https://github.com/ejfkdev/pydj' in meta
|
|
246
|
+
# PEP 639: the license is reported as an expression, not a free-text
|
|
247
|
+
# classifier. Asserting the old `License:` field would fail here even
|
|
248
|
+
# though the metadata is correct.
|
|
249
|
+
assert 'License-Expression: MPL-2.0' in meta, 'wrong license metadata'
|
|
250
|
+
assert 'License-File: LICENSE' in meta
|
|
251
|
+
|
|
252
|
+
entry_points = archive.read(
|
|
253
|
+
dist_info.replace('METADATA', 'entry_points.txt')
|
|
254
|
+
).decode()
|
|
255
|
+
assert 'pydj = pydj.cli:main' in entry_points, 'pydj console script missing'
|
|
256
|
+
assert 'dj = pydj.cli:main' in entry_points, 'dj console script missing'
|
|
257
|
+
|
|
258
|
+
# The README is the PyPI description; an empty one means a packaging
|
|
259
|
+
# regression that only shows up on the project page.
|
|
260
|
+
assert 'Dynamic JS File Extractor' in meta, 'long_description missing'
|
|
261
|
+
|
|
262
|
+
print('wheel metadata OK:', dist_info.split('/')[0])
|
|
263
|
+
PY
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag-driven, mirroring the Go project's build.yml: pushing `v0.2.0` is the only
|
|
4
|
+
# action required to cut a release.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags:
|
|
8
|
+
- 'v*'
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
# Gate the release on the same checks CI runs. A tag that does not pass tests
|
|
15
|
+
# never reaches PyPI, where a bad upload can only be yanked, not deleted.
|
|
16
|
+
verify:
|
|
17
|
+
name: Verify before publishing
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: '3.13'
|
|
25
|
+
|
|
26
|
+
- name: Install
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install -e '.[dev]'
|
|
30
|
+
|
|
31
|
+
- name: Test
|
|
32
|
+
run: python -m pytest -q
|
|
33
|
+
|
|
34
|
+
- name: Check the tag matches the packaged version
|
|
35
|
+
# A mismatch would publish a wheel whose version disagrees with its tag,
|
|
36
|
+
# and PyPI refuses re-uploads, so catch it here.
|
|
37
|
+
run: |
|
|
38
|
+
python - <<'PY'
|
|
39
|
+
import os, re, pathlib
|
|
40
|
+
tag = os.environ['GITHUB_REF_NAME'].lstrip('v')
|
|
41
|
+
text = pathlib.Path('pyproject.toml').read_text()
|
|
42
|
+
version = re.search(r'^version = "([^"]+)"', text, re.M).group(1)
|
|
43
|
+
init = pathlib.Path('src/pydj/__init__.py').read_text()
|
|
44
|
+
init_version = re.search(r'__version__ = "([^"]+)"', init).group(1)
|
|
45
|
+
if not (tag == version == init_version):
|
|
46
|
+
raise SystemExit(
|
|
47
|
+
f'version mismatch: tag={tag} pyproject={version} __init__={init_version}'
|
|
48
|
+
)
|
|
49
|
+
print(f'version {version} consistent across tag, pyproject.toml and __init__.py')
|
|
50
|
+
PY
|
|
51
|
+
|
|
52
|
+
build:
|
|
53
|
+
name: Build distributions
|
|
54
|
+
needs: verify
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
|
|
59
|
+
- uses: actions/setup-python@v5
|
|
60
|
+
with:
|
|
61
|
+
python-version: '3.13'
|
|
62
|
+
|
|
63
|
+
- name: Build sdist and wheel
|
|
64
|
+
run: |
|
|
65
|
+
python -m pip install --upgrade pip build
|
|
66
|
+
python -m build
|
|
67
|
+
|
|
68
|
+
- name: Inspect the artefacts
|
|
69
|
+
run: |
|
|
70
|
+
python -m pip install twine
|
|
71
|
+
twine check dist/*
|
|
72
|
+
ls -la dist/
|
|
73
|
+
|
|
74
|
+
- uses: actions/upload-artifact@v4
|
|
75
|
+
with:
|
|
76
|
+
name: dist
|
|
77
|
+
path: dist/
|
|
78
|
+
retention-days: 7
|
|
79
|
+
|
|
80
|
+
pypi:
|
|
81
|
+
name: Publish to PyPI
|
|
82
|
+
needs: build
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
# Trusted publishing (OIDC) -- no long-lived token in the repository. Set
|
|
85
|
+
# this up once at https://pypi.org/manage/account/publishing/ with:
|
|
86
|
+
# owner: ejfkdev repo: pydj
|
|
87
|
+
# workflow: release.yml environment: pypi
|
|
88
|
+
# `id-token: write` is what mints the short-lived credential.
|
|
89
|
+
environment:
|
|
90
|
+
name: pypi
|
|
91
|
+
url: https://pypi.org/p/pydj
|
|
92
|
+
permissions:
|
|
93
|
+
id-token: write
|
|
94
|
+
steps:
|
|
95
|
+
- uses: actions/download-artifact@v4
|
|
96
|
+
with:
|
|
97
|
+
name: dist
|
|
98
|
+
path: dist/
|
|
99
|
+
|
|
100
|
+
- name: Publish
|
|
101
|
+
# Falls back to a PYPI_API_TOKEN secret if one is configured, so the
|
|
102
|
+
# workflow works whether or not trusted publishing has been set up yet.
|
|
103
|
+
# Prefer OIDC: nothing to rotate, and a leaked repo cannot publish.
|
|
104
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
105
|
+
with:
|
|
106
|
+
password: ${{ secrets.PYPI_API_TOKEN || '' }}
|
|
107
|
+
# Nothing to attach the attestations to otherwise.
|
|
108
|
+
attestations: ${{ secrets.PYPI_API_TOKEN == '' }}
|
|
109
|
+
|
|
110
|
+
github-release:
|
|
111
|
+
name: Create GitHub Release
|
|
112
|
+
needs: pypi
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
permissions:
|
|
115
|
+
contents: write
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v4
|
|
118
|
+
with:
|
|
119
|
+
fetch-depth: 0
|
|
120
|
+
|
|
121
|
+
# On a tag push, checkout overwrites the local tag ref with a bare commit
|
|
122
|
+
# SHA; re-fetching the annotated tag lets --notes-from-tag read its message.
|
|
123
|
+
- name: Fetch tag annotation
|
|
124
|
+
run: git fetch --force origin "+refs/tags/${{ github.ref_name }}:refs/tags/${{ github.ref_name }}"
|
|
125
|
+
|
|
126
|
+
- uses: actions/download-artifact@v4
|
|
127
|
+
with:
|
|
128
|
+
name: dist
|
|
129
|
+
path: dist/
|
|
130
|
+
|
|
131
|
+
- name: Create release
|
|
132
|
+
env:
|
|
133
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
134
|
+
run: |
|
|
135
|
+
gh release create "${{ github.ref_name }}" dist/* \
|
|
136
|
+
--title "${{ github.ref_name }}" \
|
|
137
|
+
--notes-from-tag \
|
|
138
|
+
--verify-tag
|
|
139
|
+
|
|
140
|
+
- name: Announce the install command
|
|
141
|
+
run: |
|
|
142
|
+
echo "Published. Install with:" >> "$GITHUB_STEP_SUMMARY"
|
|
143
|
+
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
|
|
144
|
+
echo "pipx install pydj" >> "$GITHUB_STEP_SUMMARY"
|
|
145
|
+
echo "uvx pydj --help" >> "$GITHUB_STEP_SUMMARY"
|
|
146
|
+
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
pydj-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Byte-compiled / cache
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Distribution / packaging
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
develop-eggs/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
share/python-wheels/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.egg
|
|
25
|
+
MANIFEST
|
|
26
|
+
pip-wheel-metadata/
|
|
27
|
+
|
|
28
|
+
# Virtual environments
|
|
29
|
+
.venv/
|
|
30
|
+
venv/
|
|
31
|
+
ENV/
|
|
32
|
+
env/
|
|
33
|
+
env.bak/
|
|
34
|
+
venv.bak/
|
|
35
|
+
|
|
36
|
+
# Test / coverage artefacts
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.tox/
|
|
39
|
+
.nox/
|
|
40
|
+
.coverage
|
|
41
|
+
.coverage.*
|
|
42
|
+
coverage.xml
|
|
43
|
+
*.cover
|
|
44
|
+
.hypothesis/
|
|
45
|
+
htmlcov/
|
|
46
|
+
|
|
47
|
+
# Type checkers / linters
|
|
48
|
+
.mypy_cache/
|
|
49
|
+
.dmypy.json
|
|
50
|
+
.pyre/
|
|
51
|
+
.pytype/
|
|
52
|
+
.ruff_cache/
|
|
53
|
+
|
|
54
|
+
# Build tool caches
|
|
55
|
+
.uv/
|
|
56
|
+
|
|
57
|
+
# Editors / OS
|
|
58
|
+
.idea/
|
|
59
|
+
.vscode/
|
|
60
|
+
*.swp
|
|
61
|
+
*.swo
|
|
62
|
+
*~
|
|
63
|
+
.DS_Store
|
|
64
|
+
Thumbs.db
|
|
65
|
+
|
|
66
|
+
# Generated by examples/06_batch_scan.py
|
|
67
|
+
batch_results.json
|
|
68
|
+
|
|
69
|
+
# A Go checkout used as the differential oracle
|
|
70
|
+
/dj-ref
|