pyXcom 0.6.0__tar.gz → 0.7.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.
Files changed (49) hide show
  1. {pyxcom-0.6.0 → pyxcom-0.7.0}/CITATION.cff +3 -3
  2. {pyxcom-0.6.0 → pyxcom-0.7.0}/PKG-INFO +74 -14
  3. {pyxcom-0.6.0 → pyxcom-0.7.0}/README.md +72 -12
  4. pyxcom-0.7.0/docs/api.md +172 -0
  5. {pyxcom-0.6.0 → pyxcom-0.7.0}/docs/publishing.md +13 -13
  6. pyxcom-0.7.0/examples/research_networks.py +78 -0
  7. {pyxcom-0.6.0 → pyxcom-0.7.0}/pyproject.toml +3 -3
  8. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/__init__.py +5 -2
  9. pyxcom-0.7.0/src/pyxcom/assets.py +114 -0
  10. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/cli.py +102 -4
  11. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/client.py +54 -2
  12. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/comments.py +81 -7
  13. pyxcom-0.7.0/src/pyxcom/discovery.py +498 -0
  14. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/layout.py +6 -0
  15. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/models.py +38 -0
  16. pyxcom-0.7.0/src/pyxcom/networks.py +905 -0
  17. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/parse.py +36 -4
  18. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/schema.py +14 -0
  19. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/storage.py +134 -2
  20. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/tables.py +147 -2
  21. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/transport.py +124 -12
  22. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/validate.py +30 -0
  23. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/validate_tables.py +28 -5
  24. pyxcom-0.7.0/tests/test_assets.py +115 -0
  25. pyxcom-0.7.0/tests/test_batch_report.py +42 -0
  26. pyxcom-0.7.0/tests/test_capability_cli.py +256 -0
  27. pyxcom-0.7.0/tests/test_capability_transport.py +44 -0
  28. pyxcom-0.7.0/tests/test_comment_transport.py +37 -0
  29. pyxcom-0.7.0/tests/test_comments.py +262 -0
  30. pyxcom-0.7.0/tests/test_direct_search.py +61 -0
  31. pyxcom-0.7.0/tests/test_discovery.py +365 -0
  32. pyxcom-0.7.0/tests/test_layout.py +170 -0
  33. pyxcom-0.7.0/tests/test_networks.py +537 -0
  34. pyxcom-0.7.0/tests/test_observations.py +203 -0
  35. pyxcom-0.7.0/tests/test_parsing.py +200 -0
  36. pyxcom-0.7.0/tests/test_public_api.py +176 -0
  37. pyxcom-0.7.0/tests/test_relational_integration.py +58 -0
  38. pyxcom-0.7.0/tests/test_schema.py +66 -0
  39. pyxcom-0.7.0/tests/test_search_storage.py +105 -0
  40. pyxcom-0.7.0/tests/test_tables.py +187 -0
  41. pyxcom-0.7.0/tests/test_transport.py +125 -0
  42. pyxcom-0.6.0/docs/api.md +0 -108
  43. {pyxcom-0.6.0 → pyxcom-0.7.0}/.gitignore +0 -0
  44. {pyxcom-0.6.0 → pyxcom-0.7.0}/LICENSE +0 -0
  45. {pyxcom-0.6.0 → pyxcom-0.7.0}/examples/basic.py +0 -0
  46. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/auth.py +0 -0
  47. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/batch.py +0 -0
  48. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/errors.py +0 -0
  49. {pyxcom-0.6.0 → pyxcom-0.7.0}/src/pyxcom/search.py +0 -0
@@ -6,12 +6,12 @@ authors:
6
6
  - family-names: Wang
7
7
  given-names: Haocheng
8
8
  affiliation: Communication University of China
9
- version: 0.6.0
10
- date-released: 2026-09-30
9
+ version: 0.7.0
10
+ date-released: 2026-10-01
11
11
  license: MIT
12
12
  repository-code: "https://github.com/haochengw372-hash/pyXcom"
13
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."
14
+ abstract: "A Python package for collecting public X posts, native search results, reply branches, quotations, repost activity and observed follower/following networks, with linked tables, raw-response provenance and resumable snapshots."
15
15
  keywords:
16
16
  - computational communication
17
17
  - social media
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyXcom
3
- Version: 0.6.0
4
- Summary: Collect public X profiles, posts, replies, and search results with your existing browser session
3
+ Version: 0.7.0
4
+ Summary: Collect public X posts, search results, conversations, reposts, and observed follow networks with your browser session
5
5
  Project-URL: Homepage, https://github.com/haochengw372-hash/pyXcom
6
6
  Project-URL: Repository, https://github.com/haochengw372-hash/pyXcom
7
7
  Author: Haocheng Wang
@@ -20,7 +20,9 @@ Description-Content-Type: text/markdown
20
20
 
21
21
  **使用前请自行打开 Chrome 并登录 X。pyXcom 不会打开或控制浏览器,也不会替你登录。**
22
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`.
23
+ Collect public X user profiles, posts, replies, search results and observed network relationships using your existing Chrome or Edge session. Post datasets contain three linked CSV tables: `users.csv`, `posts.csv` and `comments.csv`; network datasets have separate relationship and snapshot tables.
24
+
25
+ Version 0.7.0 adds native search, quote discovery, repost activity and follower/following snapshots. These capabilities have passed offline tests and bounded authenticated live endpoint tests. Collection remains limited to results returned by X; the tests do not establish complete historical or network coverage.
24
26
 
25
27
  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
28
 
@@ -81,18 +83,23 @@ Methods belong to `XClient`. Every method lists its accepted parameters explicit
81
83
  | Multiple post IDs | `get_posts(post_ids)` | — | — |
82
84
  | Authored main posts | `get_user_posts(handle, ...)` | `iter_user_posts(handle, ...)` | `save_user_posts(handle, output_dir, ...)` |
83
85
  | 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, ...)` |
