speedyindex 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,35 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: python -m pip install --upgrade build
17
+ - run: PYTHONPATH=src python -m unittest discover -s tests -v
18
+ - run: python -m build
19
+ - uses: actions/upload-artifact@v4
20
+ with:
21
+ name: dist
22
+ path: dist/
23
+
24
+ publish:
25
+ needs: build
26
+ runs-on: ubuntu-latest
27
+ environment: pypi
28
+ permissions:
29
+ id-token: write # Trusted Publishing: no API token stored anywhere
30
+ steps:
31
+ - uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,4 @@
1
+ dist/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpeedyIndex
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,123 @@
1
+ Metadata-Version: 2.5
2
+ Name: speedyindex
3
+ Version: 0.1.0
4
+ Summary: Python client and CLI for the SpeedyIndex API: Google, Bing and Yandex index checks, link indexing tasks and reports.
5
+ Project-URL: Homepage, https://en.speedyindex.com/
6
+ Project-URL: Documentation, https://en.speedyindex.com/api.php
7
+ Project-URL: Source, https://github.com/uskotoivo81-netizen/speedyindex-python
8
+ Author: SpeedyIndex
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backlinks,bing,google index checker,indexing,search console,seo,speedyindex,yandex
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+
21
+ # speedyindex — Python client for the SpeedyIndex API
22
+
23
+ `speedyindex` is a small, dependency-free Python client and command-line tool for SpeedyIndex, a service for link indexing and Google, Bing and Yandex index checking. It wraps the public [SpeedyIndex API](https://en.speedyindex.com/api.php) v2, so you can check whether a list of URLs is indexed, submit links for indexing and pull reports from scripts, cron jobs or CI.
24
+
25
+ - Standard library only (`urllib`), Python 3.8+
26
+ - Index checks for Google, Bing and Yandex
27
+ - Link indexing tasks for Google and Yandex, with optional drip-feed
28
+ - Task status, task list and full reports
29
+ - Backlink checks: find links to your domains on donor pages
30
+ - `speedyindex` CLI for quick jobs from the shell
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install speedyindex
36
+ ```
37
+
38
+ ## API key
39
+
40
+ Every request needs an API key. You can find it in your SpeedyIndex dashboard or in the SpeedyIndex Telegram bot settings. The client reads it from the `SPEEDYINDEX_API_KEY` environment variable:
41
+
42
+ ```bash
43
+ export SPEEDYINDEX_API_KEY="your-key"
44
+ ```
45
+
46
+ Keep the key out of source code and commit history; use environment variables or your CI secret store.
47
+
48
+ ## Quick start (Python)
49
+
50
+ ```python
51
+ from speedyindex import SpeedyIndex
52
+
53
+ si = SpeedyIndex() # reads SPEEDYINDEX_API_KEY
54
+
55
+ print(si.balance())
56
+
57
+ # Check which URLs are in Google's index
58
+ task = si.create_task(
59
+ ["https://example.com/", "https://example.com/blog/post-1"],
60
+ engine="google",
61
+ task_type="checker",
62
+ title="blog check",
63
+ )
64
+ print(task)
65
+
66
+ # Later: progress and the full report
67
+ task_id = si.list_tasks(engine="google")["result"][0]["id"] # newest task
68
+ status = si.task_status([task_id], engine="google", task_type="checker")
69
+ report = si.full_report(task_id, engine="google", task_type="checker")
70
+ ```
71
+
72
+ Submit links for indexing, spread over several days:
73
+
74
+ ```python
75
+ si.create_task(urls, engine="google", task_type="indexer", drip_feed=True, drip_feed_days=7)
76
+ ```
77
+
78
+ Check donor pages for links to your site:
79
+
80
+ ```python
81
+ si.create_backlink_check(["https://donor.example/page"], target_domains=["mysite.com"])
82
+ ```
83
+
84
+ ## Command line
85
+
86
+ ```bash
87
+ speedyindex balance
88
+ speedyindex check urls.txt --engine google --title "weekly check"
89
+ speedyindex check urls.txt --engine bing
90
+ speedyindex index urls.txt --engine google --drip-feed-days 5
91
+ speedyindex status 65f8c7315752853b9171860a --type checker
92
+ speedyindex report 65f8c7315752853b9171860a --type checker > report.json
93
+ speedyindex tasks --engine google --page 0
94
+ speedyindex backlinks donors.txt --target mysite.com
95
+ ```
96
+
97
+ `urls.txt` holds one URL per line; empty lines and lines starting with `#` are skipped, duplicates are removed. Use `-` to read URLs from stdin:
98
+
99
+ ```bash
100
+ cat sitemap-urls.txt | speedyindex check - --engine google
101
+ ```
102
+
103
+ ## Limits the client enforces
104
+
105
+ | Rule | Value |
106
+ |---|---|
107
+ | URLs per task | up to 10,000 |
108
+ | Task IDs per status request | up to 1,000 |
109
+ | Drip-feed days | 2–30, and at least as many URLs as days |
110
+ | Bing | `checker` tasks only |
111
+ | Google `indexer` | always sent with `pay_per_indexed: true` (the only mode the API accepts) |
112
+
113
+ API error codes are raised as `SpeedyIndexError` with `.code`: `1` means the balance is too low, `2` means a validation error.
114
+
115
+ ## Tips before you run an index check
116
+
117
+ - Clean the list first: remove UTM parameters, fragments and duplicates.
118
+ - Check status codes and `noindex` before submitting: a 404 or a `noindex` page will not be indexed whatever you do.
119
+ - Google's own [URL Inspection tool](https://support.google.com/webmasters/answer/9012289) remains the reference for a single URL on a site you own.
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,103 @@
1
+ # speedyindex — Python client for the SpeedyIndex API
2
+
3
+ `speedyindex` is a small, dependency-free Python client and command-line tool for SpeedyIndex, a service for link indexing and Google, Bing and Yandex index checking. It wraps the public [SpeedyIndex API](https://en.speedyindex.com/api.php) v2, so you can check whether a list of URLs is indexed, submit links for indexing and pull reports from scripts, cron jobs or CI.
4
+
5
+ - Standard library only (`urllib`), Python 3.8+
6
+ - Index checks for Google, Bing and Yandex
7
+ - Link indexing tasks for Google and Yandex, with optional drip-feed
8
+ - Task status, task list and full reports
9
+ - Backlink checks: find links to your domains on donor pages
10
+ - `speedyindex` CLI for quick jobs from the shell
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install speedyindex
16
+ ```
17
+
18
+ ## API key
19
+
20
+ Every request needs an API key. You can find it in your SpeedyIndex dashboard or in the SpeedyIndex Telegram bot settings. The client reads it from the `SPEEDYINDEX_API_KEY` environment variable:
21
+
22
+ ```bash
23
+ export SPEEDYINDEX_API_KEY="your-key"
24
+ ```
25
+
26
+ Keep the key out of source code and commit history; use environment variables or your CI secret store.
27
+
28
+ ## Quick start (Python)
29
+
30
+ ```python
31
+ from speedyindex import SpeedyIndex
32
+
33
+ si = SpeedyIndex() # reads SPEEDYINDEX_API_KEY
34
+
35
+ print(si.balance())
36
+
37
+ # Check which URLs are in Google's index
38
+ task = si.create_task(
39
+ ["https://example.com/", "https://example.com/blog/post-1"],
40
+ engine="google",
41
+ task_type="checker",
42
+ title="blog check",
43
+ )
44
+ print(task)
45
+
46
+ # Later: progress and the full report
47
+ task_id = si.list_tasks(engine="google")["result"][0]["id"] # newest task
48
+ status = si.task_status([task_id], engine="google", task_type="checker")
49
+ report = si.full_report(task_id, engine="google", task_type="checker")
50
+ ```
51
+
52
+ Submit links for indexing, spread over several days:
53
+
54
+ ```python
55
+ si.create_task(urls, engine="google", task_type="indexer", drip_feed=True, drip_feed_days=7)
56
+ ```
57
+
58
+ Check donor pages for links to your site:
59
+
60
+ ```python
61
+ si.create_backlink_check(["https://donor.example/page"], target_domains=["mysite.com"])
62
+ ```
63
+
64
+ ## Command line
65
+
66
+ ```bash
67
+ speedyindex balance
68
+ speedyindex check urls.txt --engine google --title "weekly check"
69
+ speedyindex check urls.txt --engine bing
70
+ speedyindex index urls.txt --engine google --drip-feed-days 5
71
+ speedyindex status 65f8c7315752853b9171860a --type checker
72
+ speedyindex report 65f8c7315752853b9171860a --type checker > report.json
73
+ speedyindex tasks --engine google --page 0
74
+ speedyindex backlinks donors.txt --target mysite.com
75
+ ```
76
+
77
+ `urls.txt` holds one URL per line; empty lines and lines starting with `#` are skipped, duplicates are removed. Use `-` to read URLs from stdin:
78
+
79
+ ```bash
80
+ cat sitemap-urls.txt | speedyindex check - --engine google
81
+ ```
82
+
83
+ ## Limits the client enforces
84
+
85
+ | Rule | Value |
86
+ |---|---|
87
+ | URLs per task | up to 10,000 |
88
+ | Task IDs per status request | up to 1,000 |
89
+ | Drip-feed days | 2–30, and at least as many URLs as days |
90
+ | Bing | `checker` tasks only |
91
+ | Google `indexer` | always sent with `pay_per_indexed: true` (the only mode the API accepts) |
92
+
93
+ API error codes are raised as `SpeedyIndexError` with `.code`: `1` means the balance is too low, `2` means a validation error.
94
+
95
+ ## Tips before you run an index check
96
+
97
+ - Clean the list first: remove UTM parameters, fragments and duplicates.
98
+ - Check status codes and `noindex` before submitting: a 404 or a `noindex` page will not be indexed whatever you do.
99
+ - Google's own [URL Inspection tool](https://support.google.com/webmasters/answer/9012289) remains the reference for a single URL on a site you own.
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "speedyindex"
7
+ version = "0.1.0"
8
+ description = "Python client and CLI for the SpeedyIndex API: Google, Bing and Yandex index checks, link indexing tasks and reports."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [{ name = "SpeedyIndex" }]
13
+ keywords = ["seo", "google index checker", "indexing", "backlinks", "speedyindex", "search console", "bing", "yandex"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
20
+ "Environment :: Console",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Homepage = "https://en.speedyindex.com/"
26
+ Documentation = "https://en.speedyindex.com/api.php"
27
+ Source = "https://github.com/uskotoivo81-netizen/speedyindex-python"
28
+
29
+ [project.scripts]
30
+ speedyindex = "speedyindex.cli:main"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/speedyindex"]
@@ -0,0 +1,6 @@
1
+ """Python client and CLI for the SpeedyIndex API v2."""
2
+
3
+ from .client import DEFAULT_BASE_URL, SpeedyIndex, SpeedyIndexError
4
+
5
+ __all__ = ["SpeedyIndex", "SpeedyIndexError", "DEFAULT_BASE_URL"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,86 @@
1
+ """Command-line interface: ``speedyindex <command>``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import List, Optional
9
+
10
+ from . import __version__
11
+ from .client import SpeedyIndex, SpeedyIndexError
12
+
13
+
14
+ def _read_urls(path: str) -> List[str]:
15
+ stream = sys.stdin if path == "-" else open(path, encoding="utf-8")
16
+ with stream:
17
+ return [line.strip() for line in stream if line.strip()]
18
+
19
+
20
+ def _print(data) -> None:
21
+ print(json.dumps(data, indent=2, ensure_ascii=False))
22
+
23
+
24
+ def build_parser() -> argparse.ArgumentParser:
25
+ p = argparse.ArgumentParser(prog="speedyindex", description="SpeedyIndex API v2 command-line client")
26
+ p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
27
+ p.add_argument("--api-key", help="API key (default: SPEEDYINDEX_API_KEY env var)")
28
+ sub = p.add_subparsers(dest="command", required=True)
29
+
30
+ sub.add_parser("balance", help="show account balance")
31
+
32
+ for name, ttype in (("check", "checker"), ("index", "indexer")):
33
+ s = sub.add_parser(name, help=f"create a {ttype} task from a file of URLs (one per line, '-' for stdin)")
34
+ s.add_argument("file")
35
+ s.add_argument("--engine", default="google", choices=["google", "yandex", "bing"])
36
+ s.add_argument("--title")
37
+ s.add_argument("--drip-feed-days", type=int, help="spread submission over N days (2-30)")
38
+ s.set_defaults(task_type=ttype)
39
+
40
+ s = sub.add_parser("status", help="status of one or more tasks")
41
+ s.add_argument("task_ids", nargs="+")
42
+ s.add_argument("--engine", default="google")
43
+ s.add_argument("--type", dest="task_type", default="checker", choices=["checker", "indexer"])
44
+
45
+ s = sub.add_parser("report", help="full report of a task")
46
+ s.add_argument("task_id")
47
+ s.add_argument("--engine", default="google")
48
+ s.add_argument("--type", dest="task_type", default="checker", choices=["checker", "indexer"])
49
+
50
+ s = sub.add_parser("tasks", help="list tasks")
51
+ s.add_argument("--engine", default="google")
52
+ s.add_argument("--page", type=int, default=0)
53
+
54
+ s = sub.add_parser("backlinks", help="check donor pages for links to your domains")
55
+ s.add_argument("file")
56
+ s.add_argument("--target", action="append", required=True, help="your domain, repeatable")
57
+ return p
58
+
59
+
60
+ def main(argv: Optional[List[str]] = None) -> int:
61
+ args = build_parser().parse_args(argv)
62
+ try:
63
+ client = SpeedyIndex(api_key=args.api_key)
64
+ if args.command == "balance":
65
+ _print(client.balance())
66
+ elif args.command in ("check", "index"):
67
+ _print(client.create_task(
68
+ _read_urls(args.file), engine=args.engine, task_type=args.task_type, title=args.title,
69
+ drip_feed=bool(args.drip_feed_days), drip_feed_days=args.drip_feed_days,
70
+ ))
71
+ elif args.command == "status":
72
+ _print(client.task_status(args.task_ids, engine=args.engine, task_type=args.task_type))
73
+ elif args.command == "report":
74
+ _print(client.full_report(args.task_id, engine=args.engine, task_type=args.task_type))
75
+ elif args.command == "tasks":
76
+ _print(client.list_tasks(engine=args.engine, page=args.page))
77
+ elif args.command == "backlinks":
78
+ _print(client.create_backlink_check(_read_urls(args.file), args.target))
79
+ except (SpeedyIndexError, ValueError, OSError) as exc:
80
+ print(f"error: {exc}", file=sys.stderr)
81
+ return 1
82
+ return 0
83
+
84
+
85
+ if __name__ == "__main__":
86
+ sys.exit(main())
@@ -0,0 +1,183 @@
1
+ """Minimal client for the SpeedyIndex API v2 (standard library only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import urllib.error
8
+ import urllib.request
9
+ from typing import Any, Dict, Iterable, List, Optional
10
+
11
+ DEFAULT_BASE_URL = "https://api.speedyindex.com"
12
+ ENGINES = ("google", "yandex", "bing")
13
+ TASK_TYPES = ("indexer", "checker")
14
+ MAX_URLS_PER_TASK = 10_000
15
+ MAX_TASK_IDS = 1_000
16
+
17
+
18
+ class SpeedyIndexError(Exception):
19
+ """Raised when the API returns a non-zero code or an HTTP error."""
20
+
21
+ def __init__(self, message: str, code: Optional[int] = None, payload: Any = None):
22
+ super().__init__(message)
23
+ self.code = code
24
+ self.payload = payload
25
+
26
+
27
+ def _check_engine(engine: str, task_type: Optional[str] = None) -> None:
28
+ if engine not in ENGINES:
29
+ raise ValueError(f"engine must be one of {ENGINES}, got {engine!r}")
30
+ if task_type is not None:
31
+ if task_type not in TASK_TYPES:
32
+ raise ValueError(f"task_type must be one of {TASK_TYPES}, got {task_type!r}")
33
+ if engine == "bing" and task_type != "checker":
34
+ raise ValueError("Bing supports only the 'checker' task type")
35
+
36
+
37
+ def _clean_urls(urls: Iterable[str]) -> List[str]:
38
+ seen = set()
39
+ result = []
40
+ for url in urls:
41
+ url = url.strip()
42
+ if url and not url.startswith("#") and url not in seen:
43
+ seen.add(url)
44
+ result.append(url)
45
+ return result
46
+
47
+
48
+ class SpeedyIndex:
49
+ """Client for the SpeedyIndex API v2.
50
+
51
+ The API key is read from ``api_key`` or the ``SPEEDYINDEX_API_KEY``
52
+ environment variable. It is sent in the ``Authorization`` header.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ api_key: Optional[str] = None,
58
+ base_url: str = DEFAULT_BASE_URL,
59
+ timeout: float = 60.0,
60
+ ) -> None:
61
+ self.api_key = api_key or os.environ.get("SPEEDYINDEX_API_KEY")
62
+ if not self.api_key:
63
+ raise SpeedyIndexError(
64
+ "No API key: pass api_key=... or set SPEEDYINDEX_API_KEY"
65
+ )
66
+ self.base_url = base_url.rstrip("/")
67
+ self.timeout = timeout
68
+
69
+ # -- transport -------------------------------------------------------
70
+ def _request(self, method: str, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
71
+ data = json.dumps(body).encode("utf-8") if body is not None else None
72
+ req = urllib.request.Request(self.base_url + path, data=data, method=method)
73
+ req.add_header("Authorization", self.api_key)
74
+ req.add_header("Accept", "application/json")
75
+ req.add_header("User-Agent", "speedyindex-python")
76
+ if data is not None:
77
+ req.add_header("Content-Type", "application/json")
78
+ try:
79
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
80
+ raw = resp.read().decode("utf-8")
81
+ except urllib.error.HTTPError as exc:
82
+ raise SpeedyIndexError(f"HTTP {exc.code} for {path}", code=exc.code) from exc
83
+ try:
84
+ payload = json.loads(raw)
85
+ except ValueError as exc:
86
+ raise SpeedyIndexError(f"Non-JSON response for {path}", payload=raw) from exc
87
+ code = payload.get("code") if isinstance(payload, dict) else None
88
+ if code not in (0, None):
89
+ messages = {1: "insufficient balance", 2: "validation error"}
90
+ raise SpeedyIndexError(
91
+ f"API error {code}: {messages.get(code, 'see payload')}", code=code, payload=payload
92
+ )
93
+ return payload
94
+
95
+ # -- account ---------------------------------------------------------
96
+ def balance(self) -> Dict[str, Any]:
97
+ """GET /v2/account - current balance."""
98
+ return self._request("GET", "/v2/account")["balance"]
99
+
100
+ # -- tasks -----------------------------------------------------------
101
+ def create_task(
102
+ self,
103
+ urls: Iterable[str],
104
+ engine: str = "google",
105
+ task_type: str = "checker",
106
+ title: Optional[str] = None,
107
+ pay_per_indexed: Optional[bool] = None,
108
+ drip_feed: bool = False,
109
+ drip_feed_days: Optional[int] = None,
110
+ ) -> Dict[str, Any]:
111
+ """POST /v2/task/<engine>/<type>/create.
112
+
113
+ For ``google``/``indexer`` the API accepts only ``pay_per_indexed=True``,
114
+ so it is set automatically. Up to 10,000 URLs per request.
115
+ """
116
+ _check_engine(engine, task_type)
117
+ url_list = _clean_urls(urls)
118
+ if not url_list:
119
+ raise ValueError("urls is empty")
120
+ if len(url_list) > MAX_URLS_PER_TASK:
121
+ raise ValueError(f"max {MAX_URLS_PER_TASK} URLs per task, got {len(url_list)}")
122
+ body: Dict[str, Any] = {"urls": url_list}
123
+ if title:
124
+ body["title"] = title
125
+ if engine == "google" and task_type == "indexer":
126
+ pay_per_indexed = True
127
+ if pay_per_indexed is not None:
128
+ body["pay_per_indexed"] = pay_per_indexed
129
+ if drip_feed:
130
+ if not drip_feed_days or not 2 <= drip_feed_days <= 30:
131
+ raise ValueError("drip_feed_days must be between 2 and 30")
132
+ if len(url_list) < drip_feed_days:
133
+ raise ValueError("number of URLs must be >= drip_feed_days")
134
+ body["drip_feed"] = True
135
+ body["drip_feed_days"] = drip_feed_days
136
+ return self._request("POST", f"/v2/task/{engine}/{task_type}/create", body)
137
+
138
+ def list_tasks(self, engine: str = "google", page: int = 0) -> Dict[str, Any]:
139
+ """GET /v2/task/<engine>/list/<page> - 1000 tasks per page, newest first."""
140
+ _check_engine(engine)
141
+ return self._request("GET", f"/v2/task/{engine}/list/{int(page)}")
142
+
143
+ def task_status(self, task_ids: Iterable[str], engine: str = "google", task_type: str = "checker") -> Dict[str, Any]:
144
+ """POST /v2/task/<engine>/<type>/status - up to 1000 task IDs."""
145
+ _check_engine(engine, task_type)
146
+ ids = list(task_ids)
147
+ if len(ids) > MAX_TASK_IDS:
148
+ raise ValueError(f"max {MAX_TASK_IDS} task IDs per request")
149
+ return self._request("POST", f"/v2/task/{engine}/{task_type}/status", {"task_ids": ids})
150
+
151
+ def full_report(self, task_id: str, engine: str = "google", task_type: str = "checker") -> Dict[str, Any]:
152
+ """POST /v2/task/<engine>/<type>/fullreport - indexed and unindexed links."""
153
+ _check_engine(engine, task_type)
154
+ return self._request("POST", f"/v2/task/{engine}/{task_type}/fullreport", {"task_id": task_id})
155
+
156
+ # -- single link -----------------------------------------------------
157
+ def index_url(self, url: str, engine: str = "google") -> Dict[str, Any]:
158
+ """POST /v2/<engine>/url - submit one link (google or yandex)."""
159
+ if engine not in ("google", "yandex"):
160
+ raise ValueError("single-link submission supports google and yandex")
161
+ return self._request("POST", f"/v2/{engine}/url", {"url": url})
162
+
163
+ # -- backlink checker ------------------------------------------------
164
+ def create_backlink_check(self, urls: Iterable[str], target_domains: Iterable[str]) -> Dict[str, Any]:
165
+ """POST /v2/task/backlink/checker/create - look for links to your domains on donor pages."""
166
+ url_list = _clean_urls(urls)
167
+ if not url_list:
168
+ raise ValueError("urls is empty")
169
+ if len(url_list) > MAX_URLS_PER_TASK:
170
+ raise ValueError(f"max {MAX_URLS_PER_TASK} URLs per task")
171
+ return self._request(
172
+ "POST",
173
+ "/v2/task/backlink/checker/create",
174
+ {"urls": url_list, "target_domains": list(target_domains)},
175
+ )
176
+
177
+ def backlink_report(self, task_id: str, page: int = 0, limit: int = 50) -> Dict[str, Any]:
178
+ """POST /v2/task/backlink/checker/report - per-donor details."""
179
+ return self._request(
180
+ "POST",
181
+ "/v2/task/backlink/checker/report",
182
+ {"task_id": task_id, "page": page, "limit": limit},
183
+ )
@@ -0,0 +1,78 @@
1
+ import json
2
+ import unittest
3
+ from unittest import mock
4
+
5
+ from speedyindex import SpeedyIndex, SpeedyIndexError
6
+ from speedyindex.cli import main
7
+
8
+
9
+ class FakeResp:
10
+ def __init__(self, payload):
11
+ self._b = json.dumps(payload).encode()
12
+
13
+ def read(self):
14
+ return self._b
15
+
16
+ def __enter__(self):
17
+ return self
18
+
19
+ def __exit__(self, *a):
20
+ return False
21
+
22
+
23
+ class ClientTests(unittest.TestCase):
24
+ def setUp(self):
25
+ self.si = SpeedyIndex(api_key="k")
26
+
27
+ def _capture(self, payload):
28
+ calls = []
29
+
30
+ def fake(req, timeout=None):
31
+ calls.append(req)
32
+ return FakeResp(payload)
33
+
34
+ return calls, mock.patch("urllib.request.urlopen", fake)
35
+
36
+ def test_balance_and_auth_header(self):
37
+ calls, p = self._capture({"code": 0, "balance": {"tokens": 1}})
38
+ with p:
39
+ self.assertEqual(self.si.balance(), {"tokens": 1})
40
+ self.assertEqual(calls[0].get_header("Authorization"), "k")
41
+ self.assertTrue(calls[0].full_url.endswith("/v2/account"))
42
+
43
+ def test_google_indexer_forces_pay_per_indexed_and_dedupes(self):
44
+ calls, p = self._capture({"code": 0, "task_id": "x"})
45
+ with p:
46
+ self.si.create_task(["https://a/", "https://a/", "#c", "https://b/"], task_type="indexer")
47
+ body = json.loads(calls[0].data)
48
+ self.assertEqual(body["urls"], ["https://a/", "https://b/"])
49
+ self.assertTrue(body["pay_per_indexed"])
50
+ self.assertTrue(calls[0].full_url.endswith("/v2/task/google/indexer/create"))
51
+
52
+ def test_validation(self):
53
+ with self.assertRaises(ValueError):
54
+ self.si.create_task(["https://a/"], engine="bing", task_type="indexer")
55
+ with self.assertRaises(ValueError):
56
+ self.si.create_task(["https://a/"], drip_feed=True, drip_feed_days=2)
57
+ with self.assertRaises(ValueError):
58
+ self.si.create_task([])
59
+
60
+ def test_api_error(self):
61
+ _, p = self._capture({"code": 1})
62
+ with p, self.assertRaises(SpeedyIndexError) as ctx:
63
+ self.si.balance()
64
+ self.assertEqual(ctx.exception.code, 1)
65
+
66
+ def test_missing_key(self):
67
+ with mock.patch.dict("os.environ", {}, clear=True):
68
+ with self.assertRaises(SpeedyIndexError):
69
+ SpeedyIndex()
70
+
71
+ def test_cli_balance(self):
72
+ _, p = self._capture({"code": 0, "balance": {"tokens": 5}})
73
+ with p, mock.patch.dict("os.environ", {"SPEEDYINDEX_API_KEY": "k"}):
74
+ self.assertEqual(main(["balance"]), 0)
75
+
76
+
77
+ if __name__ == "__main__":
78
+ unittest.main()