Metadata-Version: 2.4
Name: graphon-client
Version: 0.17.0
Summary: A Python client library for the Graphon API - Build graphs from your files
Author-email: Arbaaz Khan <arbaaz@graphon.ai>
License-Expression: MIT
Project-URL: Homepage, https://github.com/arbaazkhan2/graphon-client
Project-URL: Bug Reports, https://github.com/arbaazkhan2/graphon-client/issues
Project-URL: Source, https://github.com/arbaazkhan2/graphon-client
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx==0.28.1
Provides-Extra: e2e
Requires-Dist: google-cloud-storage==3.4.0; extra == "e2e"
Dynamic: license-file

# Graphon Client

A Python client library for the Graphon API - Build unified graphs from your files.
See https://app.graphon.ai/docs .

## File metadata

You can attach structured metadata to a file at upload time and read it back later.
Metadata is a list of segments:

```python
from graphon_client import FileMetadata, FileMetadataSegment

metadata = FileMetadata(segments=[
    FileMetadataSegment(
        start=0,            # page number (documents) or seconds (video/audio)
        end=12,             # closed range, start <= end
        attributes={"character": ["Alice", "Bob"], "scene": "kitchen"},
    ),
])
```

- `start`/`end` are **page numbers** for documents (integer-valued) and **seconds**
  for video/audio. The range is closed (`start <= end`).
- `attributes` is a map of scalar or scalar-list values.

The client does no validation; the **server** enforces the limits and rejects
oversized or malformed metadata. Current server caps: max 2 MiB (2097152 bytes)
total, max 10000 segments, max 64 attributes per segment, key length 128, value
length 4096, and 256 entries per list-valued attribute.

### Uploading with metadata and display overrides

`FileUpload` bundles a local file with optional metadata for
`upload_and_process_files()`:

```python
from graphon_client import FileUpload

uploads = [
    FileUpload(
        "movie.mp4",
        metadata=metadata,
        file_name="My Movie",   # optional; defaults to the path basename
        file_type="video",      # optional; defaults to extension-inferred type
    ),
]
file_details = await client.upload_and_process_files(uploads)
```

`file_name` and `file_type` are optional overrides. By default the display name is
the local path's basename and the type is inferred from the file extension.

### Reading metadata back

`get_file_metadata` returns exactly what you submitted at upload time:

```python
md = await client.get_file_metadata(file_id)   # FileMetadata | None
if md is not None:
    for seg in md.segments:
        print(seg.start, seg.end, seg.attributes)
```

It returns `None` when the file has no metadata attached. To avoid an unnecessary
call, each item from `list_files()` exposes `has_file_metadata_segments`, which is
`True` only when the file carries user-provided metadata.

## Creating a group from local files

`upload_process_and_create_group()` is the one-shot way to go from local files to a
queryable group. It creates the group **before** any bytes are uploaded (the
create-before-upload path): the server reserves the group and a workflow waits for
each file, then processing for a file begins when the client confirms its upload
(`notify_file_uploaded`), which `upload_process_and_create_group` does for you. You
hand it paths, it hands you back a `group_id`.

```python
group_id = await client.upload_process_and_create_group(
    group_name="My Graph",
    file_paths=["video.mp4", "document.pdf"],
)
response = await client.query_group(group_id, "What is this about?")
```

It accepts `files=[FileUpload(...)]` instead of `file_paths` for per-file metadata
and display overrides (the two are mutually exclusive), and `wait_for_ready=True`
(the default) blocks until the graph is built. Pass an `on_progress(step, current,
total)` callback to follow along — the steps are `generating_urls`,
`uploading_files`, and `building_graph`:

```python
def show(step: str, current: int, total: int) -> None:
    print(f"{step}: {current}/{total}")

group_id = await client.upload_process_and_create_group(
    group_name="My Graph",
    file_paths=["video.mp4", "document.pdf"],
    on_progress=show,
)
```

### Best-effort mode: "ready with N of M files"

By default the flow is **strict**: if any file's bytes fail to upload, the whole
group is cancelled and the call raises. Pass `exclude_ineligible_files=True` for
**best-effort** mode instead — a file whose upload fails is abandoned and the group
completes with the survivors. Only an all-files-failed batch raises.

After a best-effort run, read which files were dropped from the group's
`dropped_files`:

```python
from graphon_client import DroppedFile

group_id = await client.upload_process_and_create_group(
    group_name="My Graph",
    file_paths=["video.mp4", "document.pdf", "notes.txt"],
    exclude_ineligible_files=True,
)

group = await client.get_group_status(group_id)
for dropped in group.dropped_files:   # list[DroppedFile]
    # reason is a stable code: upload_timeout | abandoned | contention | failed
    print(f"dropped {dropped.file_id}: {dropped.reason}")
```

`dropped_files` is empty for groups created by any non-uploads path. The `reason`
codes are stable identifiers — map them to your own copy rather than displaying
them raw.

### Per-file upload lifecycle

`upload_process_and_create_group()` drives these for you; reach for them only
when managing uploads manually (e.g. building your own UI or upload pipeline).

- `notify_file_uploaded(group_id, file_id)` — confirm a file's bytes finished
  uploading to an in-flight group, so its processing can start. This call is the
  **sole** trigger for processing the file; the server does not independently poll
  GCS to reconcile a confirmation that never arrives. It is idempotent, so the
  recovery for a lost or failed call is simply to **retry it**. If the confirmation
  is never delivered, the file is never processed — it waits out the group's upload
  deadline and is then dropped (best-effort group, with reason `upload_timeout`) or
  fails the group (strict).
- `abandon_file(group_id, file_id)` — give up on a file's upload so the group drops
  it. **Pre-claim only** — once the file is being processed it is too late, and the
  server returns 409.
- `renew_upload_url(group_id, file_id)` — re-mint a resumable upload URL to retry a
  failed upload; the file keeps its place in the pending group. Returns the same
  shape as `get_signed_upload_url` (`file_id`, `upload_url`, `upload_fields`,
  `expires_at`).

### Cancelling an in-flight group

`delete_group(group_id)` both deletes an ordinary group and cancels an in-flight
uploads-path creation, returning a string that tells you which happened:

- `"deleted"` — an ordinary group was deleted.
- `"cancellation_requested"` — the target was still waiting on uploads, so deletion
  was requested. The group row persists until its workflow terminates; observe that
  with `get_group_status()` or `poll_group_until_ready()`.

Files are never deleted by this call.

### `create_group` flags for pending files

`create_group(file_ids, group_name, ...)` defaults to requiring every file to be
`SUCCESS`. Two keyword flags (both default `False`) relax that:

- `allow_pending_files` — accept files whose bytes have not landed yet
  (`UNPROCESSED`). The server reserves the group and a workflow waits for each
  upload, processes it, and builds the group from the files that succeed (the
  create-before-upload path that `upload_process_and_create_group()` drives).
- `exclude_ineligible_files` — drop an ineligible file from the submission instead
  of failing the whole request.