86
+ | Replies below a post or reply | `get_post_comments(post_id_or_url, ...)` | `iter_post_comments(post_id_or_url, ...)` | `save_post_comments(post_id_or_url, output_dir, ...)` |
85
87
  | Keyword matches | `get_search_posts(keyword, ...)` | `iter_search_posts(keyword, ...)` | `save_search_posts(keyword, output_dir, ...)` |
88
+ | Native cross-user query | `get_search_query(query, ...)` | `iter_search_query(query, ...)` | `save_search_query(query, output_dir, ...)` |
89
+ | Quotes of a post | `get_post_quotes(post_id_or_url, ...)` | `iter_post_quotes(post_id_or_url, ...)` | `save_post_quotes(post_id_or_url, output_dir, ...)` |
90
+ | Account repost activity | `get_user_reposts(handle, ...)` | `iter_user_reposts(handle, ...)` | `save_user_reposts(handle, output_dir, ...)` |
91
+ | Followers / following | `get_followers(user_id, ...)` / `get_following(user_id, ...)` | `iter_followers(user_id, ...)` / `iter_following(user_id, ...)` | `save_followers(user_id, output_dir, ...)` / `save_following(user_id, output_dir, ...)` |
92
+ | Reposter user list | `get_post_reposters(post_id_or_url, ...)` | `iter_post_reposters(post_id_or_url, ...)` | `save_post_reposters(post_id_or_url, output_dir, ...)` |
86
93
  | Main posts + replies | — | — | `save_user_activity(handle, output_dir, ...)` |
87
94
  | Multiple accounts | — | — | `save_users_activity(handles, output_dir, ...)` |
88
95
 
89
- - `get_*` returns a `Profile`, `Post`, or `list[Post]`; it does not write a dataset.
96
+ - `get_*` returns a `Profile`, `Post`, `list[Post]`, or network `list[Profile]`; it does not write a dataset.
90
97
  - `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.
98
+ - `save_*` returns `CollectionResult` with `output_dir`, `post_count`, `pages_fetched`, `complete`, and `reason`. For post datasets `post_count` counts unique records, including replies; for network datasets this compatibility field counts observed users. Consult each manifest for table counts and coverage.
92
99
  - 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
100
  - 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
101
 
95
- See [API reference](docs/api.md) for signatures, return types and compatibility names.
102
+ See [API reference](https://github.com/haochengw372-hash/pyXcom/blob/main/docs/api.md) for signatures, return types and compatibility names.
96
103
 
97
104
  ### Search one account
98
105
 
@@ -110,6 +117,40 @@ with XClient(profile="Default") as client:
110
117
 
111
118
  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
119
 
120
+ ### Native search, quotes and repost activity
121
+
122
+ `*_search_query` submits X search syntax to the native Latest search endpoint; it does not require or use a mirror. `*_search_posts` retains its account/literal behavior above. Use a separate output directory for each query or source:
123
+
124
+ ```python
125
+ with XClient(profile="Default") as client:
126
+ result = client.save_search_query(
127
+ '("usage reset" OR "rate limit reset") (OpenAI OR Codex)',
128
+ "output/reset_search", since="2025-09-29", until="2026-09-30",
129
+ max_pages=2, limit=100,
130
+ )
131
+ print(result.complete, result.reason)
132
+ client.save_post_quotes("1973931546550894681", "output/reset_quotes", max_pages=2)
133
+ client.save_user_reposts("thsottiaux", "output/tibo_reposts", max_pages=2)
134
+ ```
135
+
136
+ Quote discovery uses native search followed by a check of `quoted_post_id`; zero matches do not establish zero quotes. Repost activity is collected from visible account timeline wrappers, preserving the repost ID and its action timestamp separately from the original post ID/time. A flattened timeline that only returns originals cannot establish action times and reports `repost_activity_unavailable` with an incomplete result. It is not a complete historical repost archive. Search sorting, visibility and pagination remain controlled by X.
137
+
138
+ ### Follow networks and reposter lists
139
+
140
+ Resolve a handle with `get_user(handle).id`, then pass that stable numeric ID to follower/following methods:
141
+
142
+ ```python
143
+ with XClient(profile="Default") as client:
144
+ user_id = client.get_user("thsottiaux").id
145
+ client.save_followers(user_id, "output/tibo_network", max_pages=2, snapshot_id="wave-1")
146
+ client.save_following(user_id, "output/tibo_network", max_pages=2, snapshot_id="wave-1")
147
+ client.save_post_reposters("1973931546550894681", "output/reset_reposters", max_pages=2)
148
+ ```
149
+
150
+ Follow edges point from follower to followed account. `follow_edges.csv`, `reposters.csv`, `user_snapshots.csv` and `network_manifest.json` preserve observed relationships, user profiles and coverage per snapshot. A reposter list identifies who visibly reposted a post, but its `action_time_utc` is unknown. Use account repost activity when the wrapper supplies an action time; never substitute the original post time.
151
+
152
+ Network saves resume a recoverable unfinished snapshot. Once completed or stopped by a terminal bad cursor, calling again without `snapshot_id` creates a new observation; explicitly reusing a completed ID returns that saved snapshot. Memory/iterator methods raise endpoint, parse or rate-limit errors and retain coverage in `last_network_collection`; hitting a budget returns the observed prefix. Saves return an incomplete result and preserve checkpoints on source failures. Network and post collections require separate output directories to protect their different `users.csv` exports. Follow observations cannot establish when a relationship began or reconstruct a historical follow network. Keep collection dates in the analysis. See [examples/research_networks.py](examples/research_networks.py) for a small collection recipe; importing it does not collect data.
153
+
113
154
  ### Collect several accounts
114
155
 
115
156
  ```python
@@ -124,7 +165,7 @@ with XClient(profile="Default") as client:
124
165
 
125
166
  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
167
 
127
- ## Collect a main post and nested comments
168
+ ## Collect a post or reply and nested comments
128
169
 
