seoscoreapi 1.4.0__tar.gz → 1.5.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.
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/PKG-INFO +21 -11
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/README.md +20 -10
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/pyproject.toml +1 -1
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/seoscoreapi/__init__.py +94 -42
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/seoscoreapi.egg-info/PKG-INFO +21 -11
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/seoscoreapi.egg-info/SOURCES.txt +2 -1
- seoscoreapi-1.5.0/tests/test_deep_audit.py +114 -0
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/seoscoreapi.egg-info/dependency_links.txt +0 -0
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/seoscoreapi.egg-info/requires.txt +0 -0
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/seoscoreapi.egg-info/top_level.txt +0 -0
- {seoscoreapi-1.4.0 → seoscoreapi-1.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seoscoreapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Python client for SEO Score API — audit any URL for SEO issues with one function call
|
|
5
5
|
Author-email: SEO Score API <info@seoscoreapi.com>
|
|
6
6
|
License: MIT
|
|
@@ -83,10 +83,11 @@ add_monitor(
|
|
|
83
83
|
|
|
84
84
|
Slack incoming-webhook URLs are auto-formatted as Block Kit messages; any other https endpoint receives the raw event JSON.
|
|
85
85
|
|
|
86
|
-
## Deep
|
|
86
|
+
## Deep Site Audit (Pro/Ultra, or credits)
|
|
87
87
|
|
|
88
|
-
A deep, AI-assisted audit scoring a URL across 9 dimensions (
|
|
89
|
-
|
|
88
|
+
A deep, AI-assisted audit scoring a URL across 9 dimensions (thousands of catalog
|
|
89
|
+
checks plus up to 150 AI checks). It runs asynchronously, so start a job and poll it,
|
|
90
|
+
or use `deep_audit` to block for the result:
|
|
90
91
|
|
|
91
92
|
```python
|
|
92
93
|
import seoscoreapi as seo
|
|
@@ -100,17 +101,26 @@ result = seo.deep_audit(
|
|
|
100
101
|
print(result["scores"]["lai_score"], result["scores"]["section_scores"])
|
|
101
102
|
|
|
102
103
|
# Or drive the job yourself:
|
|
103
|
-
job = seo.site_audit("https://yoursite.com", API_KEY)
|
|
104
|
-
status = seo.get_site_audit(job["job_id"], API_KEY)
|
|
104
|
+
job = seo.site_audit("https://yoursite.com", API_KEY) # POST /site-audit
|
|
105
|
+
status = seo.get_site_audit(job["job_id"], API_KEY) # GET /site-audit/{job_id}
|
|
105
106
|
# status["status"] -> queued | running | completed | failed
|
|
106
|
-
# queued
|
|
107
|
-
#
|
|
107
|
+
# queued -> {"queue_position", "eta_seconds"}
|
|
108
|
+
# completed -> {"result"}
|
|
109
|
+
result = seo.wait_for_site_audit(job["job_id"], API_KEY, timeout=600)
|
|
108
110
|
|
|
109
|
-
seo.
|
|
111
|
+
seo.deep_audit_usage(API_KEY) # GET /deep-audit/usage -> {"site_audit": {"used", "remaining"}, ...}
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
Deep audits
|
|
113
|
-
|
|
114
|
+
Deep audits are included on **Pro** ($39/mo, 20/mo) and **Ultra** ($99/mo, 100/mo);
|
|
115
|
+
any other key can run them on purchased credits.
|
|
116
|
+
|
|
117
|
+
Since 1.5.0 the SDK calls the main host, `https://seoscoreapi.com`, like every other
|
|
118
|
+
endpoint. To point Deep Audit somewhere else (a proxy, staging, or the legacy
|
|
119
|
+
`engine.seoscoreapi.com` host, which still works), pass `base_url=` to any Deep Audit
|
|
120
|
+
function, assign `seoscoreapi.DEEP_AUDIT_URL`, or set `SEOSCORE_DEEP_AUDIT_URL`.
|
|
121
|
+
`engine_usage()` is kept as an alias of `deep_audit_usage()`.
|
|
122
|
+
|
|
123
|
+
Docs: [seoscoreapi.com/docs](https://seoscoreapi.com/docs) (Deep Site Audit section).
|
|
114
124
|
|
|
115
125
|
## Full Documentation
|
|
116
126
|
|
|
@@ -65,10 +65,11 @@ add_monitor(
|
|
|
65
65
|
|
|
66
66
|
Slack incoming-webhook URLs are auto-formatted as Block Kit messages; any other https endpoint receives the raw event JSON.
|
|
67
67
|
|
|
68
|
-
## Deep
|
|
68
|
+
## Deep Site Audit (Pro/Ultra, or credits)
|
|
69
69
|
|
|
70
|
-
A deep, AI-assisted audit scoring a URL across 9 dimensions (
|
|
71
|
-
|
|
70
|
+
A deep, AI-assisted audit scoring a URL across 9 dimensions (thousands of catalog
|
|
71
|
+
checks plus up to 150 AI checks). It runs asynchronously, so start a job and poll it,
|
|
72
|
+
or use `deep_audit` to block for the result:
|
|
72
73
|
|
|
73
74
|
```python
|
|
74
75
|
import seoscoreapi as seo
|
|
@@ -82,17 +83,26 @@ result = seo.deep_audit(
|
|
|
82
83
|
print(result["scores"]["lai_score"], result["scores"]["section_scores"])
|
|
83
84
|
|
|
84
85
|
# Or drive the job yourself:
|
|
85
|
-
job = seo.site_audit("https://yoursite.com", API_KEY)
|
|
86
|
-
status = seo.get_site_audit(job["job_id"], API_KEY)
|
|
86
|
+
job = seo.site_audit("https://yoursite.com", API_KEY) # POST /site-audit
|
|
87
|
+
status = seo.get_site_audit(job["job_id"], API_KEY) # GET /site-audit/{job_id}
|
|
87
88
|
# status["status"] -> queued | running | completed | failed
|
|
88
|
-
# queued
|
|
89
|
-
#
|
|
89
|
+
# queued -> {"queue_position", "eta_seconds"}
|
|
90
|
+
# completed -> {"result"}
|
|
91
|
+
result = seo.wait_for_site_audit(job["job_id"], API_KEY, timeout=600)
|
|
90
92
|
|
|
91
|
-
seo.
|
|
93
|
+
seo.deep_audit_usage(API_KEY) # GET /deep-audit/usage -> {"site_audit": {"used", "remaining"}, ...}
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
Deep audits
|
|
95
|
-
|
|
96
|
+
Deep audits are included on **Pro** ($39/mo, 20/mo) and **Ultra** ($99/mo, 100/mo);
|
|
97
|
+
any other key can run them on purchased credits.
|
|
98
|
+
|
|
99
|
+
Since 1.5.0 the SDK calls the main host, `https://seoscoreapi.com`, like every other
|
|
100
|
+
endpoint. To point Deep Audit somewhere else (a proxy, staging, or the legacy
|
|
101
|
+
`engine.seoscoreapi.com` host, which still works), pass `base_url=` to any Deep Audit
|
|
102
|
+
function, assign `seoscoreapi.DEEP_AUDIT_URL`, or set `SEOSCORE_DEEP_AUDIT_URL`.
|
|
103
|
+
`engine_usage()` is kept as an alias of `deep_audit_usage()`.
|
|
104
|
+
|
|
105
|
+
Docs: [seoscoreapi.com/docs](https://seoscoreapi.com/docs) (Deep Site Audit section).
|
|
96
106
|
|
|
97
107
|
## Full Documentation
|
|
98
108
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seoscoreapi"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.0"
|
|
8
8
|
description = "Python client for SEO Score API — audit any URL for SEO issues with one function call"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -19,14 +19,22 @@ Full docs: https://seoscoreapi.com/docs
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations # lazy annotations so `list[str]` works on Python 3.8
|
|
21
21
|
|
|
22
|
+
import os
|
|
22
23
|
import time
|
|
23
24
|
from typing import Callable, Optional
|
|
24
25
|
|
|
25
26
|
import requests
|
|
26
27
|
|
|
27
28
|
BASE_URL = "https://seoscoreapi.com"
|
|
29
|
+
# Deep Site Audit is served on the main host (POST /site-audit,
|
|
30
|
+
# GET /site-audit/{job_id}, GET /deep-audit/usage). Override per call with
|
|
31
|
+
# ``base_url=``, globally by assigning ``seoscoreapi.DEEP_AUDIT_URL``, or with the
|
|
32
|
+
# ``SEOSCORE_DEEP_AUDIT_URL`` environment variable.
|
|
33
|
+
DEEP_AUDIT_URL = os.environ.get("SEOSCORE_DEEP_AUDIT_URL") or BASE_URL
|
|
34
|
+
# Legacy dedicated engine host. Still serves the same endpoints (its quota
|
|
35
|
+
# endpoint is plain ``/usage``); pass it as ``base_url`` if you need it.
|
|
28
36
|
ENGINE_URL = "https://engine.seoscoreapi.com"
|
|
29
|
-
__version__ = "1.
|
|
37
|
+
__version__ = "1.5.0"
|
|
30
38
|
|
|
31
39
|
_HEADERS = {"User-Agent": f"seoscoreapi-python/{__version__}"}
|
|
32
40
|
_TIMEOUT = 30 # seconds — default per-request timeout for engine calls
|
|
@@ -137,31 +145,47 @@ def history_domains(api_key: str) -> list:
|
|
|
137
145
|
return r.json()["domains"]
|
|
138
146
|
|
|
139
147
|
|
|
140
|
-
# --- Deep
|
|
148
|
+
# --- Deep Site Audit (Pro/Ultra, or credits) ---------------------------------
|
|
141
149
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
``{"job_id", "status", "poll"}``; poll it with :func:`get_site_audit`.
|
|
150
|
+
def _deep_base(base_url: Optional[str]) -> str:
|
|
151
|
+
return (base_url or DEEP_AUDIT_URL).rstrip("/")
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
|
|
154
|
+
def _usage_path(base: str) -> str:
|
|
155
|
+
# The main host serves the engine's quota at /deep-audit/usage (its own
|
|
156
|
+
# /usage is the per-URL audit allowance); the legacy engine host uses /usage.
|
|
157
|
+
return "/usage" if base.split("://", 1)[-1].startswith("engine.") else "/deep-audit/usage"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _start(url: str, api_key: str, base: str, options: dict) -> requests.Response:
|
|
161
|
+
return requests.post(
|
|
162
|
+
f"{base}/site-audit",
|
|
151
163
|
json={"url": url, **options},
|
|
152
164
|
headers={"X-API-Key": api_key, **_HEADERS},
|
|
153
165
|
timeout=_TIMEOUT,
|
|
154
166
|
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def site_audit(url: str, api_key: str, *, base_url: Optional[str] = None, **options) -> dict:
|
|
170
|
+
"""Start a Deep Site Audit. Asynchronous: returns a job
|
|
171
|
+
``{"job_id", "status", "poll"}``; poll it with :func:`get_site_audit` or
|
|
172
|
+
block on it with :func:`wait_for_site_audit`.
|
|
173
|
+
|
|
174
|
+
Optional keyword args are sent to the API as-is: ``business_type``
|
|
175
|
+
(saas | local_service | ecommerce | storefront | blog | publisher),
|
|
176
|
+
``is_local``, ``webhook_url``.
|
|
177
|
+
"""
|
|
178
|
+
r = _start(url, api_key, _deep_base(base_url), options)
|
|
155
179
|
r.raise_for_status()
|
|
156
180
|
return r.json()
|
|
157
181
|
|
|
158
182
|
|
|
159
|
-
def get_site_audit(job_id: str, api_key: str) -> dict:
|
|
160
|
-
"""Poll a
|
|
183
|
+
def get_site_audit(job_id: str, api_key: str, *, base_url: Optional[str] = None) -> dict:
|
|
184
|
+
"""Poll a Deep Site Audit job. Returns ``status`` plus ``queue_position``/
|
|
161
185
|
``eta_seconds`` while queued, ``progress``/``stage`` while running, and
|
|
162
186
|
``result`` when completed (or ``error`` when failed)."""
|
|
163
187
|
r = requests.get(
|
|
164
|
-
f"{
|
|
188
|
+
f"{_deep_base(base_url)}/site-audit/{job_id}",
|
|
165
189
|
headers={"X-API-Key": api_key, **_HEADERS},
|
|
166
190
|
timeout=_TIMEOUT,
|
|
167
191
|
)
|
|
@@ -169,6 +193,38 @@ def get_site_audit(job_id: str, api_key: str) -> dict:
|
|
|
169
193
|
return r.json()
|
|
170
194
|
|
|
171
195
|
|
|
196
|
+
def wait_for_site_audit(
|
|
197
|
+
job_id: str,
|
|
198
|
+
api_key: str,
|
|
199
|
+
*,
|
|
200
|
+
poll_interval: float = 5.0,
|
|
201
|
+
timeout: float = 600.0,
|
|
202
|
+
on_progress: Optional[Callable[[dict], None]] = None,
|
|
203
|
+
base_url: Optional[str] = None,
|
|
204
|
+
) -> dict:
|
|
205
|
+
"""Poll an existing Deep Site Audit job until it finishes; return its result.
|
|
206
|
+
|
|
207
|
+
Honors the server's ETA while queued. Raises ``TimeoutError`` if it isn't
|
|
208
|
+
done within ``timeout`` seconds, or ``RuntimeError`` if the audit fails.
|
|
209
|
+
"""
|
|
210
|
+
deadline = time.monotonic() + timeout
|
|
211
|
+
while True:
|
|
212
|
+
if time.monotonic() > deadline:
|
|
213
|
+
raise TimeoutError("Timed out waiting for audit to complete")
|
|
214
|
+
status = get_site_audit(job_id, api_key, base_url=base_url)
|
|
215
|
+
if on_progress:
|
|
216
|
+
on_progress(status)
|
|
217
|
+
state = status.get("status")
|
|
218
|
+
if state == "completed":
|
|
219
|
+
return status["result"]
|
|
220
|
+
if state == "failed":
|
|
221
|
+
raise RuntimeError(status.get("error", "Audit failed"))
|
|
222
|
+
if state == "queued" and status.get("eta_seconds"):
|
|
223
|
+
time.sleep(min(float(status["eta_seconds"]), 15.0))
|
|
224
|
+
else:
|
|
225
|
+
time.sleep(poll_interval)
|
|
226
|
+
|
|
227
|
+
|
|
172
228
|
def deep_audit(
|
|
173
229
|
url: str,
|
|
174
230
|
api_key: str,
|
|
@@ -176,22 +232,19 @@ def deep_audit(
|
|
|
176
232
|
poll_interval: float = 5.0,
|
|
177
233
|
timeout: float = 600.0,
|
|
178
234
|
on_progress: Optional[Callable[[dict], None]] = None,
|
|
235
|
+
base_url: Optional[str] = None,
|
|
179
236
|
**options,
|
|
180
237
|
) -> dict:
|
|
181
|
-
"""Start a
|
|
238
|
+
"""Start a Deep Site Audit and block until it finishes; return the result dict.
|
|
182
239
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
``timeout`` seconds, or ``RuntimeError`` if
|
|
240
|
+
Retries the submit on queue backpressure (429 + ``Retry-After``), then
|
|
241
|
+
polls with :func:`wait_for_site_audit`. Raises ``TimeoutError`` if it
|
|
242
|
+
doesn't finish within ``timeout`` seconds, or ``RuntimeError`` if it fails.
|
|
186
243
|
"""
|
|
244
|
+
base = _deep_base(base_url)
|
|
187
245
|
deadline = time.monotonic() + timeout
|
|
188
246
|
while True: # submit, retrying on backpressure
|
|
189
|
-
r =
|
|
190
|
-
f"{ENGINE_URL}/site-audit",
|
|
191
|
-
json={"url": url, **options},
|
|
192
|
-
headers={"X-API-Key": api_key, **_HEADERS},
|
|
193
|
-
timeout=_TIMEOUT,
|
|
194
|
-
)
|
|
247
|
+
r = _start(url, api_key, base, options)
|
|
195
248
|
if r.status_code == 429:
|
|
196
249
|
retry = float(r.headers.get("Retry-After", 5))
|
|
197
250
|
if time.monotonic() + retry > deadline:
|
|
@@ -201,30 +254,29 @@ def deep_audit(
|
|
|
201
254
|
r.raise_for_status()
|
|
202
255
|
job = r.json()
|
|
203
256
|
break
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if state == "completed":
|
|
213
|
-
return status["result"]
|
|
214
|
-
if state == "failed":
|
|
215
|
-
raise RuntimeError(status.get("error", "Audit failed"))
|
|
216
|
-
if state == "queued" and status.get("eta_seconds"):
|
|
217
|
-
time.sleep(min(status["eta_seconds"], 15.0))
|
|
218
|
-
else:
|
|
219
|
-
time.sleep(poll_interval)
|
|
257
|
+
return wait_for_site_audit(
|
|
258
|
+
job["job_id"],
|
|
259
|
+
api_key,
|
|
260
|
+
poll_interval=poll_interval,
|
|
261
|
+
timeout=max(deadline - time.monotonic(), 0.0),
|
|
262
|
+
on_progress=on_progress,
|
|
263
|
+
base_url=base,
|
|
264
|
+
)
|
|
220
265
|
|
|
221
266
|
|
|
222
|
-
def
|
|
223
|
-
"""
|
|
267
|
+
def deep_audit_usage(api_key: str, *, base_url: Optional[str] = None) -> dict:
|
|
268
|
+
"""Deep Site Audits used/remaining this month for this key
|
|
269
|
+
(``{"tier", "site_audit": {"used", "remaining"}, ...}``)."""
|
|
270
|
+
base = _deep_base(base_url)
|
|
224
271
|
r = requests.get(
|
|
225
|
-
f"{
|
|
272
|
+
f"{base}{_usage_path(base)}",
|
|
226
273
|
headers={"X-API-Key": api_key, **_HEADERS},
|
|
227
274
|
timeout=_TIMEOUT,
|
|
228
275
|
)
|
|
229
276
|
r.raise_for_status()
|
|
230
277
|
return r.json()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def engine_usage(api_key: str, *, base_url: Optional[str] = None) -> dict:
|
|
281
|
+
"""Deprecated alias of :func:`deep_audit_usage` (kept for 1.4 callers)."""
|
|
282
|
+
return deep_audit_usage(api_key, base_url=base_url)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seoscoreapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Python client for SEO Score API — audit any URL for SEO issues with one function call
|
|
5
5
|
Author-email: SEO Score API <info@seoscoreapi.com>
|
|
6
6
|
License: MIT
|
|
@@ -83,10 +83,11 @@ add_monitor(
|
|
|
83
83
|
|
|
84
84
|
Slack incoming-webhook URLs are auto-formatted as Block Kit messages; any other https endpoint receives the raw event JSON.
|
|
85
85
|
|
|
86
|
-
## Deep
|
|
86
|
+
## Deep Site Audit (Pro/Ultra, or credits)
|
|
87
87
|
|
|
88
|
-
A deep, AI-assisted audit scoring a URL across 9 dimensions (
|
|
89
|
-
|
|
88
|
+
A deep, AI-assisted audit scoring a URL across 9 dimensions (thousands of catalog
|
|
89
|
+
checks plus up to 150 AI checks). It runs asynchronously, so start a job and poll it,
|
|
90
|
+
or use `deep_audit` to block for the result:
|
|
90
91
|
|
|
91
92
|
```python
|
|
92
93
|
import seoscoreapi as seo
|
|
@@ -100,17 +101,26 @@ result = seo.deep_audit(
|
|
|
100
101
|
print(result["scores"]["lai_score"], result["scores"]["section_scores"])
|
|
101
102
|
|
|
102
103
|
# Or drive the job yourself:
|
|
103
|
-
job = seo.site_audit("https://yoursite.com", API_KEY)
|
|
104
|
-
status = seo.get_site_audit(job["job_id"], API_KEY)
|
|
104
|
+
job = seo.site_audit("https://yoursite.com", API_KEY) # POST /site-audit
|
|
105
|
+
status = seo.get_site_audit(job["job_id"], API_KEY) # GET /site-audit/{job_id}
|
|
105
106
|
# status["status"] -> queued | running | completed | failed
|
|
106
|
-
# queued
|
|
107
|
-
#
|
|
107
|
+
# queued -> {"queue_position", "eta_seconds"}
|
|
108
|
+
# completed -> {"result"}
|
|
109
|
+
result = seo.wait_for_site_audit(job["job_id"], API_KEY, timeout=600)
|
|
108
110
|
|
|
109
|
-
seo.
|
|
111
|
+
seo.deep_audit_usage(API_KEY) # GET /deep-audit/usage -> {"site_audit": {"used", "remaining"}, ...}
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
Deep audits
|
|
113
|
-
|
|
114
|
+
Deep audits are included on **Pro** ($39/mo, 20/mo) and **Ultra** ($99/mo, 100/mo);
|
|
115
|
+
any other key can run them on purchased credits.
|
|
116
|
+
|
|
117
|
+
Since 1.5.0 the SDK calls the main host, `https://seoscoreapi.com`, like every other
|
|
118
|
+
endpoint. To point Deep Audit somewhere else (a proxy, staging, or the legacy
|
|
119
|
+
`engine.seoscoreapi.com` host, which still works), pass `base_url=` to any Deep Audit
|
|
120
|
+
function, assign `seoscoreapi.DEEP_AUDIT_URL`, or set `SEOSCORE_DEEP_AUDIT_URL`.
|
|
121
|
+
`engine_usage()` is kept as an alias of `deep_audit_usage()`.
|
|
122
|
+
|
|
123
|
+
Docs: [seoscoreapi.com/docs](https://seoscoreapi.com/docs) (Deep Site Audit section).
|
|
114
124
|
|
|
115
125
|
## Full Documentation
|
|
116
126
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Deep Site Audit client tests. No network: requests.get/post are patched."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest import mock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
10
|
+
|
|
11
|
+
import seoscoreapi as seo # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeResponse:
|
|
15
|
+
def __init__(self, payload=None, status=200, headers=None):
|
|
16
|
+
self._payload = payload or {}
|
|
17
|
+
self.status_code = status
|
|
18
|
+
self.headers = headers or {}
|
|
19
|
+
|
|
20
|
+
def json(self):
|
|
21
|
+
return self._payload
|
|
22
|
+
|
|
23
|
+
def raise_for_status(self):
|
|
24
|
+
if self.status_code >= 400:
|
|
25
|
+
raise seo.requests.HTTPError(f"HTTP {self.status_code}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(autouse=True)
|
|
29
|
+
def no_sleep(monkeypatch):
|
|
30
|
+
monkeypatch.setattr(seo.time, "sleep", lambda s: None)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_defaults_to_main_host():
|
|
34
|
+
assert seo.DEEP_AUDIT_URL == "https://seoscoreapi.com"
|
|
35
|
+
assert seo.__version__ == "1.5.0"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_site_audit_posts_to_main_host():
|
|
39
|
+
with mock.patch.object(seo.requests, "post", return_value=FakeResponse({"job_id": "abc", "status": "queued"})) as post:
|
|
40
|
+
job = seo.site_audit("https://example.com", "k", business_type="saas")
|
|
41
|
+
assert job["job_id"] == "abc"
|
|
42
|
+
args, kwargs = post.call_args
|
|
43
|
+
assert args[0] == "https://seoscoreapi.com/site-audit"
|
|
44
|
+
assert kwargs["json"] == {"url": "https://example.com", "business_type": "saas"}
|
|
45
|
+
assert kwargs["headers"]["X-API-Key"] == "k"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_get_site_audit_url():
|
|
49
|
+
with mock.patch.object(seo.requests, "get", return_value=FakeResponse({"status": "running"})) as get:
|
|
50
|
+
seo.get_site_audit("abc", "k")
|
|
51
|
+
assert get.call_args[0][0] == "https://seoscoreapi.com/site-audit/abc"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_base_url_override_per_call():
|
|
55
|
+
with mock.patch.object(seo.requests, "post", return_value=FakeResponse({"job_id": "x"})) as post:
|
|
56
|
+
seo.site_audit("https://example.com", "k", base_url="http://localhost:9000/")
|
|
57
|
+
assert post.call_args[0][0] == "http://localhost:9000/site-audit"
|
|
58
|
+
assert "base_url" not in post.call_args[1]["json"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_module_override(monkeypatch):
|
|
62
|
+
monkeypatch.setattr(seo, "DEEP_AUDIT_URL", "https://staging.example")
|
|
63
|
+
with mock.patch.object(seo.requests, "get", return_value=FakeResponse({})) as get:
|
|
64
|
+
seo.get_site_audit("abc", "k")
|
|
65
|
+
assert get.call_args[0][0] == "https://staging.example/site-audit/abc"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_usage_main_host_path():
|
|
69
|
+
with mock.patch.object(seo.requests, "get", return_value=FakeResponse({"tier": "pro"})) as get:
|
|
70
|
+
assert seo.deep_audit_usage("k") == {"tier": "pro"}
|
|
71
|
+
assert get.call_args[0][0] == "https://seoscoreapi.com/deep-audit/usage"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_usage_legacy_engine_path_and_alias():
|
|
75
|
+
with mock.patch.object(seo.requests, "get", return_value=FakeResponse({})) as get:
|
|
76
|
+
seo.engine_usage("k", base_url=seo.ENGINE_URL)
|
|
77
|
+
assert get.call_args[0][0] == "https://engine.seoscoreapi.com/usage"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_wait_for_site_audit_completes():
|
|
81
|
+
seq = [
|
|
82
|
+
FakeResponse({"status": "queued", "eta_seconds": 30}),
|
|
83
|
+
FakeResponse({"status": "running", "progress": 50}),
|
|
84
|
+
FakeResponse({"status": "completed", "result": {"scores": {"lai_score": 3.2}}}),
|
|
85
|
+
]
|
|
86
|
+
seen = []
|
|
87
|
+
with mock.patch.object(seo.requests, "get", side_effect=seq):
|
|
88
|
+
result = seo.wait_for_site_audit("abc", "k", on_progress=lambda s: seen.append(s["status"]))
|
|
89
|
+
assert result == {"scores": {"lai_score": 3.2}}
|
|
90
|
+
assert seen == ["queued", "running", "completed"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_wait_for_site_audit_failed():
|
|
94
|
+
with mock.patch.object(seo.requests, "get", return_value=FakeResponse({"status": "failed", "error": "boom"})):
|
|
95
|
+
with pytest.raises(RuntimeError, match="boom"):
|
|
96
|
+
seo.wait_for_site_audit("abc", "k")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_wait_for_site_audit_timeout():
|
|
100
|
+
with mock.patch.object(seo.requests, "get", return_value=FakeResponse({"status": "running"})):
|
|
101
|
+
with pytest.raises(TimeoutError):
|
|
102
|
+
seo.wait_for_site_audit("abc", "k", timeout=0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_deep_audit_retries_backpressure_then_polls():
|
|
106
|
+
posts = [
|
|
107
|
+
FakeResponse(status=429, headers={"Retry-After": "1"}),
|
|
108
|
+
FakeResponse({"job_id": "abc", "status": "queued"}),
|
|
109
|
+
]
|
|
110
|
+
with mock.patch.object(seo.requests, "post", side_effect=posts) as post, \
|
|
111
|
+
mock.patch.object(seo.requests, "get", return_value=FakeResponse({"status": "completed", "result": {"ok": 1}})) as get:
|
|
112
|
+
assert seo.deep_audit("https://example.com", "k") == {"ok": 1}
|
|
113
|
+
assert post.call_count == 2
|
|
114
|
+
assert get.call_args[0][0] == "https://seoscoreapi.com/site-audit/abc"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|