scraper-for-x 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.
- scraper_for_x-0.1.0/.github/workflows/ci.yml +87 -0
- scraper_for_x-0.1.0/.github/workflows/publish.yml +52 -0
- scraper_for_x-0.1.0/.gitignore +32 -0
- scraper_for_x-0.1.0/.pre-commit-config.yaml +16 -0
- scraper_for_x-0.1.0/CHANGELOG.md +23 -0
- scraper_for_x-0.1.0/CLAUDE.md +65 -0
- scraper_for_x-0.1.0/DISCLAIMER.md +54 -0
- scraper_for_x-0.1.0/LICENSE +21 -0
- scraper_for_x-0.1.0/PKG-INFO +118 -0
- scraper_for_x-0.1.0/README.md +85 -0
- scraper_for_x-0.1.0/pyproject.toml +62 -0
- scraper_for_x-0.1.0/requirements-dev.lock +34 -0
- scraper_for_x-0.1.0/scripts/check_fixtures_pii.py +94 -0
- scraper_for_x-0.1.0/scripts/check_tag_version.py +42 -0
- scraper_for_x-0.1.0/src/scraper_for_x/__init__.py +327 -0
- scraper_for_x-0.1.0/src/scraper_for_x/_stealth_init.js +8 -0
- scraper_for_x-0.1.0/src/scraper_for_x/auth.py +432 -0
- scraper_for_x-0.1.0/src/scraper_for_x/cli.py +484 -0
- scraper_for_x-0.1.0/src/scraper_for_x/client.py +124 -0
- scraper_for_x-0.1.0/src/scraper_for_x/config.py +78 -0
- scraper_for_x-0.1.0/src/scraper_for_x/errors.py +91 -0
- scraper_for_x-0.1.0/src/scraper_for_x/gql.py +178 -0
- scraper_for_x-0.1.0/src/scraper_for_x/model.py +341 -0
- scraper_for_x-0.1.0/src/scraper_for_x/parse.py +128 -0
- scraper_for_x-0.1.0/src/scraper_for_x/queryids.py +147 -0
- scraper_for_x-0.1.0/src/scraper_for_x/redact.py +182 -0
- scraper_for_x-0.1.0/src/scraper_for_x/retrieve.py +462 -0
- scraper_for_x-0.1.0/src/scraper_for_x/session.py +275 -0
- scraper_for_x-0.1.0/tests/conftest.py +18 -0
- scraper_for_x-0.1.0/tests/fixtures/search_timeline.json +124 -0
- scraper_for_x-0.1.0/tests/fixtures/tweet_detail.json +112 -0
- scraper_for_x-0.1.0/tests/fixtures/user_tweets.json +580 -0
- scraper_for_x-0.1.0/tests/test_auth.py +70 -0
- scraper_for_x-0.1.0/tests/test_cli.py +38 -0
- scraper_for_x-0.1.0/tests/test_identifier.py +158 -0
- scraper_for_x-0.1.0/tests/test_model.py +173 -0
- scraper_for_x-0.1.0/tests/test_no_scrapling_import.py +46 -0
- scraper_for_x-0.1.0/tests/test_parse.py +151 -0
- scraper_for_x-0.1.0/tests/test_redact.py +94 -0
- scraper_for_x-0.1.0/tests/test_retrieve.py +323 -0
- scraper_for_x-0.1.0/wiki/CLI-Reference.md +443 -0
- scraper_for_x-0.1.0/wiki/Configuration.md +138 -0
- scraper_for_x-0.1.0/wiki/Contributing.md +113 -0
- scraper_for_x-0.1.0/wiki/FAQ-and-Troubleshooting.md +131 -0
- scraper_for_x-0.1.0/wiki/Installation.md +87 -0
- scraper_for_x-0.1.0/wiki/Output-Schema.md +205 -0
- scraper_for_x-0.1.0/wiki/Python-API-Reference.md +502 -0
- scraper_for_x-0.1.0/wiki/Quick-Start.md +179 -0
- scraper_for_x-0.1.0/wiki/README.md +32 -0
- scraper_for_x-0.1.0/wiki/Security-and-Privacy.md +95 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
lint-and-test:
|
|
13
|
+
# macOS is the first-class, tested target (v1); the ubuntu leg exists
|
|
14
|
+
# because the fixture/parse/CLI layer is browser-agnostic, and running it
|
|
15
|
+
# on Linux too is a cheap way to make that cross-platform claim real
|
|
16
|
+
# (plan §14). Neither leg installs an actual browser binary.
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
os: [macos-latest, ubuntu-latest]
|
|
21
|
+
runs-on: ${{ matrix.os }}
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
|
|
29
|
+
- name: Install pinned dev dependencies
|
|
30
|
+
run: python -m pip install -r requirements-dev.lock
|
|
31
|
+
|
|
32
|
+
- name: Install package (dependencies already pinned above)
|
|
33
|
+
run: python -m pip install -e . --no-deps
|
|
34
|
+
|
|
35
|
+
- name: Ruff lint
|
|
36
|
+
run: ruff check .
|
|
37
|
+
|
|
38
|
+
- name: Ruff format check
|
|
39
|
+
run: ruff format --check .
|
|
40
|
+
|
|
41
|
+
- name: Fixture PII/secret scan
|
|
42
|
+
run: python scripts/check_fixtures_pii.py
|
|
43
|
+
|
|
44
|
+
- name: Run tests
|
|
45
|
+
run: pytest
|
|
46
|
+
|
|
47
|
+
build-and-smoke:
|
|
48
|
+
# This installs the BASE wheel only — no [browser] extra, so scrapling is
|
|
49
|
+
# never installed in this job. That is deliberate: scrapling is an extra
|
|
50
|
+
# for this package (unlike scrape-fb, where it's a base dependency and
|
|
51
|
+
# that package's smoke test only guards against a missing browser
|
|
52
|
+
# *binary*). Here, the load-bearing regression this job catches is an
|
|
53
|
+
# eager top-level `import scrapling` anywhere in the base import path
|
|
54
|
+
# (session.py/cli.py/__init__.py) — if that ever creeps in, `scrape-x
|
|
55
|
+
# --version` fails at import time with scrapling not even installed,
|
|
56
|
+
# and this job is the only thing that would catch it (plan §13/§14).
|
|
57
|
+
strategy:
|
|
58
|
+
fail-fast: false
|
|
59
|
+
matrix:
|
|
60
|
+
os: [macos-latest, ubuntu-latest]
|
|
61
|
+
runs-on: ${{ matrix.os }}
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
|
+
with:
|
|
67
|
+
python-version: "3.12"
|
|
68
|
+
|
|
69
|
+
- name: Install build tool
|
|
70
|
+
run: python -m pip install build
|
|
71
|
+
|
|
72
|
+
- name: Build wheel
|
|
73
|
+
run: python -m build --wheel
|
|
74
|
+
|
|
75
|
+
- name: Install built (base) wheel into a clean venv
|
|
76
|
+
# POSIX venv layout (bin/, not Scripts\) and the bash default shell
|
|
77
|
+
# both assume macos-latest/ubuntu-latest only — adding windows-latest
|
|
78
|
+
# to the matrix above would need this step rewritten, not just the
|
|
79
|
+
# matrix line (Windows unsupported in v1; see README/pyproject).
|
|
80
|
+
run: |
|
|
81
|
+
python -m venv smoke-venv
|
|
82
|
+
smoke-venv/bin/pip install dist/*.whl
|
|
83
|
+
|
|
84
|
+
- name: Smoke test the entry point (scrapling absent)
|
|
85
|
+
run: |
|
|
86
|
+
smoke-venv/bin/scrape-x --version
|
|
87
|
+
smoke-venv/bin/scrape-x --help
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Fires when a GitHub Release is published (Releases page -> "Draft a new
|
|
4
|
+
# release" -> pick/create tag vX.Y.Z -> Publish release) — NOT on a bare
|
|
5
|
+
# `git push --tags`, which creates the tag but no visible Release entry.
|
|
6
|
+
# github.ref_name still resolves to the release's tag name under this event.
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Verify tag matches pyproject.toml version
|
|
22
|
+
run: python3 scripts/check_tag_version.py "${{ github.ref_name }}"
|
|
23
|
+
|
|
24
|
+
- name: Install build tool
|
|
25
|
+
run: python -m pip install build
|
|
26
|
+
|
|
27
|
+
- name: Build sdist and wheel
|
|
28
|
+
run: python -m build
|
|
29
|
+
|
|
30
|
+
- uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
permissions:
|
|
39
|
+
# Required to mint the OIDC token for PyPI Trusted Publishing — there is
|
|
40
|
+
# no stored API token anywhere in this repo (plan §15).
|
|
41
|
+
id-token: write
|
|
42
|
+
contents: read
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
|
|
49
|
+
# Pinned to the commit for release v1.14.0 — a floating tag on an
|
|
50
|
+
# action that runs with publish credentials is a hijack vector
|
|
51
|
+
# (plan §15). Bump deliberately, not automatically.
|
|
52
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Dev environment
|
|
10
|
+
.venv/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
uv.lock
|
|
14
|
+
|
|
15
|
+
# Raw captures and runtime outputs are a PII trap — ignore by an unambiguous,
|
|
16
|
+
# distinct name/location so a safe fixture and an unsafe raw capture can never
|
|
17
|
+
# collide under one glob (see plan §4).
|
|
18
|
+
scratch/
|
|
19
|
+
*.raw.json
|
|
20
|
+
output/
|
|
21
|
+
profiles/
|
|
22
|
+
|
|
23
|
+
# NOTE: fixtures under tests/fixtures/*.json are committed, synthetic-by-
|
|
24
|
+
# construction (see scripts/check_fixtures_pii.py) and need NO un-ignore
|
|
25
|
+
# lines here, because the raw-capture ignore above is scoped to `*.raw.json`
|
|
26
|
+
# specifically, not a blanket `*.json` — a real capture and a fixture can
|
|
27
|
+
# never collide under one glob. If that ever changes to a blanket `*.json`
|
|
28
|
+
# ignore, every fixture would then need un-ignoring BY EXACT NAME, never a
|
|
29
|
+
# wildcard (`!tests/fixtures/*.json` would silently re-include any new raw
|
|
30
|
+
# capture dropped into the same directory).
|
|
31
|
+
|
|
32
|
+
.DS_Store
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.8.6
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: local
|
|
10
|
+
hooks:
|
|
11
|
+
- id: fixture-pii-scan
|
|
12
|
+
name: scan committed fixtures for PII/secrets
|
|
13
|
+
entry: python3 scripts/check_fixtures_pii.py
|
|
14
|
+
language: system
|
|
15
|
+
files: ^tests/fixtures/.*\.json$
|
|
16
|
+
pass_filenames: false
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release: `scrape-x login` / `status` / `setup` / `doctor` / `fetch` / `search` / `tweet`.
|
|
14
|
+
- Logged-in X/Twitter reads via a harvest-then-replay hybrid (stealth-browser login, `httpx` GraphQL replay). Confirmed live: `UserTweets` and `TweetDetail` need no `x-client-transaction-id`.
|
|
15
|
+
- `--limit`, `--since`/`--until` retrieval with stop-reason reporting.
|
|
16
|
+
- JSON and NDJSON output formats.
|
|
17
|
+
- Python API: `XScraper`, `Tweet`, `User`, `Media`.
|
|
18
|
+
|
|
19
|
+
### Known limitations
|
|
20
|
+
- `scrape-x search` and `scrape-x fetch --replies` (and the Python API's `search()`/`fetch_user_tweets(replies=True)`/`iter_user_tweets(replies=True)`) are **not yet implemented** — live-verified 2026-07-05 that `SearchTimeline`/`UserTweetsAndReplies` require a single-use `x-client-transaction-id` per request, which this package's harvest-then-replay architecture doesn't reproduce. Both fail fast with a clear `FeatureNotImplementedError` (exit code 1). See [wiki/FAQ-and-Troubleshooting.md](wiki/FAQ-and-Troubleshooting.md).
|
|
21
|
+
|
|
22
|
+
[Unreleased]: https://github.com/tjdwls101010/Scraper-for-X/compare/v0.1.0...HEAD
|
|
23
|
+
[0.1.0]: https://github.com/tjdwls101010/Scraper-for-X/releases/tag/v0.1.0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
|
4
|
+
|
|
5
|
+
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
|
6
|
+
|
|
7
|
+
## 1. Think Before Coding
|
|
8
|
+
|
|
9
|
+
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
|
10
|
+
|
|
11
|
+
Before implementing:
|
|
12
|
+
- State your assumptions explicitly. If uncertain, ask.
|
|
13
|
+
- If multiple interpretations exist, present them - don't pick silently.
|
|
14
|
+
- If a simpler approach exists, say so. Push back when warranted.
|
|
15
|
+
- If something is unclear, stop. Name what's confusing. Ask.
|
|
16
|
+
|
|
17
|
+
## 2. Simplicity First
|
|
18
|
+
|
|
19
|
+
**Minimum code that solves the problem. Nothing speculative.**
|
|
20
|
+
|
|
21
|
+
- No features beyond what was asked.
|
|
22
|
+
- No abstractions for single-use code.
|
|
23
|
+
- No "flexibility" or "configurability" that wasn't requested.
|
|
24
|
+
- No error handling for impossible scenarios.
|
|
25
|
+
- If you write 200 lines and it could be 50, rewrite it.
|
|
26
|
+
|
|
27
|
+
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
|
28
|
+
|
|
29
|
+
## 3. Surgical Changes
|
|
30
|
+
|
|
31
|
+
**Touch only what you must. Clean up only your own mess.**
|
|
32
|
+
|
|
33
|
+
When editing existing code:
|
|
34
|
+
- Don't "improve" adjacent code, comments, or formatting.
|
|
35
|
+
- Don't refactor things that aren't broken.
|
|
36
|
+
- Match existing style, even if you'd do it differently.
|
|
37
|
+
- If you notice unrelated dead code, mention it - don't delete it.
|
|
38
|
+
|
|
39
|
+
When your changes create orphans:
|
|
40
|
+
- Remove imports/variables/functions that YOUR changes made unused.
|
|
41
|
+
- Don't remove pre-existing dead code unless asked.
|
|
42
|
+
|
|
43
|
+
The test: Every changed line should trace directly to the user's request.
|
|
44
|
+
|
|
45
|
+
## 4. Goal-Driven Execution
|
|
46
|
+
|
|
47
|
+
**Define success criteria. Loop until verified.**
|
|
48
|
+
|
|
49
|
+
Transform tasks into verifiable goals:
|
|
50
|
+
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
|
51
|
+
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
|
52
|
+
- "Refactor X" → "Ensure tests pass before and after"
|
|
53
|
+
|
|
54
|
+
For multi-step tasks, state a brief plan:
|
|
55
|
+
```
|
|
56
|
+
1. [Step] → verify: [check]
|
|
57
|
+
2. [Step] → verify: [check]
|
|
58
|
+
3. [Step] → verify: [check]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Disclaimer — read before you use this
|
|
2
|
+
|
|
3
|
+
This is not legal advice. It is a plain-language summary of risks you take on by using this tool. If any of this matters to your situation, talk to a lawyer.
|
|
4
|
+
|
|
5
|
+
## 1. This violates X's Terms of Service
|
|
6
|
+
|
|
7
|
+
X's Terms of Service prohibit scraping and automated access without written permission. X enforces this via account suspension/termination and, historically, litigation. Using this tool is entirely at your own risk. Use a **dedicated or throwaway account**, not your primary one, and keep volume low (§9 of the design doc; the CLI's non-bypassable inter-request floor and per-run scope exist because of this).
|
|
8
|
+
|
|
9
|
+
## 2. X is notably litigious about scraping — but read the outcomes honestly
|
|
10
|
+
|
|
11
|
+
X Corp has pursued scraping-related litigation (e.g. *X Corp v. Bright Data*), but the documented outcomes and enforcement have skewed toward **commercial mass-scrapers and data brokers**, not solo personal-scale read use. The probability of that landing on any one individual doing low-volume personal reads is uncertain; the exposure is nonetheless real, and this is named here so the choice stays informed rather than assumed away.
|
|
12
|
+
|
|
13
|
+
## 3. Publishing this tool exposes its maintainer, not just its users
|
|
14
|
+
|
|
15
|
+
This package is named `scraper-for-x`, published under a real GitHub identity, and distributed via PyPI Trusted Publishing, which binds each release to a named GitHub repository and account (`tjdwls101010/Scraper-for-X`). That is a deliberate, informed choice by the maintainer — but it means the maintainer is identifiable in a way an anonymous or unpublished tool would not be. This exposure exists regardless of how the tool is actually used by others, and is recorded here so the choice stays informed.
|
|
16
|
+
|
|
17
|
+
## 4. You may become a "data controller" for other people's data
|
|
18
|
+
|
|
19
|
+
Tweets you scrape belong to other people — authors, repliers, anyone quoted or mentioned. Collecting identifiable personal data about other people can make *you* a **data controller under GDPR**, with real obligations: a lawful basis for processing, honoring data-subject access/deletion requests, and limiting retention. "I did this for personal use" is not automatically a lawful basis.
|
|
20
|
+
|
|
21
|
+
The **CCPA/CPRA** is different in kind: it regulates for-profit "businesses" that meet statutory thresholds (e.g. revenue or data-volume tests) and generally exempts purely personal/household activity — so it typically does *not* attach to a solo, personal-scale scraper. Even so, jurisdiction-specific privacy-tort and publication-of-private-facts risk can still apply outside of CCPA/CPRA's scope. Minimize what you keep, and delete captured output once you're done with it. The MIT license on this code says nothing about, and does not excuse, privacy-law obligations around the *data* you collect with it. *(Non-legal-advice.)*
|
|
22
|
+
|
|
23
|
+
## 5. Output files are not scrubbed — treat them as sensitive
|
|
24
|
+
|
|
25
|
+
Captured tweets contain third-party names, tweet text, and media URLs. This tool:
|
|
26
|
+
- never writes output to a location you'd casually commit to git (default `--output` is outside any repo; see README),
|
|
27
|
+
- never redacts the *output* files themselves (only diagnostic/verbose logs go through redaction — see below),
|
|
28
|
+
- relies on you to delete output you no longer need.
|
|
29
|
+
|
|
30
|
+
Don't commit scraped output to a public (or even private) git repository, and don't share it beyond what you'd be comfortable being responsible for under §4.
|
|
31
|
+
|
|
32
|
+
## 6. Diagnostics are redacted — but redaction is not a certification
|
|
33
|
+
|
|
34
|
+
`-v`/`--verbose` output, error dumps, and anything printed to your terminal are passed through a single redaction path that strips session-token-shaped fields, `pbs.twimg.com`/`video.twimg.com` query strings, and truncates tweet text and names. This includes **cookie-import parse errors** — a malformed cookie export file is reported by line/field position ("line 3 malformed", "ct0 failed shape check"), never by echoing the raw cookie value. This reduces accidental leakage into terminal scrollback, bug reports, or screenshots — it is **not** a guarantee that every sensitive value is caught, and it does not apply to the actual `--output` file, which is the full, unredacted capture by design (that's the point of the tool). `--raw --no-redact` disables this path entirely for debugging; only use it locally.
|
|
35
|
+
|
|
36
|
+
## 7. Account-ban risk, and how to reduce it
|
|
37
|
+
|
|
38
|
+
Automating an X account — even just reading, via a real logged-in session — violates X's Terms of Service and X is aggressive about flagging automation. To reduce (not eliminate) the risk of a suspension or challenge:
|
|
39
|
+
- Use a **dedicated or throwaway account**, never your primary one.
|
|
40
|
+
- Keep volume low: shallow, recent fetches are safer than deep archival pulls. Deeper `--since`/`--limit` runs make more requests and raise both rate-limit and account-flag risk.
|
|
41
|
+
- Run `scrape-x` from the **same network/IP** where the session was originally established (`scrape-x login`, or wherever you exported cookies from). An abrupt IP or client change against an existing session — especially pairing a cookie-imported session with a datacenter/VPN IP — is exactly the kind of signal X's abuse systems weight, and can soft-lock the session even without an outright ban.
|
|
42
|
+
- Never run this in a loop or scheduler; there is no built-in scheduler/daemon, and none will be added.
|
|
43
|
+
|
|
44
|
+
## 8. Your session credential is a live, password-less login — protect it
|
|
45
|
+
|
|
46
|
+
`scrape-x login` (or cookie import) persists your X session as `{auth_token, ct0, user-agent}` to a directory on disk, permissioned `0700` with the credential file itself `0600`. Anyone who can read that file has authenticated, password-less access to your X account — no password or 2FA required, because the session already satisfied both. Concretely:
|
|
47
|
+
- **Do not** back this up to Time Machine, sync it via iCloud/Dropbox, or commit it anywhere.
|
|
48
|
+
- If you import cookies from an exported file, that source file *also* still contains a live session after import — delete or secure it; this tool never deletes it for you.
|
|
49
|
+
- If the machine or disk is lost or compromised, **revoke the session immediately** by logging out of that session on x.com (Settings → Security and account access → Apps and sessions, or equivalent), not just by deleting the local directory.
|
|
50
|
+
- A printed reminder from this tool (at login, or on cookie import) is exactly that — a reminder. **It is not a technical control.** Nothing in this tool encrypts the credential at rest or prevents you from copying it elsewhere; the `0700`/`0600` permissions and the reminder are the entire enforcement mechanism.
|
|
51
|
+
|
|
52
|
+
## 9. No warranty
|
|
53
|
+
|
|
54
|
+
This software is provided "as is" under the MIT License, without warranty of any kind. See [LICENSE](LICENSE). X's internal API can and does change without notice — query-ids rotate, response shapes drift — and this tool may stop working, or silently return incomplete data, at any time (see the design doc's durability section).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tjdwls101010
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scraper-for-x
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only scraping of logged-in X/Twitter data via a harvest-then-replay hybrid: a stealth-browser login harvests the session once, then reads are replayed over httpx. No client-transaction-id replay.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tjdwls101010/Scraper-for-X
|
|
6
|
+
Project-URL: Issues, https://github.com/tjdwls101010/Scraper-for-X/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/tjdwls101010/Scraper-for-X/blob/main/CHANGELOG.md
|
|
8
|
+
Author: tjdwls101010
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: graphql,scraper,scraping,twitter,x
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: platformdirs>=4.0
|
|
25
|
+
Provides-Extra: browser
|
|
26
|
+
Requires-Dist: scrapling[fetchers]<0.5,>=0.4.10; extra == 'browser'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: pre-commit>=3.8; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# scraper-for-x
|
|
35
|
+
|
|
36
|
+
Read-only scraping of logged-in **X/Twitter** data via a **harvest-then-replay hybrid**: a stealth browser (or a cookie import) logs you in once and harvests the session, then every read afterward is a plain `httpx` GraphQL request — no browser in the loop, no `x-client-transaction-id` replay.
|
|
37
|
+
|
|
38
|
+
> **Read [DISCLAIMER.md](DISCLAIMER.md) before using this.** Using this tool violates X's Terms of Service, publishing it exposes its maintainer, and scraping other people's tweets can make *you* a data controller over their personal data under GDPR. Use a dedicated/throwaway account, not your primary one.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Base install — cookie-import login only, **no browser dependency**:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install scraper-for-x
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `[browser]` extra — adds a stealth browser for `scrape-x login`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install "scraper-for-x[browser]"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If you only ever import cookies from a session you already have (e.g. exported from your own logged-in browser), the base install is all you need.
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# 1. One-time interactive login — opens a real browser window, you log in by hand.
|
|
60
|
+
scrape-x login
|
|
61
|
+
|
|
62
|
+
# 2. Fetch a profile's tweets.
|
|
63
|
+
scrape-x fetch nasa --limit 50
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CLI overview
|
|
67
|
+
|
|
68
|
+
| Command | Purpose |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `scrape-x login` | One-time login: headed stealth browser by default, or `--cookies FILE` to import an existing session |
|
|
71
|
+
| `scrape-x status` | Check whether the persisted session is logged in, expired, or rate-limited |
|
|
72
|
+
| `scrape-x setup` | Provision the login browser into an isolated cache (requires `[browser]`) |
|
|
73
|
+
| `scrape-x doctor` | Authenticated round-trip + query-id freshness check (`--refresh` re-anchors query-ids from x.com's `main.js`, browser-free) |
|
|
74
|
+
| `scrape-x fetch <identifier>` | A profile's tweets/media (`--limit`, `--since`, `--until`, `--by screen_name\|id`) — **`--replies` not yet implemented, see below** |
|
|
75
|
+
| `scrape-x search <query>` | **Not yet implemented, see below** |
|
|
76
|
+
| `scrape-x tweet <identifier>` | A single tweet plus its reply/conversation thread (`--replies`) |
|
|
77
|
+
|
|
78
|
+
`fetch`/`search`/`tweet` all share `--format json|ndjson`, `--output PATH`, `--profile NAME`, `--profile-dir PATH`, `--wait-on-limit`, `--max-wait`, `--raw` (+ `--no-redact`), and `-v/--verbose`. See the [CLI Reference](wiki/CLI-Reference.md) for every flag and exit code.
|
|
79
|
+
|
|
80
|
+
### Known limitation: `search` and `fetch --replies` (v0.1.0)
|
|
81
|
+
|
|
82
|
+
Live-verified 2026-07-05: X's `SearchTimeline` and `UserTweetsAndReplies` GraphQL operations require a fresh, **single-use** `x-client-transaction-id` header on every request — unlike plain profile fetches and single-tweet lookups, which this package proved work over plain `httpx` replay with no such header. A captured transaction-id cannot be harvested once and reused like a session cookie or query-id; reproducing X's generator for it is exactly the fragility this package's harvest-then-replay architecture was built to avoid (see [twikit#408](https://github.com/d60/twikit/issues/408)).
|
|
83
|
+
|
|
84
|
+
Both `scrape-x search` and `scrape-x fetch --replies` fail fast with a clear `FeatureNotImplementedError` (exit code 1) rather than a confusing network error. A browser-observe fallback for these two operations specifically (the same approach `scrape-fb` uses) is on the roadmap — see [wiki/FAQ-and-Troubleshooting.md](wiki/FAQ-and-Troubleshooting.md).
|
|
85
|
+
|
|
86
|
+
## Python API
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from scraper_for_x import XScraper
|
|
90
|
+
|
|
91
|
+
XScraper(profile="default").login() # one-time, opens a headed browser
|
|
92
|
+
|
|
93
|
+
with XScraper(profile="default") as x:
|
|
94
|
+
tweets = x.fetch_user_tweets("nasa", limit=50)
|
|
95
|
+
for tweet in x.iter_user_tweets("nasa", limit=50):
|
|
96
|
+
... # must be consumed inside the `with` block
|
|
97
|
+
|
|
98
|
+
x.fetch_tweet("https://x.com/nasa/status/1234567890")
|
|
99
|
+
# x.search(...) raises FeatureNotImplementedError -- see "Known limitation" above.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Documentation
|
|
103
|
+
|
|
104
|
+
This README covers the essentials. For everything else, see the **[wiki](wiki/README.md)**:
|
|
105
|
+
|
|
106
|
+
- [Installation](wiki/Installation.md)
|
|
107
|
+
- [Quick Start](wiki/Quick-Start.md)
|
|
108
|
+
- [CLI Reference](wiki/CLI-Reference.md) — every flag, every exit code
|
|
109
|
+
- [Python API Reference](wiki/Python-API-Reference.md)
|
|
110
|
+
- [Configuration](wiki/Configuration.md) — profiles, environment variables
|
|
111
|
+
- [Output Schema](wiki/Output-Schema.md) — every `Tweet`/`User`/`Media` field
|
|
112
|
+
- [Security and Privacy](wiki/Security-and-Privacy.md) — the full threat model behind [DISCLAIMER.md](DISCLAIMER.md)
|
|
113
|
+
- [FAQ and Troubleshooting](wiki/FAQ-and-Troubleshooting.md)
|
|
114
|
+
- [Contributing](wiki/Contributing.md)
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT — see [LICENSE](LICENSE). The license covers the code; it does not cover what you do with the data you collect (see [DISCLAIMER.md](DISCLAIMER.md)).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# scraper-for-x
|
|
2
|
+
|
|
3
|
+
Read-only scraping of logged-in **X/Twitter** data via a **harvest-then-replay hybrid**: a stealth browser (or a cookie import) logs you in once and harvests the session, then every read afterward is a plain `httpx` GraphQL request — no browser in the loop, no `x-client-transaction-id` replay.
|
|
4
|
+
|
|
5
|
+
> **Read [DISCLAIMER.md](DISCLAIMER.md) before using this.** Using this tool violates X's Terms of Service, publishing it exposes its maintainer, and scraping other people's tweets can make *you* a data controller over their personal data under GDPR. Use a dedicated/throwaway account, not your primary one.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Base install — cookie-import login only, **no browser dependency**:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install scraper-for-x
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The `[browser]` extra — adds a stealth browser for `scrape-x login`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "scraper-for-x[browser]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If you only ever import cookies from a session you already have (e.g. exported from your own logged-in browser), the base install is all you need.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. One-time interactive login — opens a real browser window, you log in by hand.
|
|
27
|
+
scrape-x login
|
|
28
|
+
|
|
29
|
+
# 2. Fetch a profile's tweets.
|
|
30
|
+
scrape-x fetch nasa --limit 50
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## CLI overview
|
|
34
|
+
|
|
35
|
+
| Command | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `scrape-x login` | One-time login: headed stealth browser by default, or `--cookies FILE` to import an existing session |
|
|
38
|
+
| `scrape-x status` | Check whether the persisted session is logged in, expired, or rate-limited |
|
|
39
|
+
| `scrape-x setup` | Provision the login browser into an isolated cache (requires `[browser]`) |
|
|
40
|
+
| `scrape-x doctor` | Authenticated round-trip + query-id freshness check (`--refresh` re-anchors query-ids from x.com's `main.js`, browser-free) |
|
|
41
|
+
| `scrape-x fetch <identifier>` | A profile's tweets/media (`--limit`, `--since`, `--until`, `--by screen_name\|id`) — **`--replies` not yet implemented, see below** |
|
|
42
|
+
| `scrape-x search <query>` | **Not yet implemented, see below** |
|
|
43
|
+
| `scrape-x tweet <identifier>` | A single tweet plus its reply/conversation thread (`--replies`) |
|
|
44
|
+
|
|
45
|
+
`fetch`/`search`/`tweet` all share `--format json|ndjson`, `--output PATH`, `--profile NAME`, `--profile-dir PATH`, `--wait-on-limit`, `--max-wait`, `--raw` (+ `--no-redact`), and `-v/--verbose`. See the [CLI Reference](wiki/CLI-Reference.md) for every flag and exit code.
|
|
46
|
+
|
|
47
|
+
### Known limitation: `search` and `fetch --replies` (v0.1.0)
|
|
48
|
+
|
|
49
|
+
Live-verified 2026-07-05: X's `SearchTimeline` and `UserTweetsAndReplies` GraphQL operations require a fresh, **single-use** `x-client-transaction-id` header on every request — unlike plain profile fetches and single-tweet lookups, which this package proved work over plain `httpx` replay with no such header. A captured transaction-id cannot be harvested once and reused like a session cookie or query-id; reproducing X's generator for it is exactly the fragility this package's harvest-then-replay architecture was built to avoid (see [twikit#408](https://github.com/d60/twikit/issues/408)).
|
|
50
|
+
|
|
51
|
+
Both `scrape-x search` and `scrape-x fetch --replies` fail fast with a clear `FeatureNotImplementedError` (exit code 1) rather than a confusing network error. A browser-observe fallback for these two operations specifically (the same approach `scrape-fb` uses) is on the roadmap — see [wiki/FAQ-and-Troubleshooting.md](wiki/FAQ-and-Troubleshooting.md).
|
|
52
|
+
|
|
53
|
+
## Python API
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from scraper_for_x import XScraper
|
|
57
|
+
|
|
58
|
+
XScraper(profile="default").login() # one-time, opens a headed browser
|
|
59
|
+
|
|
60
|
+
with XScraper(profile="default") as x:
|
|
61
|
+
tweets = x.fetch_user_tweets("nasa", limit=50)
|
|
62
|
+
for tweet in x.iter_user_tweets("nasa", limit=50):
|
|
63
|
+
... # must be consumed inside the `with` block
|
|
64
|
+
|
|
65
|
+
x.fetch_tweet("https://x.com/nasa/status/1234567890")
|
|
66
|
+
# x.search(...) raises FeatureNotImplementedError -- see "Known limitation" above.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
This README covers the essentials. For everything else, see the **[wiki](wiki/README.md)**:
|
|
72
|
+
|
|
73
|
+
- [Installation](wiki/Installation.md)
|
|
74
|
+
- [Quick Start](wiki/Quick-Start.md)
|
|
75
|
+
- [CLI Reference](wiki/CLI-Reference.md) — every flag, every exit code
|
|
76
|
+
- [Python API Reference](wiki/Python-API-Reference.md)
|
|
77
|
+
- [Configuration](wiki/Configuration.md) — profiles, environment variables
|
|
78
|
+
- [Output Schema](wiki/Output-Schema.md) — every `Tweet`/`User`/`Media` field
|
|
79
|
+
- [Security and Privacy](wiki/Security-and-Privacy.md) — the full threat model behind [DISCLAIMER.md](DISCLAIMER.md)
|
|
80
|
+
- [FAQ and Troubleshooting](wiki/FAQ-and-Troubleshooting.md)
|
|
81
|
+
- [Contributing](wiki/Contributing.md)
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT — see [LICENSE](LICENSE). The license covers the code; it does not cover what you do with the data you collect (see [DISCLAIMER.md](DISCLAIMER.md)).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scraper-for-x"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read-only scraping of logged-in X/Twitter data via a harvest-then-replay hybrid: a stealth-browser login harvests the session once, then reads are replayed over httpx. No client-transaction-id replay."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [{ name = "tjdwls101010" }]
|
|
14
|
+
keywords = ["twitter", "x", "scraper", "scraping", "graphql"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.27",
|
|
29
|
+
"platformdirs>=4.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
browser = [
|
|
34
|
+
"scrapling[fetchers]>=0.4.10,<0.5",
|
|
35
|
+
]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0",
|
|
38
|
+
"ruff>=0.8",
|
|
39
|
+
"pre-commit>=3.8",
|
|
40
|
+
"build>=1.2",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/tjdwls101010/Scraper-for-X"
|
|
45
|
+
Issues = "https://github.com/tjdwls101010/Scraper-for-X/issues"
|
|
46
|
+
Changelog = "https://github.com/tjdwls101010/Scraper-for-X/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
scrape-x = "scraper_for_x.cli:main"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/scraper_for_x"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = "py311"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Pinned dev/CI dependency snapshot for reproducible CI runs (plan §14).
|
|
2
|
+
# NOT the published package's dependency spec — that lives in pyproject.toml.
|
|
3
|
+
# Regenerate with: uv pip freeze --python .venv/bin/python | grep -v '^scraper\|^-e '
|
|
4
|
+
#
|
|
5
|
+
# Deliberately does NOT include scrapling: the base install (and this repo's
|
|
6
|
+
# CI lint-and-test job) never needs it -- only the wheel-install smoke test's
|
|
7
|
+
# explicit job is to confirm scrapling's ABSENCE from a clean base install
|
|
8
|
+
# (plan §13/§14 G-lazy-import). `scrapling[fetchers]` lives solely under the
|
|
9
|
+
# `[browser]` extra in pyproject.toml.
|
|
10
|
+
anyio==4.14.1
|
|
11
|
+
build==1.5.0
|
|
12
|
+
certifi==2026.6.17
|
|
13
|
+
cfgv==3.5.0
|
|
14
|
+
distlib==0.4.3
|
|
15
|
+
filelock==3.29.5
|
|
16
|
+
h11==0.16.0
|
|
17
|
+
httpcore==1.0.9
|
|
18
|
+
httpx==0.28.1
|
|
19
|
+
identify==2.6.19
|
|
20
|
+
idna==3.18
|
|
21
|
+
iniconfig==2.3.0
|
|
22
|
+
nodeenv==1.10.0
|
|
23
|
+
packaging==26.2
|
|
24
|
+
platformdirs==4.10.0
|
|
25
|
+
pluggy==1.6.0
|
|
26
|
+
pre-commit==4.6.0
|
|
27
|
+
pygments==2.20.0
|
|
28
|
+
pyproject-hooks==1.2.0
|
|
29
|
+
pytest==9.1.1
|
|
30
|
+
python-discovery==1.4.3
|
|
31
|
+
pyyaml==6.0.3
|
|
32
|
+
ruff==0.15.20
|
|
33
|
+
typing-extensions==4.16.0
|
|
34
|
+
virtualenv==21.5.1
|