129
170
  ```python
130
171
  with XClient(profile="Default") as client:
@@ -144,7 +185,7 @@ pyxcom post-comments 1973931546550894681 --profile Default \
144
185
  --output-dir output/tibo_conversation
145
186
  ```
146
187
 
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.
188
+ The seed can be a main post or a reply. Level 1 replies directly to that seed; level 2 replies to those comments. Main seeds belong in `posts.csv`; reply seeds remain replies in `comments.csv`. `seed_post_id` and `seed_relative_depth` describe this branch; `root_post_id`, `parent_post_id` and `depth` retain actual conversation relationships and do not relabel a reply as a platform root. Ancestors returned as context are stored in `context_posts.csv`, not counted as collected descendants. `since`/`until` filter descendant timestamps; traversal retains the ancestry needed to reach in-window descendants. Quoted content, unrelated recommendations and comments beyond the requested depth are excluded. Profile details absent from the response remain unavailable.
148
189
 
149
190
  `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
191
 
@@ -169,12 +210,16 @@ output/tibo_year/
169
210
 
170
211
  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
212
 
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.
213
+ `user_replies` means replies **authored by the selected account**. Use `*_post_comments` to collect replies below a particular post or reply from all visible authors, including nested replies. Neither endpoint establishes full conversation coverage.
173
214
 
174
215
  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
216
 
176
217
  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
218
 
219
+ Saved collection pages also retain platform response JSON under `.pyxcom/raw/` (network pages under their snapshot state) and query provenance in `.pyxcom/collection_log.jsonl`. Repeated post observations keep returned engagement metrics in `metric_snapshots.csv`, with source observations in `.pyxcom/observations.jsonl` and metric history in `.pyxcom/metric_snapshots.jsonl`. Unreturned counts remain unavailable. These are observations at retrieval time, not historical metrics at publication. Credentials are not included in the response archive.
220
+
221
+ When source records contain reply, quote or repost relationships, `post_edges.csv` exports their directions, source/target post and user IDs, action and observation times, and target availability/resolution flags. An unavailable target is retained; its user ID stays empty if unresolved. Known target authors can appear in `users.csv` as stubs with `profile_available=false`. Context-only records do not create source edges. Relationship, context and metric exports appear when observations provide the corresponding data.
222
+
178
223
  ## Command line
179
224
 
180
225
  CLI task names correspond to the Python methods:
@@ -189,10 +234,19 @@ pyxcom search-posts reset --handle thsottiaux --profile Default \
189
234
  --since 2025-09-29 --until 2026-09-30 --output-dir output/tibo_reset
190
235
  pyxcom users-activity --handles OpenAI AnthropicAI --profile Default \
191
236
  --since 2025-09-29 --until 2026-09-30 --output-dir output/ai_year
237
+ pyxcom search-query '("usage reset" OR "rate limit reset") Codex' --profile Default \
238
+ --since 2025-09-29 --until 2026-09-30 --max-pages 2 --output-dir output/reset_search
239
+ pyxcom post-quotes 1973931546550894681 --max-pages 2 --output-dir output/reset_quotes
240
+ pyxcom user-reposts thsottiaux --max-pages 2 --output-dir output/tibo_reposts
241
+ pyxcom user-followers USER_ID --max-pages 2 --snapshot-id wave-1 --output-dir output/network
242
+ pyxcom user-following USER_ID --max-pages 2 --snapshot-id wave-1 --output-dir output/network
243
+ pyxcom post-reposters 1973931546550894681 --max-pages 2 --output-dir output/reposters
192
244
  ```
193
245
 
194
246
  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
247
 
248
+ `post-comments --max-comments all --max-pages all` removes comment/page caps; `--max-depth` still requires a positive integer (default 2). New discovery/network commands also accept `all` for `--max-pages` and `--limit`. Unlimited budgets do not remove visibility or rate limits.
249
+
196
250
  ## Export, migration and verification
197
251
 
198
252
  These operations work offline, without browser credentials:
@@ -226,17 +280,17 @@ X endpoints may change or impose limits. pyXcom discovers current GraphQL query
226
280
 
227
281
  ## License
228
282
 
