Metadata-Version: 2.4
Name: urlscope
Version: 0.1.3rc1
Summary: Python wrapper for the urlscan.io API.
Author: Jan Wychowaniak
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: httpx
Requires-Dist: pydantic>=2
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: respx; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# urlscope

`urlscope` is an async-first Python wrapper for the urlscan.io API. It provides typed Pydantic models for common API responses, automatic API key handling, built-in retry logic for rate limits, and a sync convenience wrapper for scripts.

## Installation

```bash
pip install urlscope
```

Set your API key before making requests:

```bash
export URLSCAN_API_KEY="your-api-key-here"
```

## Quickstart

### Async submit and wait

```python
import asyncio
from urlscope import UrlscopeClient


async def main() -> None:
    async with UrlscopeClient() as client:
        result = await client.submit_and_wait(
            "https://example.com",
            visibility="public",
        )
        print(result.task.uuid)
        print(result.page.url)
        print(result.verdicts.score if result.verdicts else None)


asyncio.run(main())
```

### Sync usage

```python
from urlscope import SyncClient


with SyncClient() as client:
    result = client.get_result("scan-uuid-here")
    print(result.page.url)
```

### Search

```python
import asyncio
from urlscope import UrlscopeClient


async def main() -> None:
    async with UrlscopeClient() as client:
        response = await client.search("domain:example.com", size=10)
        for item in response.results:
            print(item.id, item.page.get("url") if item.page else None)

        # Cursor-based pagination is handled via the previous item's sort key.
        if response.has_more and response.results and response.results[-1].sort:
            next_page = await client.search(
                "domain:example.com",
                size=10,
                search_after=response.results[-1].sort,
            )
            print(len(next_page.results))


asyncio.run(main())
```

### Download artifacts

```python
import asyncio
from urlscope import UrlscopeClient


async def main() -> None:
    async with UrlscopeClient() as client:
        screenshot = await client.get_screenshot("scan-uuid-here")
        dom = await client.get_dom("scan-uuid-here")
        print(len(screenshot), len(dom))


asyncio.run(main())
```

### Check quotas

```python
import asyncio
from urlscope import UrlscopeClient


async def main() -> None:
    async with UrlscopeClient() as client:
        quotas = await client.get_quotas()
        for q in quotas.quotas[:5]:
            print(q.scope, q.action, q.window, q.remaining, q.limit)


asyncio.run(main())
```

### Error handling

```python
import asyncio
from urlscope import RateLimitError, ScanTimeoutError, UrlscopeClient


async def main() -> None:
    async with UrlscopeClient() as client:
        try:
            await client.submit_and_wait("https://example.com", poll_timeout=120.0)
        except ScanTimeoutError as exc:
            print(exc.uuid)
        except RateLimitError as exc:
            print(exc.retry_after, exc.scope, exc.window)


asyncio.run(main())
```

## API Reference

Primary clients:

- `UrlscopeClient`: async interface for submit, result retrieval, polling, search, artifacts, and quotas
- `SyncClient`: sync wrapper with the same method surface for scripts and simple integrations

Key response models:

- `SubmissionResponse`
- `ScanResult`, `TaskInfo`, `PageInfo`, `Verdicts`, `BrandMatch`, `ScanLists`, `CertificateInfo`
- `SearchResponse`, `SearchResultItem`
- `QuotaInfo`, `QuotaWindow`

Key exceptions:

- `UrlscopeError`
- `AuthenticationError`
- `ValidationError`
- `NotFoundError`
- `ScanDeletedError`
- `RateLimitError`
- `ScanTimeoutError`
- `APIError`

## License

MIT
