seoscoreapi 1.6.0__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/PKG-INFO +55 -7
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/README.md +54 -6
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/pyproject.toml +1 -1
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/seoscoreapi/__init__.py +55 -12
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/seoscoreapi.egg-info/PKG-INFO +55 -7
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/tests/test_deep_audit.py +1 -1
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/tests/test_endpoints.py +76 -0
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/seoscoreapi.egg-info/SOURCES.txt +0 -0
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/seoscoreapi.egg-info/dependency_links.txt +0 -0
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/seoscoreapi.egg-info/requires.txt +0 -0
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.0}/seoscoreapi.egg-info/top_level.txt +0 -0
- {seoscoreapi-1.6.0 → seoscoreapi-1.7.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.7.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
|
|
@@ -29,10 +29,11 @@ pip install seoscoreapi
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
|
-
|
|
32
|
+
import os
|
|
33
|
+
from seoscoreapi import audit
|
|
33
34
|
|
|
34
|
-
# Get a free API key (2 audits/day, no credit card)
|
|
35
|
-
key =
|
|
35
|
+
# Get a free API key (2 audits/day, no credit card) at https://seoscoreapi.com/#signup
|
|
36
|
+
key = os.environ["SEO_SCORE_API_KEY"]
|
|
36
37
|
|
|
37
38
|
# Run an audit
|
|
38
39
|
result = audit("https://example.com", api_key=key)
|
|
@@ -43,9 +44,10 @@ print(f"Score: {result['score']}/100 ({result['grade']})")
|
|
|
43
44
|
|
|
44
45
|
| Function | Description |
|
|
45
46
|
|---|---|
|
|
46
|
-
| `signup(email)` |
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
47
|
+
| `signup(email)` | Start signup: the API emails a 6-digit code. Returns the API's response (`{"status": "verification_required", ...}`), not a key |
|
|
48
|
+
| `verify(email, code)` | Finish signup with the emailed code. Returns the API key (shown only once) |
|
|
49
|
+
| `audit(url, api_key, cwv=False)` | Run SEO audit on a URL. `cwv=True` adds measured Core Web Vitals (slower, see below) |
|
|
50
|
+
| `batch_audit(urls, api_key, cwv=False)` | Audit up to 10 URLs in one call (paid). `cwv=True` adds Core Web Vitals to each |
|
|
49
51
|
| `compare(urls, api_key)` | Compare 2–5 URLs with a structured diff (Basic+) |
|
|
50
52
|
| `competitive_audit(url, competitor_url, keyword, api_key)` | Head-to-head audit with gap score (Pro+) |
|
|
51
53
|
| `history(url, api_key, limit=100, since=None)` | Full audit timeseries + summary for a URL (Starter+) |
|
|
@@ -84,6 +86,31 @@ print(f"Score: {result['score']}/100 ({result['grade']})")
|
|
|
84
86
|
Errors are `requests.HTTPError` (from `raise_for_status()`); the API's message is in
|
|
85
87
|
`err.response.json()["detail"]`.
|
|
86
88
|
|
|
89
|
+
## Get an API key from code
|
|
90
|
+
|
|
91
|
+
Signup takes two calls. `signup()` emails a 6-digit code (valid for 10 minutes) and
|
|
92
|
+
`verify()` trades that code for the key. You can also sign up at
|
|
93
|
+
[seoscoreapi.com](https://seoscoreapi.com/#signup).
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import requests
|
|
97
|
+
from seoscoreapi import signup, verify
|
|
98
|
+
|
|
99
|
+
email = "you@example.com"
|
|
100
|
+
print(signup(email)["message"]) # "Check your email for a 6-digit verification code."
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
key = verify(email, input("Code from the email: "))
|
|
104
|
+
except requests.HTTPError as e:
|
|
105
|
+
raise SystemExit(e.response.json()["detail"]) # wrong or expired code
|
|
106
|
+
|
|
107
|
+
print(key) # save it: the key is shown only once
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`signup()` raises `requests.HTTPError` when the address is refused (400) or already
|
|
111
|
+
has an active key (409). `verify()` raises it (400) on a wrong or expired code; five
|
|
112
|
+
wrong codes void the signup, so call `signup()` again.
|
|
113
|
+
|
|
87
114
|
## Historical tracking
|
|
88
115
|
|
|
89
116
|
Every audit on a paid plan returns a `history` block on the `/audit` response:
|
|
@@ -111,6 +138,27 @@ add_monitor(
|
|
|
111
138
|
|
|
112
139
|
Slack incoming-webhook URLs are auto-formatted as Block Kit messages; any other https endpoint receives the raw event JSON.
|
|
113
140
|
|
|
141
|
+
## Core Web Vitals (opt-in)
|
|
142
|
+
|
|
143
|
+
Audits are fast because they do not wait on Google PageSpeed Insights. Ask for measured
|
|
144
|
+
Core Web Vitals when you need them:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
result = audit("https://example.com", api_key=key, cwv=True)
|
|
148
|
+
vitals = result["core_web_vitals"]
|
|
149
|
+
if vitals["status"] == "ok":
|
|
150
|
+
print(vitals["lcp_ms"], vitals["inp_ms"], vitals["cls"], vitals["source"]) # source: "field" or "lab"
|
|
151
|
+
elif vitals.get("pending"):
|
|
152
|
+
print("Still measuring; audit this URL again in a minute")
|
|
153
|
+
else:
|
|
154
|
+
print("No Core Web Vitals:", vitals.get("reason"))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The first measurement of a URL adds about 10 seconds (the API waits up to 25); it is
|
|
158
|
+
cached for 24 hours, so repeats are instant. It works on every plan and never changes the
|
|
159
|
+
score. Without `cwv=True` the block reads `{"status": "not_requested", ...}` with the
|
|
160
|
+
metrics set to `None`.
|
|
161
|
+
|
|
114
162
|
## Accessibility audit and report files
|
|
115
163
|
|
|
116
164
|
```python
|
|
@@ -11,10 +11,11 @@ pip install seoscoreapi
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```python
|
|
14
|
-
|
|
14
|
+
import os
|
|
15
|
+
from seoscoreapi import audit
|
|
15
16
|
|
|
16
|
-
# Get a free API key (2 audits/day, no credit card)
|
|
17
|
-
key =
|
|
17
|
+
# Get a free API key (2 audits/day, no credit card) at https://seoscoreapi.com/#signup
|
|
18
|
+
key = os.environ["SEO_SCORE_API_KEY"]
|
|
18
19
|
|
|
19
20
|
# Run an audit
|
|
20
21
|
result = audit("https://example.com", api_key=key)
|
|
@@ -25,9 +26,10 @@ print(f"Score: {result['score']}/100 ({result['grade']})")
|
|
|
25
26
|
|
|
26
27
|
| Function | Description |
|
|
27
28
|
|---|---|
|
|
28
|
-
| `signup(email)` |
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
29
|
+
| `signup(email)` | Start signup: the API emails a 6-digit code. Returns the API's response (`{"status": "verification_required", ...}`), not a key |
|
|
30
|
+
| `verify(email, code)` | Finish signup with the emailed code. Returns the API key (shown only once) |
|
|
31
|
+
| `audit(url, api_key, cwv=False)` | Run SEO audit on a URL. `cwv=True` adds measured Core Web Vitals (slower, see below) |
|
|
32
|
+
| `batch_audit(urls, api_key, cwv=False)` | Audit up to 10 URLs in one call (paid). `cwv=True` adds Core Web Vitals to each |
|
|
31
33
|
| `compare(urls, api_key)` | Compare 2–5 URLs with a structured diff (Basic+) |
|
|
32
34
|
| `competitive_audit(url, competitor_url, keyword, api_key)` | Head-to-head audit with gap score (Pro+) |
|
|
33
35
|
| `history(url, api_key, limit=100, since=None)` | Full audit timeseries + summary for a URL (Starter+) |
|
|
@@ -66,6 +68,31 @@ print(f"Score: {result['score']}/100 ({result['grade']})")
|
|
|
66
68
|
Errors are `requests.HTTPError` (from `raise_for_status()`); the API's message is in
|
|
67
69
|
`err.response.json()["detail"]`.
|
|
68
70
|
|
|
71
|
+
## Get an API key from code
|
|
72
|
+
|
|
73
|
+
Signup takes two calls. `signup()` emails a 6-digit code (valid for 10 minutes) and
|
|
74
|
+
`verify()` trades that code for the key. You can also sign up at
|
|
75
|
+
[seoscoreapi.com](https://seoscoreapi.com/#signup).
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import requests
|
|
79
|
+
from seoscoreapi import signup, verify
|
|
80
|
+
|
|
81
|
+
email = "you@example.com"
|
|
82
|
+
print(signup(email)["message"]) # "Check your email for a 6-digit verification code."
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
key = verify(email, input("Code from the email: "))
|
|
86
|
+
except requests.HTTPError as e:
|
|
87
|
+
raise SystemExit(e.response.json()["detail"]) # wrong or expired code
|
|
88
|
+
|
|
89
|
+
print(key) # save it: the key is shown only once
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`signup()` raises `requests.HTTPError` when the address is refused (400) or already
|
|
93
|
+
has an active key (409). `verify()` raises it (400) on a wrong or expired code; five
|
|
94
|
+
wrong codes void the signup, so call `signup()` again.
|
|
95
|
+
|
|
69
96
|
## Historical tracking
|
|
70
97
|
|
|
71
98
|
Every audit on a paid plan returns a `history` block on the `/audit` response:
|
|
@@ -93,6 +120,27 @@ add_monitor(
|
|
|
93
120
|
|
|
94
121
|
Slack incoming-webhook URLs are auto-formatted as Block Kit messages; any other https endpoint receives the raw event JSON.
|
|
95
122
|
|
|
123
|
+
## Core Web Vitals (opt-in)
|
|
124
|
+
|
|
125
|
+
Audits are fast because they do not wait on Google PageSpeed Insights. Ask for measured
|
|
126
|
+
Core Web Vitals when you need them:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
result = audit("https://example.com", api_key=key, cwv=True)
|
|
130
|
+
vitals = result["core_web_vitals"]
|
|
131
|
+
if vitals["status"] == "ok":
|
|
132
|
+
print(vitals["lcp_ms"], vitals["inp_ms"], vitals["cls"], vitals["source"]) # source: "field" or "lab"
|
|
133
|
+
elif vitals.get("pending"):
|
|
134
|
+
print("Still measuring; audit this URL again in a minute")
|
|
135
|
+
else:
|
|
136
|
+
print("No Core Web Vitals:", vitals.get("reason"))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The first measurement of a URL adds about 10 seconds (the API waits up to 25); it is
|
|
140
|
+
cached for 24 hours, so repeats are instant. It works on every plan and never changes the
|
|
141
|
+
score. Without `cwv=True` the block reads `{"status": "not_requested", ...}` with the
|
|
142
|
+
metrics set to `None`.
|
|
143
|
+
|
|
96
144
|
## Accessibility audit and report files
|
|
97
145
|
|
|
98
146
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seoscoreapi"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.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"}
|
|
@@ -5,10 +5,11 @@ SEO Score API - Python Client
|
|
|
5
5
|
Audit any URL for SEO issues with one function call.
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
|
-
from seoscoreapi import audit, signup
|
|
8
|
+
from seoscoreapi import audit, signup, verify
|
|
9
9
|
|
|
10
|
-
# Get a free API key
|
|
11
|
-
|
|
10
|
+
# Get a free API key: signup() emails a 6-digit code, verify() returns the key
|
|
11
|
+
signup("you@example.com")
|
|
12
|
+
key = verify("you@example.com", input("Code from the email: "))
|
|
12
13
|
|
|
13
14
|
# Run an audit
|
|
14
15
|
result = audit("https://example.com", api_key=key)
|
|
@@ -34,29 +35,71 @@ DEEP_AUDIT_URL = os.environ.get("SEOSCORE_DEEP_AUDIT_URL") or BASE_URL
|
|
|
34
35
|
# Legacy dedicated engine host. Still serves the same endpoints (its quota
|
|
35
36
|
# endpoint is plain ``/usage``); pass it as ``base_url`` if you need it.
|
|
36
37
|
ENGINE_URL = "https://engine.seoscoreapi.com"
|
|
37
|
-
__version__ = "1.
|
|
38
|
+
__version__ = "1.7.0"
|
|
38
39
|
|
|
39
40
|
_HEADERS = {"User-Agent": f"seoscoreapi-python/{__version__}"}
|
|
40
41
|
_TIMEOUT = 30 # seconds — default per-request timeout for engine calls
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def signup(email: str) ->
|
|
44
|
-
"""
|
|
44
|
+
def signup(email: str) -> dict:
|
|
45
|
+
"""Start signup for a free API key. The API emails a 6-digit code to `email`.
|
|
46
|
+
|
|
47
|
+
Returns the API's response, normally
|
|
48
|
+
``{"status": "verification_required", "message": "..."}``. No key is issued
|
|
49
|
+
yet: pass the code to :func:`verify` to get it. The code expires after 10
|
|
50
|
+
minutes. Raises ``requests.HTTPError`` on a rejected address (400) or an
|
|
51
|
+
email that already has an active key (409).
|
|
52
|
+
"""
|
|
45
53
|
r = requests.post(f"{BASE_URL}/signup", json={"email": email}, headers=_HEADERS)
|
|
46
54
|
r.raise_for_status()
|
|
55
|
+
return r.json()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def verify(email: str, code: str) -> str:
|
|
59
|
+
"""Finish signup: exchange the emailed 6-digit code for the API key.
|
|
60
|
+
|
|
61
|
+
Returns the raw key (save it, it is shown only once). Raises
|
|
62
|
+
``requests.HTTPError`` (400) on a wrong or expired code, or when there is no
|
|
63
|
+
pending signup for `email`; the reason is in ``e.response.json()["detail"]``.
|
|
64
|
+
Five wrong codes void the signup, so call :func:`signup` again after that.
|
|
65
|
+
"""
|
|
66
|
+
r = requests.post(
|
|
67
|
+
f"{BASE_URL}/verify",
|
|
68
|
+
json={"email": email, "code": str(code).strip()},
|
|
69
|
+
headers=_HEADERS,
|
|
70
|
+
)
|
|
71
|
+
r.raise_for_status()
|
|
47
72
|
return r.json()["api_key"]
|
|
48
73
|
|
|
49
74
|
|
|
50
|
-
def audit(url: str, api_key: str) -> dict:
|
|
51
|
-
"""Run an SEO audit on a URL. Returns score, grade, checks, and priorities.
|
|
52
|
-
|
|
75
|
+
def audit(url: str, api_key: str, cwv: bool = False) -> dict:
|
|
76
|
+
"""Run an SEO audit on a URL. Returns score, grade, checks, and priorities.
|
|
77
|
+
|
|
78
|
+
``cwv=True`` adds measured Core Web Vitals (LCP, INP, CLS, FCP, TTFB from Google
|
|
79
|
+
PageSpeed Insights) under ``core_web_vitals``. It is off by default because the
|
|
80
|
+
first measurement of a URL adds about 10 seconds (it is then cached for 24
|
|
81
|
+
hours). Without it ``core_web_vitals["status"]`` is ``"not_requested"``; with
|
|
82
|
+
it, ``"ok"``, or ``"unavailable"`` with a ``reason`` (and ``pending: True`` when
|
|
83
|
+
the measurement is still running: audit the URL again to pick it up).
|
|
84
|
+
"""
|
|
85
|
+
params = {"url": url}
|
|
86
|
+
if cwv:
|
|
87
|
+
params["cwv"] = "true"
|
|
88
|
+
r = requests.get(f"{BASE_URL}/audit", params=params, headers={"X-API-Key": api_key, **_HEADERS})
|
|
53
89
|
r.raise_for_status()
|
|
54
90
|
return r.json()
|
|
55
91
|
|
|
56
92
|
|
|
57
|
-
def batch_audit(urls: list[str], api_key: str) -> dict:
|
|
58
|
-
"""Audit multiple URLs (paid plans only). Returns list of results.
|
|
59
|
-
|
|
93
|
+
def batch_audit(urls: list[str], api_key: str, cwv: bool = False) -> dict:
|
|
94
|
+
"""Audit multiple URLs (paid plans only). Returns list of results.
|
|
95
|
+
|
|
96
|
+
``cwv=True`` adds measured Core Web Vitals to every result (see ``audit``). The
|
|
97
|
+
batch then takes longer: allow up to three minutes.
|
|
98
|
+
"""
|
|
99
|
+
body: dict = {"urls": urls}
|
|
100
|
+
if cwv:
|
|
101
|
+
body["cwv"] = True
|
|
102
|
+
r = requests.post(f"{BASE_URL}/audit/batch", json=body, headers={"X-API-Key": api_key, **_HEADERS})
|
|
60
103
|
r.raise_for_status()
|
|
61
104
|
return r.json()
|
|
62
105
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seoscoreapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.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
|
|
@@ -29,10 +29,11 @@ pip install seoscoreapi
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
|
-
|
|
32
|
+
import os
|
|
33
|
+
from seoscoreapi import audit
|
|
33
34
|
|
|
34
|
-
# Get a free API key (2 audits/day, no credit card)
|
|
35
|
-
key =
|
|
35
|
+
# Get a free API key (2 audits/day, no credit card) at https://seoscoreapi.com/#signup
|
|
36
|
+
key = os.environ["SEO_SCORE_API_KEY"]
|
|
36
37
|
|
|
37
38
|
# Run an audit
|
|
38
39
|
result = audit("https://example.com", api_key=key)
|
|
@@ -43,9 +44,10 @@ print(f"Score: {result['score']}/100 ({result['grade']})")
|
|
|
43
44
|
|
|
44
45
|
| Function | Description |
|
|
45
46
|
|---|---|
|
|
46
|
-
| `signup(email)` |
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
47
|
+
| `signup(email)` | Start signup: the API emails a 6-digit code. Returns the API's response (`{"status": "verification_required", ...}`), not a key |
|
|
48
|
+
| `verify(email, code)` | Finish signup with the emailed code. Returns the API key (shown only once) |
|
|
49
|
+
| `audit(url, api_key, cwv=False)` | Run SEO audit on a URL. `cwv=True` adds measured Core Web Vitals (slower, see below) |
|
|
50
|
+
| `batch_audit(urls, api_key, cwv=False)` | Audit up to 10 URLs in one call (paid). `cwv=True` adds Core Web Vitals to each |
|
|
49
51
|
| `compare(urls, api_key)` | Compare 2–5 URLs with a structured diff (Basic+) |
|
|
50
52
|
| `competitive_audit(url, competitor_url, keyword, api_key)` | Head-to-head audit with gap score (Pro+) |
|
|
51
53
|
| `history(url, api_key, limit=100, since=None)` | Full audit timeseries + summary for a URL (Starter+) |
|
|
@@ -84,6 +86,31 @@ print(f"Score: {result['score']}/100 ({result['grade']})")
|
|
|
84
86
|
Errors are `requests.HTTPError` (from `raise_for_status()`); the API's message is in
|
|
85
87
|
`err.response.json()["detail"]`.
|
|
86
88
|
|
|
89
|
+
## Get an API key from code
|
|
90
|
+
|
|
91
|
+
Signup takes two calls. `signup()` emails a 6-digit code (valid for 10 minutes) and
|
|
92
|
+
`verify()` trades that code for the key. You can also sign up at
|
|
93
|
+
[seoscoreapi.com](https://seoscoreapi.com/#signup).
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import requests
|
|
97
|
+
from seoscoreapi import signup, verify
|
|
98
|
+
|
|
99
|
+
email = "you@example.com"
|
|
100
|
+
print(signup(email)["message"]) # "Check your email for a 6-digit verification code."
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
key = verify(email, input("Code from the email: "))
|
|
104
|
+
except requests.HTTPError as e:
|
|
105
|
+
raise SystemExit(e.response.json()["detail"]) # wrong or expired code
|
|
106
|
+
|
|
107
|
+
print(key) # save it: the key is shown only once
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`signup()` raises `requests.HTTPError` when the address is refused (400) or already
|
|
111
|
+
has an active key (409). `verify()` raises it (400) on a wrong or expired code; five
|
|
112
|
+
wrong codes void the signup, so call `signup()` again.
|
|
113
|
+
|
|
87
114
|
## Historical tracking
|
|
88
115
|
|
|
89
116
|
Every audit on a paid plan returns a `history` block on the `/audit` response:
|
|
@@ -111,6 +138,27 @@ add_monitor(
|
|
|
111
138
|
|
|
112
139
|
Slack incoming-webhook URLs are auto-formatted as Block Kit messages; any other https endpoint receives the raw event JSON.
|
|
113
140
|
|
|
141
|
+
## Core Web Vitals (opt-in)
|
|
142
|
+
|
|
143
|
+
Audits are fast because they do not wait on Google PageSpeed Insights. Ask for measured
|
|
144
|
+
Core Web Vitals when you need them:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
result = audit("https://example.com", api_key=key, cwv=True)
|
|
148
|
+
vitals = result["core_web_vitals"]
|
|
149
|
+
if vitals["status"] == "ok":
|
|
150
|
+
print(vitals["lcp_ms"], vitals["inp_ms"], vitals["cls"], vitals["source"]) # source: "field" or "lab"
|
|
151
|
+
elif vitals.get("pending"):
|
|
152
|
+
print("Still measuring; audit this URL again in a minute")
|
|
153
|
+
else:
|
|
154
|
+
print("No Core Web Vitals:", vitals.get("reason"))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The first measurement of a URL adds about 10 seconds (the API waits up to 25); it is
|
|
158
|
+
cached for 24 hours, so repeats are instant. It works on every plan and never changes the
|
|
159
|
+
score. Without `cwv=True` the block reads `{"status": "not_requested", ...}` with the
|
|
160
|
+
metrics set to `None`.
|
|
161
|
+
|
|
114
162
|
## Accessibility audit and report files
|
|
115
163
|
|
|
116
164
|
```python
|
|
@@ -202,3 +202,79 @@ def test_crawl_starts_then_polls():
|
|
|
202
202
|
assert seo.crawl("https://e.com", "k", max_pages=5)["result"] == {"ok": 1}
|
|
203
203
|
assert post.call_args[1]["json"] == {"url": "https://e.com", "max_pages": 5}
|
|
204
204
|
assert get.call_args[0][0] == B + "/crawl/abc"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# --- signup is two steps: POST /signup emails a code, POST /verify returns the key
|
|
208
|
+
|
|
209
|
+
def test_signup_returns_the_verification_response_not_a_key():
|
|
210
|
+
payload = {"status": "verification_required", "message": "Check your email for a 6-digit verification code."}
|
|
211
|
+
with patched("post", payload) as m:
|
|
212
|
+
out = seo.signup("you@example.com")
|
|
213
|
+
assert out == payload
|
|
214
|
+
assert m.call_args.args[0] == f"{B}/signup"
|
|
215
|
+
assert m.call_args.kwargs["json"] == {"email": "you@example.com"}
|
|
216
|
+
assert "X-API-Key" not in m.call_args.kwargs["headers"]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_signup_passes_through_a_response_that_carries_a_key():
|
|
220
|
+
with patched("post", {"api_key": "ssa_x", "tier": "free"}):
|
|
221
|
+
assert seo.signup("you@example.com")["api_key"] == "ssa_x"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_signup_raises_when_the_email_already_has_a_key():
|
|
225
|
+
with patched("post", {"detail": "Email already has an active key (prefix: ssa_abc...)"}, status=409):
|
|
226
|
+
with pytest.raises(seo.requests.HTTPError):
|
|
227
|
+
seo.signup("you@example.com")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_verify_returns_the_api_key():
|
|
231
|
+
payload = {"api_key": "ssa_live_key", "tier": "free", "limits": {}, "message": "Save your API key"}
|
|
232
|
+
with patched("post", payload) as m:
|
|
233
|
+
assert seo.verify("you@example.com", " 012345 ") == "ssa_live_key"
|
|
234
|
+
assert m.call_args.args[0] == f"{B}/verify"
|
|
235
|
+
assert m.call_args.kwargs["json"] == {"email": "you@example.com", "code": "012345"}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_verify_accepts_an_integer_code():
|
|
239
|
+
with patched("post", {"api_key": "k"}) as m:
|
|
240
|
+
seo.verify("you@example.com", 123456)
|
|
241
|
+
assert m.call_args.kwargs["json"]["code"] == "123456"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_verify_raises_on_a_bad_code():
|
|
245
|
+
with patched("post", {"detail": "Invalid verification code. 4 attempts remaining."}, status=400):
|
|
246
|
+
with pytest.raises(seo.requests.HTTPError):
|
|
247
|
+
seo.verify("you@example.com", "000000")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# --- Core Web Vitals opt-in (1.7.0) -------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def test_audit_does_not_ask_for_core_web_vitals_by_default():
|
|
253
|
+
with patched("get", {"score": 90}) as get:
|
|
254
|
+
assert seo.audit("https://e.com", "k") == {"score": 90}
|
|
255
|
+
args, kwargs = get.call_args
|
|
256
|
+
assert args[0] == B + "/audit"
|
|
257
|
+
assert kwargs["params"] == {"url": "https://e.com"}
|
|
258
|
+
assert kwargs["headers"]["X-API-Key"] == "k"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_audit_cwv_true_sends_the_parameter():
|
|
262
|
+
payload = {"score": 90, "core_web_vitals": {"status": "ok", "lcp_ms": 1800}}
|
|
263
|
+
with patched("get", payload) as get:
|
|
264
|
+
assert seo.audit("https://e.com", "k", cwv=True)["core_web_vitals"]["lcp_ms"] == 1800
|
|
265
|
+
assert get.call_args.kwargs["params"] == {"url": "https://e.com", "cwv": "true"}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_audit_cwv_false_is_the_default_request():
|
|
269
|
+
with patched("get", {}) as get:
|
|
270
|
+
seo.audit("https://e.com", "k", cwv=False)
|
|
271
|
+
assert get.call_args.kwargs["params"] == {"url": "https://e.com"}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_batch_audit_cwv():
|
|
275
|
+
with patched("post", {"count": 1, "results": []}) as post:
|
|
276
|
+
seo.batch_audit(["https://a.com"], "k")
|
|
277
|
+
assert post.call_args.kwargs["json"] == {"urls": ["https://a.com"]}
|
|
278
|
+
seo.batch_audit(["https://a.com"], "k", cwv=True)
|
|
279
|
+
assert post.call_args.kwargs["json"] == {"urls": ["https://a.com"], "cwv": True}
|
|
280
|
+
assert post.call_args.args[0] == B + "/audit/batch"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|