Metadata-Version: 2.4
Name: ffl-python
Version: 0.1.4
Summary: Python binding for FastFileLink, backed by the portable ffl.com APE
Author: ffl-python contributors
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/nuwainfo/ffl-python
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: PyYAML>=6.0; extra == "dev"
Requires-Dist: setuptools>=68; extra == "dev"
Requires-Dist: wheel>=0.42; extra == "dev"
Dynamic: license-file

# ffl-python

Python binding for FastFileLink. The package bundles the portable `ffl.com` APE and
runs it behind a Python API; callers do not need to locate or install a separate FFL
binary.

The low-level command grammar is generated by APEBind from `binding/ffl.apebind.yaml`.
The public API in `src/ffl/client.py` is intentionally handwritten so FFL-specific
library semantics stay explicit and reviewable.

## Development

```bash
python -m venv .venv
.venv/Scripts/python -m pip install -e ".[dev]"   # Windows
# or: .venv/bin/python -m pip install -e ".[dev]"

.\scripts\test.ps1                 # Windows
# or: ./scripts/test.sh              # Linux/macOS

.\scripts\build.ps1                # Windows
# or: ./scripts/build.sh             # Linux/macOS
```

The test directly imports `ffl`, shares a binary file, and downloads it through the
bundled `ffl.com` APE. It requires working network access to FastFileLink. A wheel is a
build artifact, not the source of truth.

The integration suite covers ordinary, E2EE, relay, pickup-code, and public-key
transfers; text, bytes, folders, multiple files, QR images, hooks, explicit ports, and
session shutdown, and Basic Auth download.

## Share

```python
import ffl

with ffl.share("release.zip", max_downloads=1, timeout_seconds=1800) as session:
    print(session.link)
```

`share()` always requests a foreground FFL process and disables clipboard side effects,
which gives library callers a deterministic `ShareSession` they can stop or keep alive.
FFL's runtime-owned `--json` output is used internally to wait until `session.link` is
ready.

Multiple files are accepted directly:

```python
session = ffl.share(["one.txt", "two.txt"], name="files.zip")
```

Text and bytes helpers own their temporary file until the share session is closed:

```python
with ffl.share_text("hello", name="hello.txt") as session:
    print(session.link)
```

Optional-value FFL flags use natural Python values. For example, `receipt=True` emits
`--receipt` without a value, while `receipt="me@example.com"` emits the flag with the
address.

### Stream a source without a temporary file

`share_stream()` passes a binary file-like object directly to FFL stdin. It is useful
for database dumps, generated artifacts, and other data that should not first be
materialized as a separate temporary file:

```python
with open("backup.tar", "rb") as source:
    with ffl.share_stream(source, name="backup.tar") as session:
        print(session.link)
```

## Download

```python
result = ffl.download("https://example.fastfilelink/...", output_path="download.bin")
print(result.output_path)
print(result.transfer_mode)
```

`download()` waits for the foreground FFL process to finish and returns a
`DownloadResult`. For cancellation, progress, or caller-controlled timeouts, use
`start_download()` and manage its `DownloadSession` explicitly.

### Stream a download

`download_stream()` exposes FFL stdout as binary chunks without buffering the whole
file in memory:

```python
with ffl.download_stream("https://example.fastfilelink/...") as transfer:
    with open("output.bin", "wb") as target:
        for chunk in transfer.iter_bytes():
            target.write(chunk)

    result = transfer.wait()
```

Consume `iter_bytes()` through EOF before calling `wait()`. Calling `wait()` with
unconsumed streamed stdout raises `RuntimeError`; this avoids silently discarding
binary data or deadlocking when the child process fills its stdout pipe.

## Authentication secrets

For shares protected with HTTP Basic Auth, FFL supports `FFL_AUTH_PASSWORD`. Set it in
the application environment and pass only `auth_user` to keep the password out of the
FFL command line:

```bash
export FFL_AUTH_PASSWORD='use-your-secret-manager'
```

```powershell
$env:FFL_AUTH_PASSWORD = 'use-your-secret-manager'
```

```python
with ffl.share("release.zip", auth_user="deploy") as session:
    print(session.link)
```

Do not also pass `auth_password=` when using this pattern: FFL gives the explicit CLI
option precedence over `FFL_AUTH_PASSWORD`. The environment variable applies to the
sharing side; pass download credentials explicitly when downloading a protected link.

## Key generation

```python
result = ffl.keygen("alice")
print(result.public_key_path)
print(result.private_key_path)
```

`keygen()` has a 60-second process timeout and verifies that the key paths reported by
FFL exist.

## Version and raw access

```python
print(ffl.version())
result = ffl.raw(["download", "--help"])
```

`raw()` is the escape hatch for new FFL options or commands that the semantic API has
not adopted yet.

## WSL2

If an operation fails with `TLSError([0x6300])`, WSL may be routing the bundled
`.com` APE through Windows interop. Run the following in WSL, then restart the WSL
session:

```bash
sudo sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/WSLInterop'
```

## Updating the FFL binding

Install APEBind from its source project. When adopting a new `ffl.com`, first inspect the
CLI into a raw discovery file:

```bash
./scripts/inspect.sh /path/to/ffl.com
```

This writes `binding/ffl.discovered.apebind.yaml` and automatically applies the hidden
command seeds in `binding/ffl.commands.yaml`. Review the discovered-schema diff, then
manually merge CLI changes into the canonical semantic contract
`binding/ffl.apebind.yaml`. Automatic inspection never overwrites the semantic contract.

After reviewing the semantic schema, regenerate the low-level binding:

```bash
./scripts/regenerate.sh /path/to/ffl.com
# Windows: .\scripts\regenerate.ps1 D:\ffl.com
```

The script invokes APEBind into an isolated temporary project and replaces only these
machine-owned files:

- `src/ffl/_generated.py`
- `src/ffl/_runtime.py`
- `src/ffl/bin/ffl.com`
- `src/ffl/py.typed`

It deliberately does not overwrite `client.py`, `models.py`, parsing logic, tests, or the
semantic schema.
