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.
- speedyindex-0.1.0/.github/workflows/publish.yml +35 -0
- speedyindex-0.1.0/.gitignore +4 -0
- speedyindex-0.1.0/LICENSE +21 -0
- speedyindex-0.1.0/PKG-INFO +123 -0
- speedyindex-0.1.0/README.md +103 -0
- speedyindex-0.1.0/pyproject.toml +33 -0
- speedyindex-0.1.0/src/speedyindex/__init__.py +6 -0
- speedyindex-0.1.0/src/speedyindex/cli.py +86 -0
- speedyindex-0.1.0/src/speedyindex/client.py +183 -0
- speedyindex-0.1.0/tests/test_client.py +78 -0
|
@@ -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,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,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()
|