229
- pyXcom is released under the [MIT License](LICENSE). Copyright © 2026 Haocheng Wang.
283
+ pyXcom is released under the [MIT License](https://github.com/haochengw372-hash/pyXcom/blob/main/LICENSE). Copyright © 2026 Haocheng Wang.
230
284
 
231
285
  ## Citation
232
286
 
233
287
  **Author:** Haocheng Wang, Communication University of China.
234
288
 
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).
289
+ 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](https://github.com/haochengw372-hash/pyXcom/blob/main/CITATION.cff).
236
290
 
237
291
  **Suggested reference**
238
292
 
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
293
+ Wang, H. (2026). *pyXcom: Structured and auditable X data collection for communication research* (Version 0.7.0) [Computer software]. https://github.com/haochengw372-hash/pyXcom
240
294
 
241
295
  **BibTeX**
242
296
 
@@ -245,9 +299,15 @@ Wang, H. (2026). *pyXcom: Structured and auditable X data collection for communi
245
299
  author = {Wang, Haocheng},
246
300
  title = {{pyXcom}: Structured and Auditable X Data Collection for Communication Research},
247
301
  year = {2026},
248
- version = {0.6.0},
302
+ version = {0.7.0},
249
303
  url = {https://github.com/haochengw372-hash/pyXcom}
250
304
  }
251
305
  ```
252
306
 
253
307
  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.
308
+
309
+ For longitudinal profile comparisons use `user_snapshots.csv`; `users.csv` consolidates known profile fields. Canonical post tables keep the first saved observation for each ID; later returned text and metrics remain in observation archives and metric snapshots.
310
+
311
+ Offline development checks: `PYTHONPATH=src python -m pytest -q` and `ruff check src tests examples`. The source distribution includes tests; testing tools are development-only, not runtime dependencies.
312
+
313
+ Live validation: current X SearchTimeline/Followers use POST read queries; Retweeters is discovered from public lazy-loaded TweetActivity assets. If a search source returns posts outside explicit dates, they are excluded locally and `manifest.json.date_scope` records the mismatch. An empty bounded result with this warning does not establish historical absence. Native reposts generate only repost edges; embedded quote/reply context is not attributed as another action by the reposter.
@@ -2,7 +2,9 @@
2
2
 
3
3
  **使用前请自行打开 Chrome 并登录 X。pyXcom 不会打开或控制浏览器,也不会替你登录。**
4
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`.
5
+ Collect public X user profiles, posts, replies, search results and observed network relationships using your existing Chrome or Edge session. Post datasets contain three linked CSV tables: `users.csv`, `posts.csv` and `comments.csv`; network datasets have separate relationship and snapshot tables.
6
+
7
+ Version 0.7.0 adds native search, quote discovery, repost activity and follower/following snapshots. These capabilities have passed offline tests and bounded authenticated live endpoint tests. Collection remains limited to results returned by X; the tests do not establish complete historical or network coverage.
6
8
 
7
9
  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
10
 
@@ -63,18 +65,23 @@ Methods belong to `XClient`. Every method lists its accepted parameters explicit
63
65
  | Multiple post IDs | `get_posts(post_ids)` | — | — |
64
66
  | Authored main posts | `get_user_posts(handle, ...)` | `iter_user_posts(handle, ...)` | `save_user_posts(handle, output_dir, ...)` |
65
67
  | 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, ...)` |
68
+ | Replies below a post or reply | `get_post_comments(post_id_or_url, ...)` | `iter_post_comments(post_id_or_url, ...)` | `save_post_comments(post_id_or_url, output_dir, ...)` |
67
69
  | Keyword matches | `get_search_posts(keyword, ...)` | `iter_search_posts(keyword, ...)` | `save_search_posts(keyword, output_dir, ...)` |
70
+ | Native cross-user query | `get_search_query(query, ...)` | `iter_search_query(query, ...)` | `save_search_query(query, output_dir, ...)` |
71
+ | Quotes of a post | `get_post_quotes(post_id_or_url, ...)` | `iter_post_quotes(post_id_or_url, ...)` | `save_post_quotes(post_id_or_url, output_dir, ...)` |
72
+ | Account repost activity | `get_user_reposts(handle, ...)` | `iter_user_reposts(handle, ...)` | `save_user_reposts(handle, output_dir, ...)` |
73
+ | Followers / following | `get_followers(user_id, ...)` / `get_following(user_id, ...)` | `iter_followers(user_id, ...)` / `iter_following(user_id, ...)` | `save_followers(user_id, output_dir, ...)` / `save_following(user_id, output_dir, ...)` |
74
+ | Reposter user list | `get_post_reposters(post_id_or_url, ...)` | `iter_post_reposters(post_id_or_url, ...)` | `save_post_reposters(post_id_or_url, output_dir, ...)` |
68
75
  | Main posts + replies | — | — | `save_user_activity(handle, output_dir, ...)` |
69
76
  | Multiple accounts | — | — | `save_users_activity(handles, output_dir, ...)` |
70
77
 
71
- - `get_*` returns a `Profile`, `Post`, or `list[Post]`; it does not write a dataset.
78
+ - `get_*` returns a `Profile`, `Post`, `list[Post]`, or network `list[Profile]`; it does not write a dataset.
72
79
  - `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.
80
+ - `save_*` returns `CollectionResult` with `output_dir`, `post_count`, `pages_fetched`, `complete`, and `reason`. For post datasets `post_count` counts unique records, including replies; for network datasets this compatibility field counts observed users. Consult each manifest for table counts and coverage.
74
81
  - 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
82
  - 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
83
 
