Skip to main content

mangools

Python client for the Mangools API — KWFinder, SERPChecker, SERPWatcher, LinkMiner, SiteProfiler and AI Search Watcher. 82 operations, 184 models, httpx-based, fully typed, py.typed.

Early access, generated client — feedback welcome. Every line under mangools/ is generated from the OpenAPI document committed in this repo (openapi.json); nothing is hand-written. The 0.x series will change shape as the spec is completed, and some responses are still untyped because the spec does not describe them yet — see Known gaps. Please report anything that surprises you.

Install

pip install mangools

Python 3.9 or newer. Runtime dependencies: httpx, attrs, python-dateutil.

Hello world

import os

from mangools import MangoolsClient
from mangools.api.aisearchwatcher import get_aiwatcher_monitors
from mangools.api.kwfinder import get_kwfinder_related_keywords
from mangools.models import Error

client = MangoolsClient(api_key=os.environ["MANGOOLS_API_KEY"])

related = get_kwfinder_related_keywords.sync(client=client, kw="seo tools")
if isinstance(related, Error):
    raise SystemExit(f"{related.error.type_}: {related.error.message}")
if related is not None and related.keywords:
    print(f"related keywords ({related.count_keywords_before_limit} before the limit):")
    for keyword in related.keywords[:5]:
        print(f"  {keyword.kw!r:<28} sv={keyword.sv} cpc={keyword.cpc} seo={keyword.seo}")

monitors = get_aiwatcher_monitors.sync(client=client)
if isinstance(monitors, Error):
    raise SystemExit(f"{monitors.error.type_}: {monitors.error.message}")
if monitors is not None and monitors.monitors:
    print(f"\n{len(monitors.monitors)} AI Search Watcher monitors:")
    for monitor in monitors.monitors:
        print(f"  {monitor.field_id}  {monitor.brand!r} -> {monitor.domain}")

get_kwfinder_related_keywords also takes location_id and language_id; both default to 0 (worldwide / all languages). Resolve real values with mangools.api.kwfinder.get_mangools_locations.

Authentication

Mangools authenticates with an API key in the x-access-token header. It is not an RFC 6750 bearer token, so Authorization: Bearer … will not work.

from mangools import MangoolsClient

client = MangoolsClient(api_key=os.environ["MANGOOLS_API_KEY"])

MangoolsClient is a thin factory over the generated AuthenticatedClient that fills in the header name from the spec's ApiKeyAuth scheme. Keyword arguments are forwarded verbatim, so timeout=httpx.Timeout(30.0), headers={...}, follow_redirects=True, verify_ssl=False and httpx_args={...} all work:

import httpx

client = MangoolsClient(
    api_key=os.environ["MANGOOLS_API_KEY"],
    timeout=httpx.Timeout(30.0),
    raise_on_unexpected_status=True,
)

base_url defaults to the spec's servers[0].url, https://api.mangools.com/v3. Override it to point at a sandbox.

Never hard-code the key. Read it from the environment or a secret manager.

Calling an endpoint

Every operation is a module under mangools.api.<tag> exposing four functions:

function returns on an undocumented status
sync(...) the parsed body, or None None (or raises if raise_on_unexpected_status)
sync_detailed(...) Response[T] with status_code, headers, content, parsed same
asyncio(...) awaitable parsed body same
asyncio_detailed(...) awaitable Response[T] same
import asyncio

from mangools.api.serpwatcher import get_serpwatcher_trackings

async def main() -> None:
    response = await get_serpwatcher_trackings.asyncio_detailed(client=client)
    print(response.status_code, response.parsed)

asyncio.run(main())

Use sync_detailed / asyncio_detailed when you need the status code or headers — for example the Retry-After on a 429.

Optional fields are Unset, not None, because the API distinguishes "absent" from "explicitly null". Unset is falsy and narrows correctly under mypy:

from mangools.types import UNSET, Unset

if keyword.sv is not UNSET:
    ...       # keyword.sv is an int here

Errors

Most non-2xx responses parse into the Error envelope, so error handling is a type check rather than a status-code table:

from mangools.models import Error

result = get_kwfinder_related_keywords.sync(client=client, kw="seo tools")
if isinstance(result, Error):
    print(result.error.type_, result.error.message)
    if result.error.retry_after is not UNSET:
        print("retry after", result.error.retry_after, "s")

