apivault-substack-archive-scraper 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ApiVault Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: apivault-substack-archive-scraper
3
+ Version: 0.1.0
4
+ Summary: Python client for Substack Archive Scraper on Apify. Run, resume and download results with diagnostics.
5
+ Author: ApiVault Labs
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://apify.com/apivault_labs/substack-archive-scraper
8
+ Project-URL: Source, https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper
9
+ Project-URL: Documentation, https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper#readme
10
+ Keywords: apify,python-sdk,substack,archive,scraper
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # Substack Archive Scraper - Python client
17
+
18
+ Collect public Substack archive posts, dates, URLs and optional public article text.
19
+
20
+ This client starts the hosted [Apify Actor](https://apify.com/apivault_labs/substack-archive-scraper), waits for completion and downloads Dataset records. It also reads SUMMARY and ERRORS when available. Source coverage and result fields depend on the Actor.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install apivault-substack-archive-scraper
26
+ ```
27
+
28
+ ## Run
29
+
30
+ Set `APIFY_TOKEN` in your environment. Replace the example values with your own valid inputs from the [Actor input form](https://apify.com/apivault_labs/substack-archive-scraper/input).
31
+
32
+ ```python
33
+ from apivault_substack_archive_scraper import Client, WaitTimeout
34
+
35
+ client = Client()
36
+ actor_input = {
37
+ "urls": [
38
+ "https://www.experimental-history.com"
39
+ ],
40
+ "maxItems": 5
41
+ }
42
+ result = client.run(actor_input, max_total_charge_usd=0.05, max_rows=100)
43
+ print(result.run_id, len(result.rows), result.truncated)
44
+ print(result.summary)
45
+ ```
46
+
47
+ An Apify account and token are required. The package has no subscription of its own; hosted runs are billed by Apify under the Actor pricing. The event charge cap may not include platform usage. Review the [current price](https://apify.com/apivault_labs/substack-archive-scraper/pricing) before changing it. `max_rows` limits the download, not the Actor collection or its charges; configure an Actor result limit too.
48
+
49
+ ## Resume and diagnostics
50
+
51
+ `client.start(input)` returns a run ID; `client.wait(run_id)` and `client.result(run_id)` resume an existing run. A `WaitTimeout` exposes `run_id`. Starting a run is never automatically retried. Use an existing run ID after a connection or waiting failure to avoid duplicate paid runs. Failed and aborted runs raise `ActorError`. No-match and partial results require inspecting `summary` and `errors`; diagnostic rows must not be mistaken for useful results.
52
+
53
+ Rows can be serialized as JSON or used in your own CSV/Excel pipeline. Native Dataset exports are available in Apify. Optional integrations and credentials depend on the Actor.
54
+
55
+ [Hosted Actor](https://apify.com/apivault_labs/substack-archive-scraper) | [Source](https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper) | [PyPI](https://pypi.org/project/apivault-substack-archive-scraper/)
56
+
57
+
58
+ ## Related integration
59
+
60
+ [Python client](https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper) · [PyPI](https://pypi.org/project/apivault-substack-archive-scraper/) · [n8n source and workflow](https://github.com/apivault-labs/apivault-labs/tree/main/community-nodes/n8n-nodes-apivault-substack-archive-scraper) · [npm](https://www.npmjs.com/package/n8n-nodes-apivault-substack-archive-scraper)
61
+
62
+
63
+ ## Source coverage and limits
64
+
65
+ Public archives and explicit post URLs only. Paid posts provide metadata and a short preview; no subscriber content, login or paywall bypass. Inspect source diagnostics and continuation limits. Reading a saved run does not continue the archive; copy continuation inputs into a new separately billed run.
66
+
67
+
68
+ ## Collection and spend limits
69
+
70
+ The small example sets `maxItems` to bound paid collection. `max_rows` / Maximum Rows to Read limits only the dataset download. The event charge cap does not cap infrastructure or proxy usage. The clients do not start an automatic paid retry after a start request fails. Save the run ID and inspect that run before starting another. For source failures, inspect SUMMARY and ERRORS in Apify Console; Client.result only returns successful runs.
71
+
72
+ The hosted Actor also charges an Actor Start event at USD 0.00005 per GB of configured memory, minimum one event, in addition to delivered-result events and infrastructure usage. Empty or failed runs can still incur start and resource costs.
@@ -0,0 +1,57 @@
1
+ # Substack Archive Scraper - Python client
2
+
3
+ Collect public Substack archive posts, dates, URLs and optional public article text.
4
+
5
+ This client starts the hosted [Apify Actor](https://apify.com/apivault_labs/substack-archive-scraper), waits for completion and downloads Dataset records. It also reads SUMMARY and ERRORS when available. Source coverage and result fields depend on the Actor.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install apivault-substack-archive-scraper
11
+ ```
12
+
13
+ ## Run
14
+
15
+ Set `APIFY_TOKEN` in your environment. Replace the example values with your own valid inputs from the [Actor input form](https://apify.com/apivault_labs/substack-archive-scraper/input).
16
+
17
+ ```python
18
+ from apivault_substack_archive_scraper import Client, WaitTimeout
19
+
20
+ client = Client()
21
+ actor_input = {
22
+ "urls": [
23
+ "https://www.experimental-history.com"
24
+ ],
25
+ "maxItems": 5
26
+ }
27
+ result = client.run(actor_input, max_total_charge_usd=0.05, max_rows=100)
28
+ print(result.run_id, len(result.rows), result.truncated)
29
+ print(result.summary)
30
+ ```
31
+
32
+ An Apify account and token are required. The package has no subscription of its own; hosted runs are billed by Apify under the Actor pricing. The event charge cap may not include platform usage. Review the [current price](https://apify.com/apivault_labs/substack-archive-scraper/pricing) before changing it. `max_rows` limits the download, not the Actor collection or its charges; configure an Actor result limit too.
33
+
34
+ ## Resume and diagnostics
35
+
36
+ `client.start(input)` returns a run ID; `client.wait(run_id)` and `client.result(run_id)` resume an existing run. A `WaitTimeout` exposes `run_id`. Starting a run is never automatically retried. Use an existing run ID after a connection or waiting failure to avoid duplicate paid runs. Failed and aborted runs raise `ActorError`. No-match and partial results require inspecting `summary` and `errors`; diagnostic rows must not be mistaken for useful results.
37
+
38
+ Rows can be serialized as JSON or used in your own CSV/Excel pipeline. Native Dataset exports are available in Apify. Optional integrations and credentials depend on the Actor.
39
+
40
+ [Hosted Actor](https://apify.com/apivault_labs/substack-archive-scraper) | [Source](https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper) | [PyPI](https://pypi.org/project/apivault-substack-archive-scraper/)
41
+
42
+
43
+ ## Related integration
44
+
45
+ [Python client](https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper) · [PyPI](https://pypi.org/project/apivault-substack-archive-scraper/) · [n8n source and workflow](https://github.com/apivault-labs/apivault-labs/tree/main/community-nodes/n8n-nodes-apivault-substack-archive-scraper) · [npm](https://www.npmjs.com/package/n8n-nodes-apivault-substack-archive-scraper)
46
+
47
+
48
+ ## Source coverage and limits
49
+
50
+ Public archives and explicit post URLs only. Paid posts provide metadata and a short preview; no subscriber content, login or paywall bypass. Inspect source diagnostics and continuation limits. Reading a saved run does not continue the archive; copy continuation inputs into a new separately billed run.
51
+
52
+
53
+ ## Collection and spend limits
54
+
55
+ The small example sets `maxItems` to bound paid collection. `max_rows` / Maximum Rows to Read limits only the dataset download. The event charge cap does not cap infrastructure or proxy usage. The clients do not start an automatic paid retry after a start request fails. Save the run ID and inspect that run before starting another. For source failures, inspect SUMMARY and ERRORS in Apify Console; Client.result only returns successful runs.
56
+
57
+ The hosted Actor also charges an Actor Start event at USD 0.00005 per GB of configured memory, minimum one event, in addition to delivered-result events and infrastructure usage. Empty or failed runs can still incur start and resource costs.
@@ -0,0 +1,4 @@
1
+ from .client import Client, RunResult, ActorError, WaitTimeout
2
+
3
+ __all__ = ["Client", "RunResult", "ActorError", "WaitTimeout"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,113 @@
1
+ """Thin client for the hosted substack-archive-scraper Apify Actor."""
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass
4
+ import json, os, re, time, urllib.error, urllib.parse, urllib.request
5
+ from typing import Any, Mapping
6
+
7
+ ACTOR_ID = "nJFN711cBZgSpDITt"
8
+ API = "https://api.apify.com/v2"
9
+
10
+ class ActorError(RuntimeError):
11
+ """The API rejected a request or the run did not succeed."""
12
+
13
+ class WaitTimeout(ActorError):
14
+ """Waiting ended; resume this run instead of starting a duplicate."""
15
+ def __init__(self, run_id: str):
16
+ self.run_id = run_id
17
+ super().__init__(f"Waiting timed out; resume run {run_id}")
18
+
19
+ @dataclass
20
+ class RunResult:
21
+ run_id: str
22
+ dataset_id: str
23
+ status: str
24
+ rows: list[dict[str, Any]]
25
+ summary: Any
26
+ errors: Any
27
+ truncated: bool
28
+
29
+ class Client:
30
+ """Start one hosted run, wait safely, and read rows plus diagnostics."""
31
+ def __init__(self, token: str | None = None):
32
+ self._token = (token or os.environ.get("APIFY_TOKEN") or os.environ.get("APIFY_API_TOKEN") or "").strip()
33
+ if not self._token:
34
+ raise ValueError("Provide an Apify token or set APIFY_TOKEN")
35
+
36
+ @staticmethod
37
+ def _id(value: str) -> str:
38
+ if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", value):
39
+ raise ValueError("Invalid Apify identifier")
40
+ return value
41
+
42
+ def _request(self, method: str, path: str, body: Any = None, params: dict | None = None, optional: bool = False) -> Any:
43
+ url = API + path + ("?" + urllib.parse.urlencode(params) if params else "")
44
+ request = urllib.request.Request(url, method=method,
45
+ data=json.dumps(body).encode("utf-8") if body is not None else None,
46
+ headers={"Authorization": "Bearer " + self._token, "Content-Type": "application/json", "User-Agent": "apivault-substack-archive-scraper/0.1.0"})
47
+ try:
48
+ with urllib.request.urlopen(request, timeout=30) as response:
49
+ raw = response.read()
50
+ return json.loads(raw) if raw else None
51
+ except urllib.error.HTTPError as error:
52
+ if optional and error.code == 404:
53
+ return None
54
+ raise ActorError(f"Apify request failed with HTTP {error.code}") from None
55
+ except (urllib.error.URLError, TimeoutError):
56
+ raise ActorError("Apify request could not complete; check an existing run before retrying") from None
57
+
58
+ def start(self, actor_input: Mapping[str, Any], *, max_total_charge_usd: float = 0.05, actor_timeout_secs: int = 300) -> dict[str, Any]:
59
+ if not isinstance(actor_input, Mapping):
60
+ raise TypeError("actor_input must be a mapping")
61
+ if not 0 < max_total_charge_usd <= 1000 or not 1 <= actor_timeout_secs <= 3600:
62
+ raise ValueError("Use a positive charge cap and timeout of 1–3600 seconds")
63
+ # POST is never automatically retried: a retry could start a paid duplicate.
64
+ response = self._request("POST", f"/acts/{ACTOR_ID}/runs", dict(actor_input),
65
+ {"maxTotalChargeUsd": max_total_charge_usd, "timeout": actor_timeout_secs, "build": "latest"})
66
+ run = response.get("data", {}) if isinstance(response, dict) else {}
67
+ if not run.get("id"):
68
+ raise ActorError("Apify did not return a run ID; check Console before retrying")
69
+ return run
70
+
71
+ def wait(self, run_id: str, *, wait_secs: float = 600, poll_interval: float = 3) -> dict[str, Any]:
72
+ run_id = self._id(run_id)
73
+ if wait_secs <= 0 or poll_interval <= 0:
74
+ raise ValueError("wait_secs and poll_interval must be positive")
75
+ deadline = time.monotonic() + wait_secs
76
+ while time.monotonic() < deadline:
77
+ run = self._request("GET", f"/actor-runs/{run_id}")["data"]
78
+ if run["status"] == "SUCCEEDED":
79
+ return run
80
+ if run["status"] in {"FAILED", "ABORTED", "TIMED-OUT"}:
81
+ raise ActorError(f"Run {run_id} ended with status {run['status']}")
82
+ time.sleep(min(poll_interval, max(0, deadline-time.monotonic())))
83
+ raise WaitTimeout(run_id)
84
+
85
+ def result(self, run_id: str, *, max_rows: int = 1000) -> RunResult:
86
+ run_id = self._id(run_id)
87
+ if not isinstance(max_rows, int) or not 1 <= max_rows <= 100000:
88
+ raise ValueError("max_rows must be an integer from 1 to 100000")
89
+ run = self._request("GET", f"/actor-runs/{run_id}")["data"]
90
+ if run["status"] != "SUCCEEDED":
91
+ raise ActorError(f"Run {run_id} is {run['status']}; wait before reading results")
92
+ dataset = self._id(run["defaultDatasetId"])
93
+ metadata = self._request("GET", f"/datasets/{dataset}")["data"]
94
+ rows: list[dict[str, Any]] = []
95
+ while len(rows) < max_rows:
96
+ limit = min(1000, max_rows-len(rows))
97
+ page = self._request("GET", f"/datasets/{dataset}/items", params={"clean": "true", "limit": limit, "offset": len(rows)})
98
+ if not isinstance(page, list):
99
+ raise ActorError("Apify returned an unexpected Dataset response")
100
+ if len(page) > limit:
101
+ raise ActorError("Dataset response exceeded the requested page limit")
102
+ rows.extend(page)
103
+ if len(page) < limit:
104
+ break
105
+ store = self._id(run["defaultKeyValueStoreId"])
106
+ summary = self._request("GET", f"/key-value-stores/{store}/records/SUMMARY", optional=True)
107
+ errors = self._request("GET", f"/key-value-stores/{store}/records/ERRORS", optional=True)
108
+ return RunResult(run_id, dataset, run["status"], rows, summary, errors, metadata.get("itemCount", len(rows)) > len(rows))
109
+
110
+ def run(self, actor_input: Mapping[str, Any], *, max_total_charge_usd: float = 0.05, actor_timeout_secs: int = 300, wait_secs: float = 600, max_rows: int = 1000) -> RunResult:
111
+ run = self.start(actor_input, max_total_charge_usd=max_total_charge_usd, actor_timeout_secs=actor_timeout_secs)
112
+ self.wait(run["id"], wait_secs=wait_secs)
113
+ return self.result(run["id"], max_rows=max_rows)
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: apivault-substack-archive-scraper
3
+ Version: 0.1.0
4
+ Summary: Python client for Substack Archive Scraper on Apify. Run, resume and download results with diagnostics.
5
+ Author: ApiVault Labs
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://apify.com/apivault_labs/substack-archive-scraper
8
+ Project-URL: Source, https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper
9
+ Project-URL: Documentation, https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper#readme
10
+ Keywords: apify,python-sdk,substack,archive,scraper
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # Substack Archive Scraper - Python client
17
+
18
+ Collect public Substack archive posts, dates, URLs and optional public article text.
19
+
20
+ This client starts the hosted [Apify Actor](https://apify.com/apivault_labs/substack-archive-scraper), waits for completion and downloads Dataset records. It also reads SUMMARY and ERRORS when available. Source coverage and result fields depend on the Actor.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install apivault-substack-archive-scraper
26
+ ```
27
+
28
+ ## Run
29
+
30
+ Set `APIFY_TOKEN` in your environment. Replace the example values with your own valid inputs from the [Actor input form](https://apify.com/apivault_labs/substack-archive-scraper/input).
31
+
32
+ ```python
33
+ from apivault_substack_archive_scraper import Client, WaitTimeout
34
+
35
+ client = Client()
36
+ actor_input = {
37
+ "urls": [
38
+ "https://www.experimental-history.com"
39
+ ],
40
+ "maxItems": 5
41
+ }
42
+ result = client.run(actor_input, max_total_charge_usd=0.05, max_rows=100)
43
+ print(result.run_id, len(result.rows), result.truncated)
44
+ print(result.summary)
45
+ ```
46
+
47
+ An Apify account and token are required. The package has no subscription of its own; hosted runs are billed by Apify under the Actor pricing. The event charge cap may not include platform usage. Review the [current price](https://apify.com/apivault_labs/substack-archive-scraper/pricing) before changing it. `max_rows` limits the download, not the Actor collection or its charges; configure an Actor result limit too.
48
+
49
+ ## Resume and diagnostics
50
+
51
+ `client.start(input)` returns a run ID; `client.wait(run_id)` and `client.result(run_id)` resume an existing run. A `WaitTimeout` exposes `run_id`. Starting a run is never automatically retried. Use an existing run ID after a connection or waiting failure to avoid duplicate paid runs. Failed and aborted runs raise `ActorError`. No-match and partial results require inspecting `summary` and `errors`; diagnostic rows must not be mistaken for useful results.
52
+
53
+ Rows can be serialized as JSON or used in your own CSV/Excel pipeline. Native Dataset exports are available in Apify. Optional integrations and credentials depend on the Actor.
54
+
55
+ [Hosted Actor](https://apify.com/apivault_labs/substack-archive-scraper) | [Source](https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper) | [PyPI](https://pypi.org/project/apivault-substack-archive-scraper/)
56
+
57
+
58
+ ## Related integration
59
+
60
+ [Python client](https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper) · [PyPI](https://pypi.org/project/apivault-substack-archive-scraper/) · [n8n source and workflow](https://github.com/apivault-labs/apivault-labs/tree/main/community-nodes/n8n-nodes-apivault-substack-archive-scraper) · [npm](https://www.npmjs.com/package/n8n-nodes-apivault-substack-archive-scraper)
61
+
62
+
63
+ ## Source coverage and limits
64
+
65
+ Public archives and explicit post URLs only. Paid posts provide metadata and a short preview; no subscriber content, login or paywall bypass. Inspect source diagnostics and continuation limits. Reading a saved run does not continue the archive; copy continuation inputs into a new separately billed run.
66
+
67
+
68
+ ## Collection and spend limits
69
+
70
+ The small example sets `maxItems` to bound paid collection. `max_rows` / Maximum Rows to Read limits only the dataset download. The event charge cap does not cap infrastructure or proxy usage. The clients do not start an automatic paid retry after a start request fails. Save the run ID and inspect that run before starting another. For source failures, inspect SUMMARY and ERRORS in Apify Console; Client.result only returns successful runs.
71
+
72
+ The hosted Actor also charges an Actor Start event at USD 0.00005 per GB of configured memory, minimum one event, in addition to delivered-result events and infrastructure usage. Empty or failed runs can still incur start and resource costs.
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ apivault_substack_archive_scraper/__init__.py
5
+ apivault_substack_archive_scraper/client.py
6
+ apivault_substack_archive_scraper.egg-info/PKG-INFO
7
+ apivault_substack_archive_scraper.egg-info/SOURCES.txt
8
+ apivault_substack_archive_scraper.egg-info/dependency_links.txt
9
+ apivault_substack_archive_scraper.egg-info/top_level.txt
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "apivault-substack-archive-scraper"
7
+ version = "0.1.0"
8
+ description = "Python client for Substack Archive Scraper on Apify. Run, resume and download results with diagnostics."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name="ApiVault Labs"}]
14
+ keywords = ["apify", "python-sdk", "substack", "archive", "scraper"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://apify.com/apivault_labs/substack-archive-scraper"
18
+ Source = "https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper"
19
+ Documentation = "https://github.com/apivault-labs/apivault-labs/tree/main/packages/apivault-substack-archive-scraper#readme"
20
+
21
+ [tool.setuptools.packages.find]
22
+ include = ["apivault_substack_archive_scraper*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+