77
- See [API reference](docs/api.md) for signatures, return types and compatibility names.
84
+ See [API reference](https://github.com/haochengw372-hash/pyXcom/blob/main/docs/api.md) for signatures, return types and compatibility names.
78
85
 
79
86
  ### Search one account
80
87
 
@@ -92,6 +99,40 @@ with XClient(profile="Default") as client:
92
99
 
93
100
  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
101
 
102
+ ### Native search, quotes and repost activity
103
+
104
+ `*_search_query` submits X search syntax to the native Latest search endpoint; it does not require or use a mirror. `*_search_posts` retains its account/literal behavior above. Use a separate output directory for each query or source:
105
+
106
+ ```python
107
+ with XClient(profile="Default") as client:
108
+ result = client.save_search_query(
109
+ '("usage reset" OR "rate limit reset") (OpenAI OR Codex)',
110
+ "output/reset_search", since="2025-09-29", until="2026-09-30",
111
+ max_pages=2, limit=100,
112
+ )
113
+ print(result.complete, result.reason)
114
+ client.save_post_quotes("1973931546550894681", "output/reset_quotes", max_pages=2)
115
+ client.save_user_reposts("thsottiaux", "output/tibo_reposts", max_pages=2)
116
+ ```
117
+
118
+ Quote discovery uses native search followed by a check of `quoted_post_id`; zero matches do not establish zero quotes. Repost activity is collected from visible account timeline wrappers, preserving the repost ID and its action timestamp separately from the original post ID/time. A flattened timeline that only returns originals cannot establish action times and reports `repost_activity_unavailable` with an incomplete result. It is not a complete historical repost archive. Search sorting, visibility and pagination remain controlled by X.
119
+
120
+ ### Follow networks and reposter lists
121
+
122
+ Resolve a handle with `get_user(handle).id`, then pass that stable numeric ID to follower/following methods:
123
+
124
+ ```python
125
+ with XClient(profile="Default") as client:
126
+ user_id = client.get_user("thsottiaux").id
127
+ client.save_followers(user_id, "output/tibo_network", max_pages=2, snapshot_id="wave-1")
128
+ client.save_following(user_id, "output/tibo_network", max_pages=2, snapshot_id="wave-1")
129
+ client.save_post_reposters("1973931546550894681", "output/reset_reposters", max_pages=2)
130
+ ```
131
+
132
+ Follow edges point from follower to followed account. `follow_edges.csv`, `reposters.csv`, `user_snapshots.csv` and `network_manifest.json` preserve observed relationships, user profiles and coverage per snapshot. A reposter list identifies who visibly reposted a post, but its `action_time_utc` is unknown. Use account repost activity when the wrapper supplies an action time; never substitute the original post time.
133
+
134
+ Network saves resume a recoverable unfinished snapshot. Once completed or stopped by a terminal bad cursor, calling again without `snapshot_id` creates a new observation; explicitly reusing a completed ID returns that saved snapshot. Memory/iterator methods raise endpoint, parse or rate-limit errors and retain coverage in `last_network_collection`; hitting a budget returns the observed prefix. Saves return an incomplete result and preserve checkpoints on source failures. Network and post collections require separate output directories to protect their different `users.csv` exports. Follow observations cannot establish when a relationship began or reconstruct a historical follow network. Keep collection dates in the analysis. See [examples/research_networks.py](examples/research_networks.py) for a small collection recipe; importing it does not collect data.
135
+
95
136
  ### Collect several accounts
96
137
 
97
138
  ```python
@@ -106,7 +147,7 @@ with XClient(profile="Default") as client:
106
147
 
107
148
  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
149
 
109
- ## Collect a main post and nested comments
150
+ ## Collect a post or reply and nested comments
110
151
 
111
152
  ```python
112
153
  with XClient(profile="Default") as client:
@@ -126,7 +167,7 @@ pyxcom post-comments 1973931546550894681 --profile Default \
126
167
  --output-dir output/tibo_conversation
127
168
  ```
128
169
 
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.
170
+ The seed can be a main post or a reply. Level 1 replies directly to that seed; level 2 replies to those comments. Main seeds belong in `posts.csv`; reply seeds remain replies in `comments.csv`. `seed_post_id` and `seed_relative_depth` describe this branch; `root_post_id`, `parent_post_id` and `depth` retain actual conversation relationships and do not relabel a reply as a platform root. Ancestors returned as context are stored in `context_posts.csv`, not counted as collected descendants. `since`/`until` filter descendant timestamps; traversal retains the ancestry needed to reach in-window descendants. Quoted content, unrelated recommendations and comments beyond the requested depth are excluded. Profile details absent from the response remain unavailable.
130
171
 
131
172
  `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
173
 
@@ -151,12 +192,16 @@ output/tibo_year/
151
192
 
152
193
  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
194
 
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.
195
+ `user_replies` means replies **authored by the selected account**. Use `*_post_comments` to collect replies below a particular post or reply from all visible authors, including nested replies. Neither endpoint establishes full conversation coverage.
155
196
 
156
197
  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
198
 
158
199
  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
200
 
201
+ Saved collection pages also retain platform response JSON under `.pyxcom/raw/` (network pages under their snapshot state) and query provenance in `.pyxcom/collection_log.jsonl`. Repeated post observations keep returned engagement metrics in `metric_snapshots.csv`, with source observations in `.pyxcom/observations.jsonl` and metric history in `.pyxcom/metric_snapshots.jsonl`. Unreturned counts remain unavailable. These are observations at retrieval time, not historical metrics at publication. Credentials are not included in the response archive.
202
+
203
+ When source records contain reply, quote or repost relationships, `post_edges.csv` exports their directions, source/target post and user IDs, action and observation times, and target availability/resolution flags. An unavailable target is retained; its user ID stays empty if unresolved. Known target authors can appear in `users.csv` as stubs with `profile_available=false`. Context-only records do not create source edges. Relationship, context and metric exports appear when observations provide the corresponding data.
204
+
160
205
  ## Command line
161
206
 
162
207
  CLI task names correspond to the Python methods:
@@ -171,10 +216,19 @@ pyxcom search-posts reset --handle thsottiaux --profile Default \
171
216
  --since 2025-09-29 --until 2026-09-30 --output-dir output/tibo_reset
172
217
  pyxcom users-activity --handles OpenAI AnthropicAI --profile Default \
173
218
  --since 2025-09-29 --until 2026-09-30 --output-dir output/ai_year
219
+ pyxcom search-query '("usage reset" OR "rate limit reset") Codex' --profile Default \
220
+ --since 2025-09-29 --until 2026-09-30 --max-pages 2 --output-dir output/reset_search
221
+ pyxcom post-quotes 1973931546550894681 --max-pages 2 --output-dir output/reset_quotes
222
+ pyxcom user-reposts thsottiaux --max-pages 2 --output-dir output/tibo_reposts
223
+ pyxcom user-followers USER_ID --max-pages 2 --snapshot-id wave-1 --output-dir output/network
224
+ pyxcom user-following USER_ID --max-pages 2 --snapshot-id wave-1 --output-dir output/network
225
+ pyxcom post-reposters 1973931546550894681 --max-pages 2 --output-dir output/reposters
174
226
  ```
175
227
 
176
228
  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
229
 
230
+ `post-comments --max-comments all --max-pages all` removes comment/page caps; `--max-depth` still requires a positive integer (default 2). New discovery/network commands also accept `all` for `--max-pages` and `--limit`. Unlimited budgets do not remove visibility or rate limits.
231
+
178
232
  ## Export, migration and verification
179
233
 
180
234
  These operations work offline, without browser credentials:
@@ -208,17 +262,17 @@ X endpoints may change or impose limits. pyXcom discovers current GraphQL query
208
262
 
209
263
  ## License
210
264
 
211
- pyXcom is released under the [MIT License](LICENSE). Copyright © 2026 Haocheng Wang.
265
+ pyXcom is released under the [MIT License](https://github.com/haochengw372-hash/pyXcom/blob/main/LICENSE). Copyright © 2026 Haocheng Wang.
212
266
 
213
267
  ## Citation
214
268
 
215
269
  **Author:** Haocheng Wang, Communication University of China.
216
270
 
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).
271
+ 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](https://github.com/haochengw372-hash/pyXcom/blob/main/CITATION.cff).
218
272
 
219
273
  **Suggested reference**
220
274
 
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
275
+ Wang, H. (2026). *pyXcom: Structured and auditable X data collection for communication research* (Version 0.7.0) [Computer software]. https://github.com/haochengw372-hash/pyXcom
222
276
 
223
277
  **BibTeX**
224
278
 
@@ -227,9 +281,15 @@ Wang, H. (2026). *pyXcom: Structured and auditable X data collection for communi
227
281
  author = {Wang, Haocheng},
228
282
  title = {{pyXcom}: Structured and Auditable X Data Collection for Communication Research},
229
283
  year = {2026},
230
- version = {0.6.0},
284
+ version = {0.7.0},
231
285
  url = {https://github.com/haochengw372-hash/pyXcom}
232
286
  }
233
287
  ```
234
288
 
235
289
  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.
290
+
291
+ For longitudinal profile comparisons use `user_snapshots.csv`; `users.csv` consolidates known profile fields. Canonical post tables keep the first saved observation for each ID; later returned text and metrics remain in observation archives and metric snapshots.
292
+
293
+ Offline development checks: `PYTHONPATH=src python -m pytest -q` and `ruff check src tests examples`. The source distribution includes tests; testing tools are development-only, not runtime dependencies.
294
+
295
+ Live validation: current X SearchTimeline/Followers use POST read queries; Retweeters is discovered from public lazy-loaded TweetActivity assets. If a search source returns posts outside explicit dates, they are excluded locally and `manifest.json.date_scope` records the mismatch. An empty bounded result with this warning does not establish historical absence. Native reposts generate only repost edges; embedded quote/reply context is not attributed as another action by the reposter.
@@ -0,0 +1,172 @@
1
+ # Public API — pyXcom 0.7.0
2
+
3
+ The native discovery and network methods have passed offline tests and bounded authenticated live endpoint tests. These tests verify the exercised acquisition paths, not complete historical or network coverage.
4
+
5
+ Import `XClient`, `Profile`, `Post`, `CollectionResult`, `PyXcomError`, `AuthenticationError`, `APIError`, and `RateLimitError` from `pyxcom`.
6
+
7
+ ## Client
8
+
9
+ ```python
10
+ XClient(*, browser="chrome", profile=None, cookie_db=None, proxy=None,
11
+ mirror_base=None, delay=1.0, timeout=30)
12
+ ```
13
+
14
+ 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.
15
+
16
+ ## Lookup methods
17
+
18
+ ```python
19
+ get_user(handle: str) -> Profile
20
+ get_post(post_id_or_url: str) -> Post
21
+ get_posts(post_ids: list[str]) -> list[Post]
22
+ get_raw_post(post_id_or_url: str) -> dict
23
+ get_raw_user_timeline(handle: str, *, timeline="posts", cursor=None) -> dict
24
+ ```
25
+
26
+ `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.
27
+
28
+ ## Account content
29
+
30
+ ```python
31
+ iter_user_posts(handle, *, timeline="posts", since=None, until=None,
32
+ max_pages=None, limit=None) -> Iterator[Post]
33
+ get_user_posts(handle, *, timeline="posts", since=None, until=None,
34
+ max_pages=None, limit=None) -> list[Post]
35
+ save_user_posts(handle, output_dir, *, timeline="posts", since=None, until=None,
36
+ max_pages=None, limit=None) -> CollectionResult
37
+
38
+ iter_user_replies(handle, *, since=None, until=None,
39
+ max_pages=None, limit=None) -> Iterator[Post]
40
+ get_user_replies(handle, *, since=None, until=None,
41
+ max_pages=None, limit=None) -> list[Post]
42
+ save_user_replies(handle, output_dir, *, since=None, until=None,
43
+ max_pages=None, limit=None) -> CollectionResult
44
+
45
+ save_user_activity(handle, output_dir, *, since=None, until=None,
46
+ max_pages=None) -> CollectionResult
47
+ save_users_activity(handles, output_dir, *, since, until, pages_per_round=5,
48
+ rounds=1, wait_on_rate_limit=False, progress=None) -> CollectionResult
49
+ ```
50
+
51
+ `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`.
52
+
53
+ `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.
54
+
55
+ ## Comments received by a post or reply
56
+
57
+ ```python
58
+ iter_post_comments(post_id_or_url: str, *, max_depth: int = 2,
59
+ max_comments: int | None = 100,
60
+ max_pages: int | None = 20,
61
+ since: str | None = None, until: str | None = None) -> Iterator[Post]
62
+ get_post_comments(post_id_or_url: str, *, max_depth: int = 2,
63
+ max_comments: int | None = 100,
64
+ max_pages: int | None = 20,
65
+ since: str | None = None, until: str | None = None) -> list[Post]
66
+ save_post_comments(post_id_or_url: str, output_dir: str | Path, *,
67
+ max_depth: int = 2, max_comments: int | None = 100,
68
+ max_pages: int | None = 20,
69
+ since: str | None = None, until: str | None = None) -> CollectionResult
70
+ ```
71
+
72
+ These methods expand descendants of the seed from all visible authors, including when the seed itself is a reply. They differ from `*_user_replies`, which fetch only one account's authored replies. `get` and `iter` return descendants only; `save` preserves the seed in its proper main/comment table, profiles in `users.csv`, comments in `comments.csv`, and private traversal state for resume. Ancestor context is retained in `context_posts.csv` and is not counted as descendants.
73
+
74
+ Traversal depth is relative to the seed (direct replies=1), recorded as `seed_post_id` and `seed_relative_depth`. Actual platform `root_post_id`, `parent_post_id` and `depth` remain separate; missing ancestors can leave actual `depth` unresolved. UTC `since`/`until` filter returned descendants while preserving traversal context. 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 seed/depth/date scope requires a separate dataset. Completion only means visible traversal within the requested depth ended, not an exhaustive historical thread. CLI comment/page caps accept `all` for Python `None`; depth must remain a positive integer.
75
+
76
+ ## Keyword search
77
+
78
+ ```python
79
+ iter_search_posts(keyword, *, handle=None, since=None, until=None,
80
+ max_pages=None, limit=None) -> Iterator[Post]
81
+ get_search_posts(keyword, *, handle=None, since=None, until=None,
82
+ max_pages=None, limit=None) -> list[Post]
83
+ save_search_posts(keyword, output_dir, *, handle=None, since=None, until=None,
84
+ max_pages=None, limit=None) -> CollectionResult
85
+ ```
86
+
87
+ `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.
88
+
89
+ ## Native discovery
90
+
91
+ ```python
92
+ iter_search_query(query, *, since=None, until=None, max_pages=None, limit=None) -> Iterator[Post]
93
+ get_search_query(query, *, since=None, until=None, max_pages=None, limit=None) -> list[Post]
94
+ save_search_query(query, output_dir, *, since=None, until=None,
95
+ max_pages=None, limit=None) -> CollectionResult
96
+
97
+ iter_post_quotes(post_id_or_url, *, since=None, until=None,
98
+ max_pages=None, limit=None) -> Iterator[Post]
99
+ get_post_quotes(post_id_or_url, *, since=None, until=None,
100
+ max_pages=None, limit=None) -> list[Post]
101
+ save_post_quotes(post_id_or_url, output_dir, *, since=None, until=None,
102
+ max_pages=None, limit=None) -> CollectionResult
103
+
104
+ iter_user_reposts(handle, *, since=None, until=None,
105
+ max_pages=None, limit=None) -> Iterator[Post]
106
+ get_user_reposts(handle, *, since=None, until=None,
107
+ max_pages=None, limit=None) -> list[Post]
108
+ save_user_reposts(handle, output_dir, *, since=None, until=None,
109
+ max_pages=None, limit=None) -> CollectionResult
110
+ ```
111
+
112
+ All options are explicit and keyword-only. Dates are inclusive/exclusive UTC dates; budgets are positive integers or `None`. Native search uses X Latest, preserves query operators, and does not use the mirror. `*_post_quotes` discovers candidates using `quoted_tweet_id:` and verifies the returned target ID. Its result is a visible search sample, not the complete quote population. `*_user_reposts` parses visible repost wrappers in the account timeline, preserving the action ID/time and original target ID/time; it cannot infer missing reposts. If X returns flattened originals instead of wrappers, action times cannot be recovered; the save reports incomplete `repost_activity_unavailable` rather than substituting original publication times.
113
+
114
+ Saved discovery queries retain original/effective query provenance, raw response pages and resumable cursors. Save scopes cannot be changed within the same directory. Use `complete` and `reason` to report caps, source end, rate limits or stalled pagination. A visible source end does not establish historical exhaustiveness.
115
+
116
+ ## Network lists and snapshots
117
+
118
+ ```python
119
+ iter_followers(user_id, *, max_pages=None, limit=None) -> Iterator[Profile]
120
+ get_followers(user_id, *, max_pages=None, limit=None) -> list[Profile]
121
+ save_followers(user_id, output_dir, *, max_pages=None, limit=None,
122
+ snapshot_id=None) -> CollectionResult
123
+
124
+ iter_following(user_id, *, max_pages=None, limit=None) -> Iterator[Profile]
125
+ get_following(user_id, *, max_pages=None, limit=None) -> list[Profile]
126
+ save_following(user_id, output_dir, *, max_pages=None, limit=None,
127
+ snapshot_id=None) -> CollectionResult
128
+
129
+ iter_post_reposters(post_id_or_url, *, max_pages=None, limit=None) -> Iterator[Profile]
130
+ get_post_reposters(post_id_or_url, *, max_pages=None, limit=None) -> list[Profile]
131
+ save_post_reposters(post_id_or_url, output_dir, *, max_pages=None, limit=None,
132
+ snapshot_id=None) -> CollectionResult
133
+ ```
134
+
135
+ `user_id` is a numeric ID string, resolved with `get_user(handle).id`. Network methods have no historical date filter. Saves write `follow_edges.csv` or `reposters.csv`, `users.csv`, `user_snapshots.csv`, and `network_manifest.json`. Network and post collections must use separate output directories to protect their different table layouts. `CollectionResult.post_count` is the observed user count for these methods. Follow edges point follower → followed account. Reposter edges point reposter → original author with `target_post_id`; `action_time_utc` remains unknown because the user list does not return repost activity times. Discovery/network CLI budgets accept `all` for Python `None`.
136
+
137
+ `snapshot_id` accepts 1–128 letters, digits, underscores or hyphens. Without it, a save resumes a recoverable unfinished snapshot for that source, or creates a new snapshot after completion or a terminal bad cursor. Reusing an explicit completed ID returns that observation. History and raw pages remain under `.pyxcom/networks/`; the network manifest records completion and stop reasons per snapshot. `get`/`iter` raise `APIError`, `ParseError` or `RateLimitError` on source failures and set `last_network_collection`; budget stops return the prefix. Saves return incomplete status with resumable checkpoints for recoverable failures. Snapshot time is the time of observation, not relationship creation time. Present-day follower/following lists cannot backfill historical follow networks.
138
+
139
+ ## Observation archives
140
+
141
+ Saved post collections retain page responses under `.pyxcom/raw/` and collection provenance under `.pyxcom/collection_log.jsonl`; network responses are kept under each snapshot state. Re-observing a post preserves returned engagement-count history in `metric_snapshots.csv` and `.pyxcom/metric_snapshots.jsonl`, with observations retained in `.pyxcom/observations.jsonl`, while public post tables remain deduplicated by ID. Unknown counts remain unavailable, not zero. A repeated observation does not reconstruct an earlier historical metric. Raw archives contain platform payloads, not request cookies or authorization headers.
142
+
143
+ `post_edges.csv` exports observed reply, quote and repost edges when present. Columns are `source_post_id`, `target_post_id`, `source_user_id`, `target_user_id`, `edge_type`, `action_time_utc`, `observed_at_utc`, `target_post_available`, `target_author_resolved` and `observation_role`. Edges point from the acting user/post to the target. Unavailable targets remain in the table; unresolved target user IDs stay empty with `target_author_resolved=false`. Known but otherwise unobserved target authors have stub user rows (`profile_available=false`). Context-only records do not generate source edges. Relationship, context and metric exports are conditional on corresponding observations being present.
144
+
145
+ ## Offline dataset functions
146
+
147
+ ```python
148
+ export_tables(output_dir: str | Path) -> dict
149
+ validate_tables(output_dir: str | Path) -> dict
150
+ validate_collection(output_dir: str | Path) -> dict
151
+ validate_network_collection(output_dir: str | Path) -> dict
152
+ finalize_collection(output_dir: str | Path) -> CollectionResult
153
+ schema_summary(output_dir: str | Path) -> dict
154
+ write_schema_report(output_dir: str | Path) -> dict
155
+ apply_role_schema(output_dir: str | Path) -> dict
156
+ ```
157
+
158
+ 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.
159
+
160
+ Post export/finalization/schema helpers operate on post datasets. `validate_collection` automatically recognizes network datasets; `validate_network_collection` explicitly verifies network snapshot hashes, directions and provenance. To resume or regenerate network exports, repeat the corresponding network `save_*` call.
161
+
162
+ ## Backward-compatible names
163
+
164
+ | Previous API | Preferred API |
165
+ | --- | --- |
166
+ | `iter_search(..., user=...)` | `iter_search_posts(..., handle=...)` |
167
+ | `get_search(..., user=...)` | `get_search_posts(..., handle=...)` |
168
+ | `save_search(..., user=...)` | `save_search_posts(..., handle=...)` |
169
+ | `save_accounts(...)` | `save_users_activity(...)` |
170
+ | `*_user_posts(..., timeline="replies")` | `*_user_replies(...)` |
171
+
172
+ No removal is scheduled. These names call the same implementation; no separate authentication flow or duplicate scraper is introduced.
@@ -36,18 +36,18 @@ production releases.
36
36
  `twine check`, installs the wheel into a clean virtual environment, runs the
37
37
  unit tests, and checks the CLI. The separate publish job downloads those exact
38
38
  artifacts and uploads them with an OIDC identity.
39
- 4. Install the TestPyPI release in a new environment. For version `0.6.0`:
39
+ 4. Install the TestPyPI release in a new environment. For version `0.7.0`:
40
40
 
41
41
  ```bash
42
- python3 -m venv /tmp/pyxcom-testpypi-060
43
- /tmp/pyxcom-testpypi-060/bin/python -m pip install \
42
+ python3 -m venv /tmp/pyxcom-testpypi-070
43
+ /tmp/pyxcom-testpypi-070/bin/python -m pip install \
44
44
  'browser-cookie3>=0.19,<1' 'beautifulsoup4>=4.12,<5' 'httpx>=0.27,<1'
45
- /tmp/pyxcom-testpypi-060/bin/python -m pip install \
46
- --index-url https://test.pypi.org/simple/ --no-deps 'pyXcom==0.6.0'
47
- /tmp/pyxcom-testpypi-060/bin/python -m pip check
48
- /tmp/pyxcom-testpypi-060/bin/python -c \
45
+ /tmp/pyxcom-testpypi-070/bin/python -m pip install \
46
+ --index-url https://test.pypi.org/simple/ --no-deps 'pyXcom==0.7.0'
47
+ /tmp/pyxcom-testpypi-070/bin/python -m pip check
48
+ /tmp/pyxcom-testpypi-070/bin/python -c \
49
49
  'from importlib.metadata import version; from pyxcom import XClient; print(version("pyXcom"))'
50
- /tmp/pyxcom-testpypi-060/bin/pyxcom --help
50
+ /tmp/pyxcom-testpypi-070/bin/pyxcom --help
51
51
  ```
52
52
 
53
53
  Dependencies come from the normal PyPI index, while the package under test
@@ -65,11 +65,11 @@ After the production jobs succeed, verify the public index in another new virtua
65
65
  environment:
66
66
 
67
67
  ```bash
68
- python3 -m venv /tmp/pyxcom-pypi-060
69
- /tmp/pyxcom-pypi-060/bin/python -m pip install --index-url https://pypi.org/simple/ 'pyXcom==0.6.0'
70
- /tmp/pyxcom-pypi-060/bin/python -m pip check
71
- /tmp/pyxcom-pypi-060/bin/python -c 'from importlib.metadata import version; from pyxcom import XClient; print(version("pyXcom"))'
72
- /tmp/pyxcom-pypi-060/bin/pyxcom --help
68
+ python3 -m venv /tmp/pyxcom-pypi-070
69
+ /tmp/pyxcom-pypi-070/bin/python -m pip install --index-url https://pypi.org/simple/ 'pyXcom==0.7.0'
70
+ /tmp/pyxcom-pypi-070/bin/python -m pip check
71
+ /tmp/pyxcom-pypi-070/bin/python -c 'from importlib.metadata import version; from pyxcom import XClient; print(version("pyXcom"))'
72
+ /tmp/pyxcom-pypi-070/bin/pyxcom --help
73
73
  ```
74
74
 
75
75
  The workflow is manually dispatched; pull requests and branch pushes do not