pyXcom 0.6.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.
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ .clean-venv/
3
+ dist/
4
+ *.egg-info/
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ output/
9
+ data/
10
+ *.cookies.json
11
+ .env
12
+
13
+ .omx/
14
+ .mypy_cache/
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use pyXcom in your research, please cite this software and report the version used."
3
+ type: software
4
+ title: "pyXcom: Structured and Auditable X Data Collection for Communication Research"
5
+ authors:
6
+ - family-names: Wang
7
+ given-names: Haocheng
8
+ affiliation: Communication University of China
9
+ version: 0.6.0
10
+ date-released: 2026-09-30
11
+ license: MIT
12
+ repository-code: "https://github.com/haochengw372-hash/pyXcom"
13
+ url: "https://github.com/haochengw372-hash/pyXcom"
14
+ abstract: "A Python package for collecting public X profiles, authored posts and replies, keyword matches, and bounded multi-level conversations with linked tables and resumable collection."
15
+ keywords:
16
+ - computational communication
17
+ - social media
18
+ - X
19
+ - data collection
20
+ - Python
pyxcom-0.6.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Haocheng Wang
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.
pyxcom-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyXcom
3
+ Version: 0.6.0
4
+ Summary: Collect public X profiles, posts, replies, and search results with your existing browser session
5
+ Project-URL: Homepage, https://github.com/haochengw372-hash/pyXcom
6
+ Project-URL: Repository, https://github.com/haochengw372-hash/pyXcom
7
+ Author: Haocheng Wang
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Internet :: WWW/HTTP
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: beautifulsoup4<5,>=4.12
15
+ Requires-Dist: browser-cookie3<1,>=0.19
16
+ Requires-Dist: httpx<1,>=0.27
17
+ Description-Content-Type: text/markdown
18
+
19
+ # pyXcom
20
+
21
+ **使用前请自行打开 Chrome 并登录 X。pyXcom 不会打开或控制浏览器,也不会替你登录。**
22
+
23
+ Collect public X user profiles, main posts, authored replies and keyword matches using your existing Chrome or Edge session. Outputs are three linked CSV tables: `users.csv`, `posts.csv` and `comments.csv`.
24
+
25
+ The interface follows [PykTok](https://github.com/dfreelon/pyktok)'s approachable get/save convention, with explicit parameters and resumable datasets. pyXcom is an independent implementation; it does not depend on PykTok or twikit.
26
+
27
+ ## Install
28
+
29
+ Python 3.10 or newer:
30
+
31
+ ```bash
32
+ python -m pip install pyXcom
33
+ ```
34
+
35
+ From a cloned repository, use `python -m pip install .`.
36
+
37
+ Or in a development environment:
38
+
39
+ ```bash
40
+ python3 -m venv .venv
41
+ .venv/bin/python -m pip install -e .
42
+ ```
43
+
44
+ The distribution name is `pyXcom`; the import and command are `pyxcom`.
45
+
46
+ ## Quick start
47
+
48
+ 1. Open Chrome yourself and sign in to X.
49
+ 2. Find your profile at `chrome://version` → **Profile Path**. Use its folder name, such as `Default` or `Profile 3`.
50
+ 3. Run:
51
+
52
+ ```python
53
+ from pyxcom import XClient
54
+
55
+ with XClient(profile="Default") as client:
56
+ user = client.get_user("thsottiaux")
57
+ print(user.name, user.followers_count)
58
+
59
+ result = client.save_user_activity(
60
+ "thsottiaux",
61
+ output_dir="output/tibo_year",
62
+ since="2025-09-29",
63
+ until="2026-09-30",
64
+ max_pages=2,
65
+ )
66
+ print(result.to_dict())
67
+ ```
68
+
69
+ Repeat the same call and output directory to resume. `max_pages=2` fetches at most two pages **per timeline per call**. Remove this option to continue paging until the source ends or the date boundary is reached. A saved partial result reports its stop reason.
70
+
71
+ Optional constructor settings include `browser="edge"`, `cookie_db=...`, `proxy=...`, `delay=1.0`, and `timeout=30`. Cookies are read into memory and sent only to X. No passwords or exported cookie files are needed.
72
+
73
+ ## Python functions
74
+
75
+ Methods belong to `XClient`. Every method lists its accepted parameters explicitly.
76
+
77
+ | Task | Get into memory | Stream records | Save a dataset |
78
+ | --- | --- | --- | --- |
79
+ | User profile | `get_user(handle)` | — | — |
80
+ | Post by ID or URL | `get_post(post_id_or_url)` | — | — |
81
+ | Multiple post IDs | `get_posts(post_ids)` | — | — |
82
+ | Authored main posts | `get_user_posts(handle, ...)` | `iter_user_posts(handle, ...)` | `save_user_posts(handle, output_dir, ...)` |
83
+ | Authored replies | `get_user_replies(handle, ...)` | `iter_user_replies(handle, ...)` | `save_user_replies(handle, output_dir, ...)` |
84
+ | Replies below a main post | `get_post_comments(post_id_or_url, ...)` | `iter_post_comments(post_id_or_url, ...)` | `save_post_comments(post_id_or_url, output_dir, ...)` |
85
+ | Keyword matches | `get_search_posts(keyword, ...)` | `iter_search_posts(keyword, ...)` | `save_search_posts(keyword, output_dir, ...)` |
86
+ | Main posts + replies | — | — | `save_user_activity(handle, output_dir, ...)` |
87
+ | Multiple accounts | — | — | `save_users_activity(handles, output_dir, ...)` |
88
+
89
+ - `get_*` returns a `Profile`, `Post`, or `list[Post]`; it does not write a dataset.
90
+ - `iter_*` returns an iterator. Requests happen as it is consumed.
91
+ - `save_*` writes the standard dataset and returns `CollectionResult` with `output_dir`, `post_count`, `pages_fetched`, `complete`, and `reason`. `post_count` counts **all collected observations**, including replies; use the output manifest for separate table counts.
92
+ - Parameters: `handle` for one account, `handles` for several, `keyword` for search, `output_dir` for saved datasets, and `since`/`until` for dates. Options are keyword-only.
93
+ - Dates are UTC: `since` inclusive, `until` exclusive. `max_pages` and `limit` must be positive integers or `None`. Saved timeline/mirror `limit` is a page-boundary stopping threshold, so a final page can exceed it; iterator limits are exact.
94
+
95
+ See [API reference](docs/api.md) for signatures, return types and compatibility names.
96
+
97
+ ### Search one account
98
+
99
+ ```python
100
+ with XClient(profile="Default") as client:
101
+ matches = client.get_search_posts(
102
+ "reset", handle="thsottiaux",
103
+ since="2025-09-29", until="2026-09-30", limit=20,
104
+ )
105
+ result = client.save_search_posts(
106
+ "reset", output_dir="output/tibo_reset",
107
+ handle="thsottiaux", since="2025-09-29", until="2026-09-30",
108
+ )
109
+ ```
110
+
111
+ Direct account search scans main posts and authored replies, then matches a literal word/phrase without case sensitivity. Matching posts do not represent deduplicated real-world events. Cross-user search requires an explicit `mirror_base`; it sends search terms to that mirror and retrieves discovered post details from X. No mirror is enabled by default.
112
+
113
+ ### Collect several accounts
114
+
115
+ ```python
116
+ with XClient(profile="Default") as client:
117
+ result = client.save_users_activity(
118
+ ["OpenAI", "AnthropicAI", "thsottiaux", "sama", "alexalbert__", "bcherny"],
119
+ output_dir="output/ai_year",
120
+ since="2025-09-29", until="2026-09-30",
121
+ pages_per_round=5, rounds=1,
122
+ )
123
+ ```
124
+
125
+ Repeat to resume. `rounds=0, wait_on_rate_limit=True` keeps running and waits for rate windows when needed. `progress=callback` receives status dictionaries.
126
+
127
+ ## Collect a main post and nested comments
128
+
129
+ ```python
130
+ with XClient(profile="Default") as client:
131
+ result = client.save_post_comments(
132
+ "1973931546550894681",
133
+ output_dir="output/tibo_conversation",
134
+ max_depth=2,
135
+ max_comments=100,
136
+ max_pages=20,
137
+ )
138
+ print(result.complete, result.reason)
139
+ ```
140
+
141
+ ```bash
142
+ pyxcom post-comments 1973931546550894681 --profile Default \
143
+ --max-depth 2 --max-comments 100 --max-pages 20 \
144
+ --output-dir output/tibo_conversation
145
+ ```
146
+
147
+ The root belongs in `posts.csv`, replies from **all visible authors** in `comments.csv`, and author profiles in `users.csv`. The root must be a main post. Level 1 replies directly to it; level 2 replies to those comments. Quoted content, unrelated recommendations and comments beyond the requested depth are excluded. Profile details absent from the response remain unavailable.
148
+
149
+ `max_comments` is an exact total comment cap, excluding the root. `max_pages` caps conversation requests per call. Repeat the same root/depth/output to resume saved branches and cursors; raise the comment cap to continue a capped dataset. Pending page records are retained, so stopping midway through a page does not skip them. Changing depth requires a separate dataset. Python accepts `None` for unlimited comment/page budgets; depth must be positive.
150
+
151
+ Stop reasons include `comment_limit`, `page_limit`, `rate_limited`, `partial_conversation`, and `visible_source_end`. `complete=true` means the visible queue was exhausted **within the requested depth**, not that every comment on X was recovered. Missing ancestors and stalled pagination are reported as partial. `iter_post_comments` / `get_post_comments` return reply records without writing a dataset; use `save_post_comments` when you need coverage status and resumability.
152
+
153
+ ## Output
154
+
155
+ ```text
156
+ output/tibo_year/
157
+ ├── users.csv
158
+ ├── posts.csv
159
+ ├── comments.csv
160
+ ├── manifest.json
161
+ └── .pyxcom/ # Checkpoints, observations, source streams and reports
162
+ ```
163
+
164
+ | Table | Unit | Main keys |
165
+ | --- | --- | --- |
166
+ | `users.csv` | One user | `user_id`, `username`, `display_name` |
167
+ | `posts.csv` | One original or quote main post | `post_id`, `author_id`, `post_type`, `quoted_post_id` |
168
+ | `comments.csv` | One reply at any observed depth | `comment_id`, `author_id`, `root_post_id`, `parent_post_id`, `depth` |
169
+
170
+ Replies to oneself remain comments. A quote within a reply retains its quote link in `comments.csv`. Direct replies are depth 1; replies to those are depth 2. Missing ancestry leaves `depth` empty with a `depth_status`; `parent_in_dataset` and `root_in_dataset` indicate observed relationships. Do not interpret missing parents as first-level replies.
171
+
172
+ `user_replies` means replies **authored by the selected account**. Use `*_post_comments` to collect replies below a particular main post from all visible authors, including nested replies. Neither endpoint establishes full conversation coverage.
173
+
174
+ CSV conventions: UTF-8 with BOM, snake_case columns, string IDs (import as text in Excel), JSON arrays for list fields, empty values for unavailable scalars, UTC timestamp fields. Users without a saved profile have `profile_available=false`. Engagement counts are snapshots; unknown values are not replaced with zero.
175
+
176
+ If source records include pure reposts, an additional `interactions.csv` preserves them. The current authored-account collector does not establish complete repost activity. `manifest.json` records schema/layout versions, table counts, collection scope, relationship coverage and hashes. `.pyxcom/` must be retained for resuming; the three CSV files can be shared independently for analysis.
177
+
178
+ ## Command line
179
+
180
+ CLI task names correspond to the Python methods:
181
+
182
+ ```bash
183
+ pyxcom user thsottiaux --profile Default
184
+ pyxcom user-posts thsottiaux --profile Default --output-dir output/tibo_posts
185
+ pyxcom user-replies thsottiaux --profile Default --output-dir output/tibo_replies
186
+ pyxcom user-activity thsottiaux --profile Default \
187
+ --since 2025-09-29 --until 2026-09-30 --output-dir output/tibo_year
188
+ pyxcom search-posts reset --handle thsottiaux --profile Default \
189
+ --since 2025-09-29 --until 2026-09-30 --output-dir output/tibo_reset
190
+ pyxcom users-activity --handles OpenAI AnthropicAI --profile Default \
191
+ --since 2025-09-29 --until 2026-09-30 --output-dir output/ai_year
192
+ ```
193
+
194
+ Add `--proxy URL` if your network requires one. Use `pyxcom COMMAND --help` for task options. Individual `user`, `post`, and `raw-post` commands accept `--output FILE` for a JSON file.
195
+
196
+ ## Export, migration and verification
197
+
198
+ These operations work offline, without browser credentials:
199
+
200
+ ```python
201
+ from pyxcom import export_tables, validate_collection, finalize_collection
202
+
203
+ export_tables("output/ai_year")
204
+ print(validate_collection("output/ai_year"))
205
+ # Rebuild an interrupted dataset from its saved observations:
206
+ finalize_collection("output/ai_year")
207
+ ```
208
+
209
+ ```bash
210
+ pyxcom export --output-dir output/ai_year
211
+ pyxcom validate --output-dir output/ai_year
212
+ pyxcom finalize --output-dir output/ai_year
213
+ pyxcom schema --output-dir output/ai_year
214
+ ```
215
+
216
+ Opening/exporting a legacy dataset migrates its recognized internal files into `.pyxcom/`, with originals backed up under `.pyxcom/legacy/`. It publishes the three tables at the root; the previous mixed `posts.csv` is retained internally. Unrelated user files are left in place. Conflicting migration destinations fail rather than overwrite. `schema --apply` also backfills classification fields in older records; field reports live under `.pyxcom/`.
217
+
218
+ ## Compatibility and limitations
219
+
220
+ Old Python names `get_search`, `iter_search`, `save_search` (with `user=`), and `save_accounts` remain supported. Prefer `*_search_posts` (with `handle=`) and `save_users_activity` for new code. `timeline="replies"` still works, but the explicit `*_user_replies` methods are clearer. Timeline outputs exclude same-author context of the other role. Resuming an older timeline applies the same rule and preserves its original observations in an internal compressed backup. Combined activity keeps both roles.
221
+
222
+ Old CLI names `profile`, `posts`, `activity`, `search`, `batch`, `--user` and dataset `--output` remain aliases. Existing code reading root `posts.csv` as a mixed table must adapt: it now contains main posts only; replies are in `comments.csv`.
223
+
224
+ X endpoints may change or impose limits. pyXcom discovers current GraphQL query IDs from X's web bundle, while response parsers still need maintenance. A completed run indicates the requested date boundary or visible source end was reached, not proof of all historical content. Deleted, protected or unavailable posts cannot be recovered. Output migration alone does not fetch comments; call `save_post_comments` to acquire them from X.
225
+
226
+
227
+ ## License
228
+
229
+ pyXcom is released under the [MIT License](LICENSE). Copyright © 2026 Haocheng Wang.
230
+
231
+ ## Citation
232
+
233
+ **Author:** Haocheng Wang, Communication University of China.
234
+
235
+ If you use pyXcom in a paper, thesis, dataset, or other research output, please cite the software and report the version used. GitHub's **Cite this repository** menu reads the machine-readable [CITATION.cff](CITATION.cff).
236
+
237
+ **Suggested reference**
238
+
239
+ Wang, H. (2026). *pyXcom: Structured and auditable X data collection for communication research* (Version 0.6.0) [Computer software]. https://github.com/haochengw372-hash/pyXcom
240
+
241
+ **BibTeX**
242
+
243
+ ```bibtex
244
+ @software{wang2026pyxcom,
245
+ author = {Wang, Haocheng},
246
+ title = {{pyXcom}: Structured and Auditable X Data Collection for Communication Research},
247
+ year = {2026},
248
+ version = {0.6.0},
249
+ url = {https://github.com/haochengw372-hash/pyXcom}
250
+ }
251
+ ```
252
+
253
+ For reproducible reporting, also describe the collection dates, account or keyword scope, comment-depth limits, package version, and coverage/stop reasons recorded in the output manifest. No DOI or published-paper citation is currently assigned; this reference cites the software itself. Citation is appreciated and does not add a condition to the MIT license.
pyxcom-0.6.0/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # pyXcom
2
+
3
+ **使用前请自行打开 Chrome 并登录 X。pyXcom 不会打开或控制浏览器,也不会替你登录。**
4
+
5
+ Collect public X user profiles, main posts, authored replies and keyword matches using your existing Chrome or Edge session. Outputs are three linked CSV tables: `users.csv`, `posts.csv` and `comments.csv`.
6
+
7
+ The interface follows [PykTok](https://github.com/dfreelon/pyktok)'s approachable get/save convention, with explicit parameters and resumable datasets. pyXcom is an independent implementation; it does not depend on PykTok or twikit.
8
+
9
+ ## Install
10
+
11
+ Python 3.10 or newer:
12
+
13
+ ```bash
14
+ python -m pip install pyXcom
15
+ ```
16
+
17
+ From a cloned repository, use `python -m pip install .`.
18
+
19
+ Or in a development environment:
20
+
21
+ ```bash
22
+ python3 -m venv .venv
23
+ .venv/bin/python -m pip install -e .
24
+ ```
25
+
26
+ The distribution name is `pyXcom`; the import and command are `pyxcom`.
27
+
28
+ ## Quick start
29
+
30
+ 1. Open Chrome yourself and sign in to X.
31
+ 2. Find your profile at `chrome://version` → **Profile Path**. Use its folder name, such as `Default` or `Profile 3`.
32
+ 3. Run:
33
+
34
+ ```python
35
+ from pyxcom import XClient
36
+
37
+ with XClient(profile="Default") as client:
38
+ user = client.get_user("thsottiaux")
39
+ print(user.name, user.followers_count)
40
+
41
+ result = client.save_user_activity(
42
+ "thsottiaux",
43
+ output_dir="output/tibo_year",
44
+ since="2025-09-29",
45
+ until="2026-09-30",
46
+ max_pages=2,
47
+ )
48
+ print(result.to_dict())
49
+ ```
50
+
51
+ Repeat the same call and output directory to resume. `max_pages=2` fetches at most two pages **per timeline per call**. Remove this option to continue paging until the source ends or the date boundary is reached. A saved partial result reports its stop reason.
52
+
53
+ Optional constructor settings include `browser="edge"`, `cookie_db=...`, `proxy=...`, `delay=1.0`, and `timeout=30`. Cookies are read into memory and sent only to X. No passwords or exported cookie files are needed.
54
+
55
+ ## Python functions
56
+
57
+ Methods belong to `XClient`. Every method lists its accepted parameters explicitly.
58
+
59
+ | Task | Get into memory | Stream records | Save a dataset |
60
+ | --- | --- | --- | --- |
61
+ | User profile | `get_user(handle)` | — | — |
62
+ | Post by ID or URL | `get_post(post_id_or_url)` | — | — |
63
+ | Multiple post IDs | `get_posts(post_ids)` | — | — |
64
+ | Authored main posts | `get_user_posts(handle, ...)` | `iter_user_posts(handle, ...)` | `save_user_posts(handle, output_dir, ...)` |
65
+ | Authored replies | `get_user_replies(handle, ...)` | `iter_user_replies(handle, ...)` | `save_user_replies(handle, output_dir, ...)` |
66
+ | Replies below a main post | `get_post_comments(post_id_or_url, ...)` | `iter_post_comments(post_id_or_url, ...)` | `save_post_comments(post_id_or_url, output_dir, ...)` |
67
+ | Keyword matches | `get_search_posts(keyword, ...)` | `iter_search_posts(keyword, ...)` | `save_search_posts(keyword, output_dir, ...)` |
68
+ | Main posts + replies | — | — | `save_user_activity(handle, output_dir, ...)` |
69
+ | Multiple accounts | — | — | `save_users_activity(handles, output_dir, ...)` |
70
+
71
+ - `get_*` returns a `Profile`, `Post`, or `list[Post]`; it does not write a dataset.
72
+ - `iter_*` returns an iterator. Requests happen as it is consumed.
73
+ - `save_*` writes the standard dataset and returns `CollectionResult` with `output_dir`, `post_count`, `pages_fetched`, `complete`, and `reason`. `post_count` counts **all collected observations**, including replies; use the output manifest for separate table counts.
74
+ - Parameters: `handle` for one account, `handles` for several, `keyword` for search, `output_dir` for saved datasets, and `since`/`until` for dates. Options are keyword-only.
75
+ - Dates are UTC: `since` inclusive, `until` exclusive. `max_pages` and `limit` must be positive integers or `None`. Saved timeline/mirror `limit` is a page-boundary stopping threshold, so a final page can exceed it; iterator limits are exact.
76
+
77
+ See [API reference](docs/api.md) for signatures, return types and compatibility names.
78
+
79
+ ### Search one account
80
+
81
+ ```python
82
+ with XClient(profile="Default") as client:
83
+ matches = client.get_search_posts(
84
+ "reset", handle="thsottiaux",
85
+ since="2025-09-29", until="2026-09-30", limit=20,
86
+ )
87
+ result = client.save_search_posts(
88
+ "reset", output_dir="output/tibo_reset",
89
+ handle="thsottiaux", since="2025-09-29", until="2026-09-30",
90
+ )
91
+ ```
92
+
93
+ Direct account search scans main posts and authored replies, then matches a literal word/phrase without case sensitivity. Matching posts do not represent deduplicated real-world events. Cross-user search requires an explicit `mirror_base`; it sends search terms to that mirror and retrieves discovered post details from X. No mirror is enabled by default.
94
+
95
+ ### Collect several accounts
96
+
97
+ ```python
98
+ with XClient(profile="Default") as client:
99
+ result = client.save_users_activity(
100
+ ["OpenAI", "AnthropicAI", "thsottiaux", "sama", "alexalbert__", "bcherny"],
101
+ output_dir="output/ai_year",
102
+ since="2025-09-29", until="2026-09-30",
103
+ pages_per_round=5, rounds=1,
104
+ )
105
+ ```
106
+
107
+ Repeat to resume. `rounds=0, wait_on_rate_limit=True` keeps running and waits for rate windows when needed. `progress=callback` receives status dictionaries.
108
+
109
+ ## Collect a main post and nested comments
110
+
111
+ ```python
112
+ with XClient(profile="Default") as client:
113
+ result = client.save_post_comments(
114
+ "1973931546550894681",
115
+ output_dir="output/tibo_conversation",
116
+ max_depth=2,
117
+ max_comments=100,
118
+ max_pages=20,
119
+ )
120
+ print(result.complete, result.reason)
121
+ ```
122
+
123
+ ```bash
124
+ pyxcom post-comments 1973931546550894681 --profile Default \
125
+ --max-depth 2 --max-comments 100 --max-pages 20 \
126
+ --output-dir output/tibo_conversation
127
+ ```
128
+
129
+ The root belongs in `posts.csv`, replies from **all visible authors** in `comments.csv`, and author profiles in `users.csv`. The root must be a main post. Level 1 replies directly to it; level 2 replies to those comments. Quoted content, unrelated recommendations and comments beyond the requested depth are excluded. Profile details absent from the response remain unavailable.
130
+
131
+ `max_comments` is an exact total comment cap, excluding the root. `max_pages` caps conversation requests per call. Repeat the same root/depth/output to resume saved branches and cursors; raise the comment cap to continue a capped dataset. Pending page records are retained, so stopping midway through a page does not skip them. Changing depth requires a separate dataset. Python accepts `None` for unlimited comment/page budgets; depth must be positive.
132
+
133
+ Stop reasons include `comment_limit`, `page_limit`, `rate_limited`, `partial_conversation`, and `visible_source_end`. `complete=true` means the visible queue was exhausted **within the requested depth**, not that every comment on X was recovered. Missing ancestors and stalled pagination are reported as partial. `iter_post_comments` / `get_post_comments` return reply records without writing a dataset; use `save_post_comments` when you need coverage status and resumability.
134
+
135
+ ## Output
136
+
137
+ ```text
138
+ output/tibo_year/
139
+ ├── users.csv
140
+ ├── posts.csv
141
+ ├── comments.csv
142
+ ├── manifest.json
143
+ └── .pyxcom/ # Checkpoints, observations, source streams and reports
144
+ ```
145
+
146
+ | Table | Unit | Main keys |
147
+ | --- | --- | --- |
148
+ | `users.csv` | One user | `user_id`, `username`, `display_name` |
149
+ | `posts.csv` | One original or quote main post | `post_id`, `author_id`, `post_type`, `quoted_post_id` |
150
+ | `comments.csv` | One reply at any observed depth | `comment_id`, `author_id`, `root_post_id`, `parent_post_id`, `depth` |
151
+
152
+ Replies to oneself remain comments. A quote within a reply retains its quote link in `comments.csv`. Direct replies are depth 1; replies to those are depth 2. Missing ancestry leaves `depth` empty with a `depth_status`; `parent_in_dataset` and `root_in_dataset` indicate observed relationships. Do not interpret missing parents as first-level replies.
153
+
154
+ `user_replies` means replies **authored by the selected account**. Use `*_post_comments` to collect replies below a particular main post from all visible authors, including nested replies. Neither endpoint establishes full conversation coverage.
155
+
156
+ CSV conventions: UTF-8 with BOM, snake_case columns, string IDs (import as text in Excel), JSON arrays for list fields, empty values for unavailable scalars, UTC timestamp fields. Users without a saved profile have `profile_available=false`. Engagement counts are snapshots; unknown values are not replaced with zero.
157
+
158
+ If source records include pure reposts, an additional `interactions.csv` preserves them. The current authored-account collector does not establish complete repost activity. `manifest.json` records schema/layout versions, table counts, collection scope, relationship coverage and hashes. `.pyxcom/` must be retained for resuming; the three CSV files can be shared independently for analysis.
159
+
160
+ ## Command line
161
+
162
+ CLI task names correspond to the Python methods:
163
+
164
+ ```bash
165
+ pyxcom user thsottiaux --profile Default
166
+ pyxcom user-posts thsottiaux --profile Default --output-dir output/tibo_posts
167
+ pyxcom user-replies thsottiaux --profile Default --output-dir output/tibo_replies
168
+ pyxcom user-activity thsottiaux --profile Default \
169
+ --since 2025-09-29 --until 2026-09-30 --output-dir output/tibo_year
170
+ pyxcom search-posts reset --handle thsottiaux --profile Default \
171
+ --since 2025-09-29 --until 2026-09-30 --output-dir output/tibo_reset
172
+ pyxcom users-activity --handles OpenAI AnthropicAI --profile Default \
173
+ --since 2025-09-29 --until 2026-09-30 --output-dir output/ai_year
174
+ ```
175
+
176
+ Add `--proxy URL` if your network requires one. Use `pyxcom COMMAND --help` for task options. Individual `user`, `post`, and `raw-post` commands accept `--output FILE` for a JSON file.
177
+
178
+ ## Export, migration and verification
179
+
180
+ These operations work offline, without browser credentials:
181
+
182
+ ```python
183
+ from pyxcom import export_tables, validate_collection, finalize_collection
184
+
185
+ export_tables("output/ai_year")
186
+ print(validate_collection("output/ai_year"))
187
+ # Rebuild an interrupted dataset from its saved observations:
188
+ finalize_collection("output/ai_year")
189
+ ```
190
+
191
+ ```bash
192
+ pyxcom export --output-dir output/ai_year
193
+ pyxcom validate --output-dir output/ai_year
194
+ pyxcom finalize --output-dir output/ai_year
195
+ pyxcom schema --output-dir output/ai_year
196
+ ```
197
+
198
+ Opening/exporting a legacy dataset migrates its recognized internal files into `.pyxcom/`, with originals backed up under `.pyxcom/legacy/`. It publishes the three tables at the root; the previous mixed `posts.csv` is retained internally. Unrelated user files are left in place. Conflicting migration destinations fail rather than overwrite. `schema --apply` also backfills classification fields in older records; field reports live under `.pyxcom/`.
199
+
200
+ ## Compatibility and limitations
201
+
202
+ Old Python names `get_search`, `iter_search`, `save_search` (with `user=`), and `save_accounts` remain supported. Prefer `*_search_posts` (with `handle=`) and `save_users_activity` for new code. `timeline="replies"` still works, but the explicit `*_user_replies` methods are clearer. Timeline outputs exclude same-author context of the other role. Resuming an older timeline applies the same rule and preserves its original observations in an internal compressed backup. Combined activity keeps both roles.
203
+
204
+ Old CLI names `profile`, `posts`, `activity`, `search`, `batch`, `--user` and dataset `--output` remain aliases. Existing code reading root `posts.csv` as a mixed table must adapt: it now contains main posts only; replies are in `comments.csv`.
205
+
206
+ X endpoints may change or impose limits. pyXcom discovers current GraphQL query IDs from X's web bundle, while response parsers still need maintenance. A completed run indicates the requested date boundary or visible source end was reached, not proof of all historical content. Deleted, protected or unavailable posts cannot be recovered. Output migration alone does not fetch comments; call `save_post_comments` to acquire them from X.
207
+
208
+
209
+ ## License
210
+
211
+ pyXcom is released under the [MIT License](LICENSE). Copyright © 2026 Haocheng Wang.
212
+
213
+ ## Citation
214
+
215
+ **Author:** Haocheng Wang, Communication University of China.
216
+
217
+ If you use pyXcom in a paper, thesis, dataset, or other research output, please cite the software and report the version used. GitHub's **Cite this repository** menu reads the machine-readable [CITATION.cff](CITATION.cff).
218
+
219
+ **Suggested reference**
220
+
221
+ Wang, H. (2026). *pyXcom: Structured and auditable X data collection for communication research* (Version 0.6.0) [Computer software]. https://github.com/haochengw372-hash/pyXcom
222
+
223
+ **BibTeX**
224
+
225
+ ```bibtex
226
+ @software{wang2026pyxcom,
227
+ author = {Wang, Haocheng},
228
+ title = {{pyXcom}: Structured and Auditable X Data Collection for Communication Research},
229
+ year = {2026},
230
+ version = {0.6.0},
231
+ url = {https://github.com/haochengw372-hash/pyXcom}
232
+ }
233
+ ```
234
+
235
+ For reproducible reporting, also describe the collection dates, account or keyword scope, comment-depth limits, package version, and coverage/stop reasons recorded in the output manifest. No DOI or published-paper citation is currently assigned; this reference cites the software itself. Citation is appreciated and does not add a condition to the MIT license.
@@ -0,0 +1,108 @@
1
+ # Public API — pyXcom 0.6
2
+
3
+ Import `XClient`, `Profile`, `Post`, `CollectionResult`, `PyXcomError`, `AuthenticationError`, `APIError`, and `RateLimitError` from `pyxcom`.
4
+
5
+ ## Client
6
+
7
+ ```python
8
+ XClient(*, browser="chrome", profile=None, cookie_db=None, proxy=None,
9
+ mirror_base=None, delay=1.0, timeout=30)
10
+ ```
11
+
12
+ Use as a context manager to close connections. `profile` selects the local browser profile; it is not an X username. Manually sign in first. The client never launches a browser.
13
+
14
+ ## Lookup methods
15
+
16
+ ```python
17
+ get_user(handle: str) -> Profile
18
+ get_post(post_id_or_url: str) -> Post
19
+ get_posts(post_ids: list[str]) -> list[Post]
20
+ get_raw_post(post_id_or_url: str) -> dict
21
+ get_raw_user_timeline(handle: str, *, timeline="posts", cursor=None) -> dict
22
+ ```
23
+
24
+ `get_posts` can return fewer items when X does not return every requested ID. Raw methods return the platform response for advanced use and have no stable field schema.
25
+
26
+ ## Account content
27
+
28
+ ```python
29
+ iter_user_posts(handle, *, timeline="posts", since=None, until=None,
30
+ max_pages=None, limit=None) -> Iterator[Post]
31
+ get_user_posts(handle, *, timeline="posts", since=None, until=None,
32
+ max_pages=None, limit=None) -> list[Post]
33
+ save_user_posts(handle, output_dir, *, timeline="posts", since=None, until=None,
34
+ max_pages=None, limit=None) -> CollectionResult
35
+
36
+ iter_user_replies(handle, *, since=None, until=None,
37
+ max_pages=None, limit=None) -> Iterator[Post]
38
+ get_user_replies(handle, *, since=None, until=None,
39
+ max_pages=None, limit=None) -> list[Post]
40
+ save_user_replies(handle, output_dir, *, since=None, until=None,
41
+ max_pages=None, limit=None) -> CollectionResult
42
+
43
+ save_user_activity(handle, output_dir, *, since=None, until=None,
44
+ max_pages=None) -> CollectionResult
45
+ save_users_activity(handles, output_dir, *, since, until, pages_per_round=5,
46
+ rounds=1, wait_on_rate_limit=False, progress=None) -> CollectionResult
47
+ ```
48
+
49
+ `handle: str`; `handles: list[str]`; `output_dir: str | Path`. Dates are `str | None` in YYYY-MM-DD UTC. `max_pages` and `limit` are positive `int | None`. `progress` is an optional callable taking a dictionary. `get`/`iter` start a fresh traversal; `save` resumes the same dataset query. `limit` on a saved timeline/mirror search is checked after a page; it may overshoot. Account activity and multi-account saves intentionally have no total-record `limit`.
50
+
51
+ `user_posts` defaults to original/quote main posts. `user_replies` returns authored replies, including self-replies. Neither is an API to retrieve a main post's entire received comment tree. `timeline` is a compatibility option for selecting the source stream.
52
+
53
+ ## Comments received by a main post
54
+
55
+ ```python
56
+ iter_post_comments(post_id_or_url: str, *, max_depth: int = 2,
57
+ max_comments: int | None = 100,
58
+ max_pages: int | None = 20) -> Iterator[Post]
59
+ get_post_comments(post_id_or_url: str, *, max_depth: int = 2,
60
+ max_comments: int | None = 100,
61
+ max_pages: int | None = 20) -> list[Post]
62
+ save_post_comments(post_id_or_url: str, output_dir: str | Path, *,
63
+ max_depth: int = 2, max_comments: int | None = 100,
64
+ max_pages: int | None = 20) -> CollectionResult
65
+ ```
66
+
67
+ These methods query the main post's conversation and expand replies from all visible authors. They differ from `*_user_replies`, which fetch only one account's authored replies. `get` and `iter` return comments only; `save` includes the root in `posts.csv`, profiles in `users.csv`, comments in `comments.csv`, and private traversal state for resume.
68
+
69
+ Depth is relative to the main post (direct replies=1). Comment cap is exact and cumulative per dataset; page cap applies to each call. A rate-limited save preserves its queue with `complete=false`; memory/iterator methods raise `RateLimitError`. Network/parse failures save a partial checkpoint and raise. Repeating a save with larger comment/page budgets resumes it; changing root/depth is rejected to protect query consistency. Completion only means visible traversal within the requested depth ended, not an exhaustive historical thread.
70
+
71
+ ## Keyword search
72
+
73
+ ```python
74
+ iter_search_posts(keyword, *, handle=None, since=None, until=None,
75
+ max_pages=None, limit=None) -> Iterator[Post]
76
+ get_search_posts(keyword, *, handle=None, since=None, until=None,
77
+ max_pages=None, limit=None) -> list[Post]
78
+ save_search_posts(keyword, output_dir, *, handle=None, since=None, until=None,
79
+ max_pages=None, limit=None) -> CollectionResult
80
+ ```
81
+
82
+ `keyword: str`, `handle: str | None`. Direct search requires a handle and matches literal text without case sensitivity across that account's main posts and replies. Cross-account discovery requires constructor `mirror_base`. `max_pages` applies per stream for direct search.
83
+
84
+ ## Offline dataset functions
85
+
86
+ ```python
87
+ export_tables(output_dir: str | Path) -> dict
88
+ validate_tables(output_dir: str | Path) -> dict
89
+ validate_collection(output_dir: str | Path) -> dict
90
+ finalize_collection(output_dir: str | Path) -> CollectionResult
91
+ schema_summary(output_dir: str | Path) -> dict
92
+ write_schema_report(output_dir: str | Path) -> dict
93
+ apply_role_schema(output_dir: str | Path) -> dict
94
+ ```
95
+
96
+ Export/finalization migrate legacy files with backups and regenerate public tables. Validation never fetches X data. `validate_collection` checks internal observations and public tables; `validate_tables` checks normalized relationships, source/file hashes and counts. Reports describe missing ancestry explicitly. `schema_summary` describes the internal standard Post/Profile fields, not only the normalized table columns.
97
+
98
+ ## Backward-compatible names
99
+
100
+ | Previous API | Preferred API |
101
+ | --- | --- |
102
+ | `iter_search(..., user=...)` | `iter_search_posts(..., handle=...)` |
103
+ | `get_search(..., user=...)` | `get_search_posts(..., handle=...)` |
104
+ | `save_search(..., user=...)` | `save_search_posts(..., handle=...)` |
105
+ | `save_accounts(...)` | `save_users_activity(...)` |
106
+ | `*_user_posts(..., timeline="replies")` | `*_user_replies(...)` |
107
+
108
+ No removal is scheduled. These names call the same implementation; no separate authentication flow or duplicate scraper is introduced.