Skip to content

klyrek-api

Status: built, not yet on PyPI (blocked on PyPI's new-project rate limit) — install from source

API surface discovery: OpenAPI/Swagger spec detection, common API-root probing, and GraphQL detection, plus REST-pattern classification for already-discovered URLs.

from klyrek_api.inventory import build_api_inventory
from klyrek_api.classify import is_rest_like

endpoints = build_api_inventory(client, "https://api.sandbox.klyrek.com/")
is_rest_like("https://api.sandbox.klyrek.com/api/v1/products")   # True

build_api_inventory probes a curated list of common spec paths (/openapi.json, /swagger.json, /api-docs, /v2/api-docs, /v3/api-docs, /swagger/v1/swagger.json, /.well-known/openapi.json) and common API-root paths (/api, /api/v1, /api/v2, /rest, /rest/v1), and parses any OpenAPI/Swagger spec it finds into real Endpoint records via parse_openapi_paths.

The sandbox's own openapi.json (a hand-authored, valid OpenAPI 3.0 document) parses cleanly into exactly its three real routes when run through this exact function.

API reference

klyrek_api.discovery

Probe common paths where an OpenAPI/Swagger spec or a bare API root is often exposed.

discover_api_roots

discover_api_roots(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]

Probe common API root paths; a non-404 response suggests something lives there.

Source code in klyrek_api\discovery.py
def discover_api_roots(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]:
    """Probe common API root paths; a non-404 response suggests something lives there."""
    endpoints: list[Endpoint] = []
    for path in COMMON_API_ROOT_PATHS:
        url = urljoin(base_url, path)
        if not client.scope.is_authorized(url):
            continue
        try:
            response = client.get(url)
        except Exception:
            logger.debug("failed to probe api root %s", url, exc_info=True)
            continue
        if response.status_code == 404:
            continue
        endpoints.append(
            Endpoint(
                url=url,
                method="GET",
                source="api-discovery",
                status_code=response.status_code,
                content_type=response.headers.get("content-type"),
                discovered_by="klyrek-api",
            )
        )
    return endpoints

discover_openapi_spec

discover_openapi_spec(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]

Probe well-known paths for an OpenAPI/Swagger spec; parse the first one found.

Source code in klyrek_api\discovery.py
def discover_openapi_spec(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]:
    """Probe well-known paths for an OpenAPI/Swagger spec; parse the first one found."""
    for path in COMMON_SPEC_PATHS:
        url = urljoin(base_url, path)
        if not client.scope.is_authorized(url):
            continue
        try:
            response = client.get(url)
        except Exception:
            logger.debug("failed to probe spec path %s", url, exc_info=True)
            continue
        if response.status_code != 200:
            continue
        try:
            document = response.json()
        except ValueError:
            continue
        if looks_like_openapi_spec(document):
            return parse_openapi_paths(document, base_url)
    return []

klyrek_api.inventory

Combine spec discovery, API-root probing, and GraphQL detection into one inventory.

build_api_inventory

build_api_inventory(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]

Probe a target for an OpenAPI/Swagger spec, common API roots, and a GraphQL endpoint.

Source code in klyrek_api\inventory.py
def build_api_inventory(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]:
    """Probe a target for an OpenAPI/Swagger spec, common API roots, and a GraphQL endpoint."""
    endpoints: list[Endpoint] = []
    seen: set[tuple[str, str]] = set()

    def add_all(discovered: list[Endpoint]) -> None:
        for endpoint in discovered:
            key = (endpoint.method, endpoint.url)
            if key not in seen:
                seen.add(key)
                endpoints.append(endpoint)

    add_all(discover_openapi_spec(client, base_url))
    add_all(discover_api_roots(client, base_url))
    add_all(discover_graphql(client, base_url))
    return endpoints

klyrek_api.classify

Heuristics for tagging already-discovered URLs as API-shaped, without any new requests.

is_rest_like

is_rest_like(url: str) -> bool

Heuristic: does this URL's path look like a REST API route rather than a page?

Source code in klyrek_api\classify.py
def is_rest_like(url: str) -> bool:
    """Heuristic: does this URL's path look like a REST API route rather than a page?"""
    path = urlsplit(url).path
    return bool(_API_PATH_PATTERN.search(path) or _NUMERIC_ID_SEGMENT.search(path))

klyrek_api.openapi

Parse OpenAPI/Swagger spec documents into Endpoint records.

looks_like_openapi_spec

looks_like_openapi_spec(document: Any) -> bool

Cheap structural check: does this parsed JSON body look like an OpenAPI/Swagger spec?

Source code in klyrek_api\openapi.py
def looks_like_openapi_spec(document: Any) -> bool:
    """Cheap structural check: does this parsed JSON body look like an OpenAPI/Swagger spec?"""
    return isinstance(document, dict) and ("openapi" in document or "swagger" in document)

parse_openapi_paths

parse_openapi_paths(spec: dict[str, Any], base_url: str) -> list[Endpoint]

Extract Endpoint records from an OpenAPI/Swagger spec's paths object.

Source code in klyrek_api\openapi.py
def parse_openapi_paths(spec: dict[str, Any], base_url: str) -> list[Endpoint]:
    """Extract Endpoint records from an OpenAPI/Swagger spec's `paths` object."""
    endpoints: list[Endpoint] = []
    base_path = _base_path(spec)

    for path, path_item in spec.get("paths", {}).items():
        if not isinstance(path_item, dict):
            continue
        full_path = f"{base_path.rstrip('/')}/{path.lstrip('/')}" if base_path else path
        url = urljoin(base_url, full_path)
        shared_params = [
            p["name"]
            for p in path_item.get("parameters", [])
            if isinstance(p, dict) and p.get("name")
        ]

        for method, operation in path_item.items():
            if method.lower() not in _HTTP_METHODS or not isinstance(operation, dict):
                continue
            params = [
                *shared_params,
                *(
                    p["name"]
                    for p in operation.get("parameters", [])
                    if isinstance(p, dict) and p.get("name")
                ),
            ]
            endpoints.append(
                Endpoint(
                    url=url,
                    method=method.upper(),
                    source="api-spec",
                    params=params,
                    discovered_by="klyrek-api",
                )
            )

    return endpoints

klyrek_api.graphql

Detect a GraphQL endpoint via a minimal, introspection-free probe query.

discover_graphql

discover_graphql(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]

Probe common GraphQL paths; confirm by response shape, not just HTTP status.

Source code in klyrek_api\graphql.py
def discover_graphql(client: KlyrekHTTPClient, base_url: str) -> list[Endpoint]:
    """Probe common GraphQL paths; confirm by response shape, not just HTTP status."""
    endpoints: list[Endpoint] = []
    for path in COMMON_GRAPHQL_PATHS:
        url = urljoin(base_url, path)
        if not client.scope.is_authorized(url):
            continue
        try:
            response = client.post(url, json=_PROBE_QUERY)
        except Exception:
            logger.debug("failed to probe graphql path %s", url, exc_info=True)
            continue
        if response.status_code >= 500:
            continue
        try:
            data = response.json()
        except ValueError:
            continue
        if _looks_like_graphql_response(data):
            endpoints.append(
                Endpoint(
                    url=url,
                    method="POST",
                    source="api-graphql",
                    status_code=response.status_code,
                    discovered_by="klyrek-api",
                )
            )
    return endpoints