error.errors carries the Joi messages on a 422 and error.retry_after the wait in seconds on a 429. A status the spec does not document raises mangools.errors.UnexpectedStatus when the client is built with raise_on_unexpected_status=True.

Two 429s exist and they do not look alike. The application's own 429 is the Error envelope above. The gateway's is produced by nginx's limit_req before the request reaches the application, so its body is nginx's HTML error page; the spec declares it as text/html, and the operations that carry it gain a str arm rather than an Error arm:

from mangools.api.kwfinder import get_kwfinder_limits

limits = get_kwfinder_limits.sync(client=client)   # Union[Limit, str] | None
if isinstance(limits, str):
    ...       # the gateway HTML page: 4 req/s per client IP was exceeded

GET /kwfinder/limits is also the exception to the Error type check. It accepts an anonymous caller and has no validation or quota gate, so there is no Error branch to check for at all — an isinstance(result, Error) there is dead code.

Typing

The package ships a PEP 561 py.typed marker, so mypy and pyright type-check your call sites with no stub package. The generated client itself passes mypy --strict with no type: ignore anywhere.

Regenerating

The client is a pure function of openapi.json plus the pinned toolchain:

pip install -r requirements-dev.txt
./scripts/regenerate.sh

CI runs the same script and fails if the working tree moves, so mangools/ is never edited by hand. scripts/regenerate.sh also rewrites SPEC_GAPS.md and TYPE_GAPS.md, which means a spec change that closes a gap has to land the updated report in the same commit.

openapi-python-client is used rather than OpenAPI Generator: OpenAPI Generator's python generator offers only asyncio, tornado and urllib3 transports, none of them httpx.

Spec pin

spec document openapi.json, committed verbatim in this repo
spec release v1.1.0 in mangools/api-spec
info.version 3.0.0
sha256 63864af3095f2e171ab8e2ee154a8282aa956c83770b1957c5c68ae4cc004f3f
previous sha256 ee00faf91df54a1e74fb12be34bb77055f9a60a945047e753a0451d8e3857370
generator openapi-python-client==0.26.2

No operation moved between those two documents: the same 82 under the same operationIds at the same paths, so no generated function was renamed and no call site has to change. v1.1.0 gives the four free-form metric maps a value schema and gives the 404 of GET /mangools/locations/{location} the Error body it always should have had.

Typing the two RankDist maps added six models (178 → 184), all of them new names nothing referred to before:

RankDistRankAdditionalProperty              RankDistVisibilityAdditionalProperty
RankDistRankAdditionalPropertyOrganicItem   RankDistVisibilityAdditionalPropertyOrganic
RankDistRankAdditionalPropertyPaidItem      RankDistVisibilityAdditionalPropertyPaid

Where you read dict[str, Any] out of RankDist.rank or .visibility before, you now read a typed object. That is the one place a caller written against v1.0.0 sees a difference, and it is a widening of what the type system knows, not of what the API returns.

The first published spec did move four operations. If you are upgrading from it rather than from the previous pin, correcting paths the API does not serve renamed four modules:

was is
post_kwfinder_lists_by_list_id_keyword post_kwfinder_lists_by_list_id_keywords
delete_kwfinder_lists_by_list_id_keyword delete_kwfinder_lists_by_list_id_keywords
put_serpwatcher_trackings_by_tracking_id patch_serpwatcher_trackings_by_tracking_id
delete_serpwatcher_trackings_by_tracking_id_tags delete_serpwatcher_trackings_by_tracking_id_tags_by_tag_id (the tag is a path parameter, not a body field)

POST /serpwatcher/trackings/{tracking_id}/stats also moved kwIds from the query string into the request body, where the API reads it: the signature is now sync(tracking_id, *, client, body=PostSerpwatcherTrackingsByTrackingIdStatsBody(...), from_=…, to=…). Nothing is aliased; these five call sites have to be updated.

The bytes in openapi.json are the pin — the SDK cannot describe an endpoint the committed spec does not contain. The same document is tagged v1.1.0 in mangools/api-spec, which is private for now; the sha256 above is what identifies the pin from outside that repository.

Known gaps

