Metadata-Version: 2.4
Name: bypass-vuotlink-sdk
Version: 0.4.0
Summary: Python SDK for the bypass-vuotlink API
Requires-Python: >=3.10
Requires-Dist: httpx>=0.24
Description-Content-Type: text/markdown

# bypass-vuotlink-sdk

Python SDK for the [bypass-vuotlink](https://github.com/htilssu/bypass_vuotlink) API — a service that resolves shortlink/safelink URLs (e.g. vuotnhanh.com) to their final destination by running a real browser workflow.

## Install

```bash
pip install bypass-vuotlink-sdk
```

## Quick start

```python
from bypass_vuotlink_sdk import BypassVuotLink

with BypassVuotLink(base_url="http://localhost:8000") as client:
    result = client.resolve("https://vuotnhanh.com/abc123")
    print(result.final_url)
```

### Async

```python
from bypass_vuotlink_sdk import AsyncBypassVuotLink

async with AsyncBypassVuotLink(base_url="http://localhost:8000") as client:
    result = await client.resolve("https://vuotnhanh.com/abc123")
    print(result.final_url)
```

## Configuration

```python
from bypass_vuotlink_sdk import BypassVuotLink, ClientConfig

# Pass parameters directly
client = BypassVuotLink(
    base_url="http://localhost:8000",
    timeout=90.0,
    api_key="secret",
)

# Or via a config object
config = ClientConfig(base_url="http://localhost:8000", api_key="secret")
client = BypassVuotLink.from_config(config)

# Or from environment variables
# BYPASS_BASE_URL, BYPASS_TIMEOUT, BYPASS_API_KEY
client = BypassVuotLink.from_env()
```

## Result

```python
result.requested_url  # the URL you passed in
result.final_url      # resolved destination
result.status_code    # HTTP status at final_url
result.title          # page title
result.workflow       # workflow used (e.g. "vuotnhanh")
result.hops_used      # how many workflow hops were actually followed
result.hops_exceeded  # True if max_hops was hit before a terminal workflow was reached
```

## Supported workflows

```python
workflows = client.list_workflows()

for workflow in workflows.workflows:
    print(workflow.name)
    print(workflow.link_formats)
    print(workflow.maintenance, workflow.maintenance_message)
```

`resolve()` raises `WorkflowMaintenanceError` when the matched workflow is
temporarily disabled. The exception exposes `workflow` and
`maintenance_message`.

Example item:

```python
workflow.name          # "funlink"
workflow.link_formats  # ["https://funlink.io/..."]
```

## Chained links (max_hops)

Some links resolve through more than one bypass workflow in a row (e.g. a
shortlink resolving to a funlink link, which itself resolves to a toplinks
link). By default `resolve()` follows up to 10 such hops before returning.
Pass `max_hops` to cap that:

```python
# Solve only the first link and return immediately, without following any
# further chained hop.
result = client.resolve(url, max_hops=1)
```

`result.hops_used` reports how many hops were actually followed. If the chain
doesn't reach a terminal workflow within `max_hops`, `result.hops_exceeded` is
`True` (`hops_used == max_hops`) and `result.final_url` is just wherever the
last hop left off, not necessarily the fully-resolved destination.

## Fetch a code without submitting it

```python
result = client.fetch_code(
    "https://funlink.io/some/destination",
    "funlink",  # or "toplinks"
    source="live",  # or "background"
)
result.code       # confirmation code
result.dest_url   # the destination it was fetched from
```

## Code pool

```python
# Pop a prefetched code for a cached keyword_text/image_url pair.
result = client.consume_code("some keyword", image_url="https://...")
result.code
result.resolved_url
```

## Not-found tasks (manual search fallback)

When a keyword's destination link couldn't be auto-resolved (e.g. via OCR +
Google search), it's marked `not_found` in the cache. These helpers let a
human/worker claim one, search the web manually, and report back a candidate:

```python
task = client.claim_not_found_task(lease_seconds=900)
task.keyword_id
task.keyword_text
task.image_url
task.search_query

# ... search the web manually, find a candidate destination URL ...

result = client.verify_not_found_task(task.keyword_id, "https://found-it.example/page")
if result.verified:
    print("Accepted:", result.resolved_url)
else:
    print("Rejected:", result.reason)
```

`claim_not_found_task()` raises `ApiError` (status_code 404) when there are no
claimable not-found entries. The lease (`lease_seconds`) auto-expires if never
reported back, so an unreported claim becomes claimable by someone else again
without any explicit release call.

```python
# Re-open every not-found entry for another search attempt.
result = client.reset_not_found()
result.reset_count
```

## Error handling

```python
from bypass_vuotlink_sdk import (
    UnsupportedUrlError,
    UnsafeUrlError,
    BrowserExecutionError,
    BypassVuotlinkError,
)

try:
    result = client.resolve(url)
except UnsupportedUrlError:
    ...  # no workflow supports this URL type (HTTP 422)
except UnsafeUrlError:
    ...  # URL is private or unsafe (HTTP 400)
except BrowserExecutionError:
    ...  # browser workflow failed (HTTP 502)
except BypassVuotlinkError:
    ...  # catch-all
```