Two generated reports track everything the spec does not yet say:

  • SPEC_GAPS.md — the full audit of openapi.json: 0 blockers, 0 bugs, 1 gap, with a JSON Pointer and the concrete schema that would fix it. It is the description-only allOf member on SPMetrics.fb, which takes nothing away from the caller.
  • TYPE_GAPS.md0 blocking positions. No operation returns Any and no model attribute is typed Any. Under spec v1.0.0 this was 1: GET /mangools/locations/{location} declared a 404 with no content and widened the parsed union to Union[Any, Location, str].

The four metric maps are typed as of v1.1.0

MozMetric.v, RankDist.rank, RankDist.visibility and RankDist.metrics_absolute.*.organic were free-form objects under spec v1.0.0 and reached you as dict[str, Any]. They now carry value schemas: MozMetric.v from 2000 sampled url_metrics documents and the Mozscape column bitmask, the two RankDist maps from their month buckets, and organic from the 20 DataForSEO columns that parseDomainStats writes.

RankDist.metrics_absolute.*.organic stays an open map on purpose, with a typed value: the provider chooses which columns it returns, so the column set is not the contract but the value type is.

Both gates are green on this pin. mypy --strict passes on 279 files and scripts/check_spec_any.py reports zero spec-derived Any. That gate stays in CI rather than being retired, because it is what would catch the next schema that arrives untyped.

Metric groups: absent, not null

An optional metric group is omitted from the response until its provider has been queried — the API builds it with a lookup that yields undefined, and res.json drops the key. The attribute is therefore Union[Unset, T]: check is not UNSET, not is not None.

SPMetrics.fb is the one exception. Its producer defaults the lookup to null, so it really can arrive as JSON null, and it is typed Union[SPMetricsFbType0, None, Unset] — three states, all reachable:

from mangools.api.siteprofiler import get_siteprofiler_overview

overview = get_siteprofiler_overview.sync(client=client, url_query="mangools.com")
if overview is not None and not isinstance(overview, Error):
    if overview.fb is UNSET:
        ...   # the field was not in the response
    elif overview.fb is None:
        ...   # Facebook data has never been fetched for this domain
    else:
        ...   # overview.fb.l is the engagement count

Base URL and the AI Search Watcher paths

The epic flags a possible /v3/ double prefix on AI Search Watcher operations. It does not occur in this spec: no path in openapi.json carries a /v3 prefix, AI Search Watcher paths are /aiwatcher/… like every other tag, and servers[0].url already ends in /v3. The generated client therefore requests https://api.mangools.com/v3/aiwatcher/monitors, which is the route the API serves. Nothing needs to be worked around; documentation that writes the path as /v3/aiwatcher/… is quoting the full URL rather than a spec path.

Releasing

The package is prepared but has never been uploaded; the mangools name on PyPI is free. Publishing is a deliberate human step:

pip install -r requirements-dev.txt
python -m build                     # writes dist/mangools-<version>.{tar.gz,whl}
twine check --strict dist/*
twine upload dist/*                 # the one command that publishes

To bump the version, edit package_version_override in openapi-python-client.yaml and run ./scripts/regenerate.sh. That is the only place the version is written by hand: regeneration renders it into mangools/__init__.py, and hatchling reads it back out of there. Editing __init__.py directly fails the regenerate-and-diff gate.

License

Apache-2.0. See LICENSE.

Release files for mangools 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mangools 0.1.0
File Size Uploaded
mangools-0.1.0.tar.gz 171.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mangools 0.1.0
File Interpreter ABI Platform
mangools-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 496.3 kB

Release files / mangools-0.1.0.tar.gz

Download URL mangools-0.1.0.tar.gz
Size 171.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3387f251de54832162ddbaded379c46d5b23500da66aa4a3e998de7797a8f524
BLAKE2b-256 checksum
How to use checksums
bc343d80b9b417754b9782e29226b36c0cf513b7045f7e38a0fb50d26c615965
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / mangools-0.1.0-py3-none-any.whl

Download URL mangools-0.1.0-py3-none-any.whl
Size 325.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
108d941436b7f12fa0f1497e04fef59bc0c14d5404f29caa10e8a33026b2b669
BLAKE2b-256 checksum
How to use checksums
baa0688d7fde6922aefa74f4c34351206c33e1db5ef74277c14fed1ac7b9b326
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page