Metadata-Version: 2.4
Name: rftools-io
Version: 0.2.0
Summary: Python client for rftools.io — 241 RF & electronics calculators, plus async simulation jobs
Author-email: "rftools.io" <hello@rftools.io>
License-Expression: MIT
Project-URL: Homepage, https://rftools.io
Project-URL: Documentation, https://rftools.io/docs/python
Project-URL: Repository, https://github.com/antonpogrebenko-public/rftools-py
Project-URL: Bug Tracker, https://github.com/rftools/rftools-py/issues
Keywords: rf,electronics,calculator,antenna,pcb,signal
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
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: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Requires-Dist: click>=8.1
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Dynamic: license-file

# rftools

[![PyPI](https://img.shields.io/pypi/v/rftools-io)](https://pypi.org/project/rftools-io/)
[![Python](https://img.shields.io/pypi/pyversions/rftools-io)](https://pypi.org/project/rftools-io/)

Python client for **rftools.io** — 241 RF & electronics calculators, plus 13 async
simulation job types (FDTD, antenna NEC-2, filter Monte Carlo, and more), accessible
from Python and the command line.

## Installation

```bash
pip install rftools-io
```

## Quick Start

Get a free API key at [rftools.io/pricing](https://rftools.io/pricing) (5 calls/month free).

```bash
export RFTOOLS_API_KEY=rfc_your_key_here
```

```python
import rftools

result = rftools.calculate("vswr-return-loss", {"vswr": 2.5})
print(result["returnLoss"])  # 9.54 dB
print(result.values)         # {"returnLoss": 9.54, "reflectionCoeff": 0.333, ...}
```

## Authenticated Usage (API tier)

Get an API key at [rftools.io/pricing](https://rftools.io/pricing) ($19/mo, 10,000 calls/month).

```python
import rftools

client = rftools.Client(api_key="rfc_live_xxx")
# or set environment variable: export RFTOOLS_API_KEY=rfc_live_xxx

result = client.calculate("free-space-path-loss", {"frequency": 2400, "distance": 100})
print(result["pathLoss"])  # dB
```

## Batch Calculations

Run up to 50 calculations in a single HTTP request (API tier only):

```python
results = client.batch([
    ("vswr-return-loss", {"vswr": 1.5}),
    ("vswr-return-loss", {"vswr": 2.0}),
    ("free-space-path-loss", {"frequency": 2400, "distance": 50}),
])
for r in results:
    if r.ok:
        print(r.values)
    else:
        print(f"Error: {r.error}")
```

## Typed Category Stubs

IDE-friendly typed functions with parameter defaults matching the web UI:

```python
from rftools.calculators import rf, pcb, antenna, power

# RF
result = rf.vswr_return_loss(vswr=2.5)
result = rf.free_space_path_loss(frequency=2400.0, distance=100.0)

# Antenna
result = antenna.dipole_antenna(frequency=433.0)
result = antenna.parabolic_dish_antenna(frequency=10000.0, diameter=0.6)

# PCB
result = pcb.trace_width_current(current=2.0, tempRise=10.0, thickness=1.0)

# Power
result = power.voltage_divider(vin=12.0, r1=10000.0, r2=10000.0)
```

All 13 categories available: `rf`, `pcb`, `power`, `signal`, `antenna`, `general`, `motor`, `protocol`, `emc`, `thermal`, `sensor`, `unit_conversion`, `audio`.

## CLI

```bash
# Run a calculation
rftools calc vswr-return-loss --vswr 2.5

# With API key
RFTOOLS_API_KEY=rfc_xxx rftools calc free-space-path-loss --frequency 2400 --distance 100

# JSON output (pipe to jq)
rftools calc vswr-return-loss --vswr 2.5 --json | jq '.values.returnLoss'

# Submit an async simulation job, wait for it, print the result summary
rftools sim eye_diagram --param dataRate=10e9 --file trace.s2p

# Submit and return immediately (poll later with the printed job id)
rftools sim fdtd_sparam --param solveMode=express --no-wait

# List all calculators
rftools list

# Filter by category
rftools list --category rf

# Show calculator inputs/outputs
rftools info free-space-path-loss

# Library version
rftools version
```

## Error Handling

Errors are classified by HTTP status code (and, for a finished job, by the service's
`errorKind`) — never by matching text in a message.

```python
from rftools.exceptions import (
    AuthError, QuotaError, RateLimitError, ValidationError, NotFound, JobFailed,
)

try:
    result = client.calculate("vswr-return-loss", {"vswr": 2.5})
except QuotaError as e:
    print(f"Quota exceeded. Retry after: {e.retry_after}s")
except RateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after}s")
except AuthError:
    print("Invalid API key")
except NotFound:
    print("Unknown calculator slug")
except ValidationError as e:
    print(f"Bad inputs: {e.failures}")
```

| Exception | When |
|---|---|
| `AuthError` | Invalid or missing API key (HTTP 401) — also a spent quota, until the API ships a dedicated 402 for it, and an upload attempted with no key at all, which is raised locally |
| `QuotaError` | Monthly allowance exhausted (HTTP 402) |
| `RateLimitError` | Too many requests too fast (HTTP 429) |
| `ValidationError` | Bad inputs, locally or on the service (HTTP 400/422); `.failures` is the detail list |
| `NotFound` | Unknown calculator slug or job id (HTTP 404) |
| `JobFailed` | A job reached a terminal `failed` state; `.kind` is the service's `errorKind` |
| `APIError` | Unexpected/unclassified HTTP error, or a network failure |

`RftoolsError` is the base class for all of the above. Pre-0.2 names (`RFToolsError`,
`NotFoundError`) still work as aliases.

## Simulation jobs

Async simulation job types — FDTD transmission-line, antenna NEC-2, filter Monte Carlo,
eye diagram, and 9 more — run through the same `Client`. Parameters and input files are
validated locally against the job type's contract before anything is uploaded or sent.

```python
import rftools

client = rftools.Client(api_key="rfc_live_xxx")

# Submit and wait for it to finish, then fetch a summary of the result
job = client.submit_job(
    "eye_diagram",
    {"dataRate": 10e9, "prbs": "prbs15"},
    files=["trace.s2p"],   # paths, or (filename, bytes) tuples
    wait=True,
    on_progress=lambda j: print(j.status, j.progress, j.stage),
)
print(job.result())          # summary: headline values, warnings, provenance
print(job.result(full=True)) # the whole result payload

# Or submit now, poll later
job = client.submit_job("fdtd_sparam", {"solveMode": "express"})
print(job.id, job.status)
...
job = client.get_job(job.id)
job = job.wait(timeout=60)   # returns even if not finished by the deadline
```

A job that ends in `failed` raises `JobFailed` from `wait()` (or `result()`), carrying
`.kind` (one of the service's `errorKind` values) and the error message.

`wait()` polls the way the web app does: the first check happens immediately (no fixed
initial delay), then every 2s for the first 30s, every 5s until 5 minutes, and every 15s
after that, until the job reaches `completed` or `failed`.

### Uploads need a key

Uploading a file needs an API key; set `RFTOOLS_API_KEY`. The service refuses an
anonymous upload, so `upload()` and `submit_job(files=...)` raise `AuthError` with that
sentence locally — before the file is read and before any request is made. A job type
that takes no file still runs without a key, on the free lane, as before.

The uploaded object is recorded against the key's account, and only that account may
submit it: a key obtained from a log or a shared link is refused as though it did not
exist.

Uploaded files are capped at 10 MB (the API's limit) — `upload()`/`submit_job()` raise
`ValidationError` locally if a file is over that, before any request is made.

## Async Support

```python
import asyncio
import rftools

async def main():
    async with rftools.AsyncClient(api_key="rfc_live_xxx") as client:
        result = await client.calculate("vswr-return-loss", {"vswr": 2.5})
        print(result["returnLoss"])

asyncio.run(main())
```

Ideal for running many calculations concurrently in FastAPI or async scripts.

## Browse the Catalog

```python
# List all calculators
calcs = rftools.list_calculators()
print(f"{len(calcs)} calculators available")

# Filter by category
rf_calcs = rftools.list_calculators(category="rf")
for c in rf_calcs:
    print(f"{c.slug}: {c.title}")

# Get a single calculator's metadata
info = rftools.get_calculator("vswr-return-loss")
print(info.inputs)   # tuple of InputField
print(info.outputs)  # tuple of OutputField
```

## All Calculator Categories

The typed stub catalog (`rftools/calculators/`, `rftools.list_calculators()`) is
generated from a snapshot of the registry and is due for a refresh to the current
241; run `python scripts/generate_stubs.py --frontend-dir /path/to/rfhub/frontend`
to regenerate it. The counts below describe that snapshot.

| Category | Count | Example |
|---|---|---|
| `rf` | 26 | `vswr-return-loss`, `free-space-path-loss`, `rf-link-budget` |
| `pcb` | 13 | `trace-width-current`, `via-calculator`, `microstrip-impedance` |
| `power` | 20 | `voltage-divider`, `led-resistor`, `battery-life` |
| `signal` | 13 | `filter-designer`, `op-amp-gain`, `pwm-duty-cycle` |
| `antenna` | 8 | `dipole-antenna`, `eirp-calculator`, `parabolic-dish-antenna` |
| `general` | 21 | `lc-resonance`, `ohms-law`, `rc-time-constant` |
| `motor` | 18 | `brushless-dc-motor`, `stepper-motor`, `servo-motor` |
| `protocol` | 11 | `uart-baud-rate`, `i2c-pullup`, `can-bus-bit-timing` |
| `emc` | 16 | `emi-filter-lc`, `shielding-effectiveness`, `ground-loop` |
| `thermal` | 6 | `thermal-resistance`, `heat-sink`, `junction-temperature` |
| `sensor` | 17 | `thermistor-ntc`, `strain-gauge`, `hall-effect` |
| `unit-conversion` | 17 | `dbm-watts`, `frequency-wavelength`, `temperature` |
| `audio` | 17 | `speaker-crossover`, `amplifier-gain`, `room-acoustics` |

## Contributing

Regenerate stubs after calculator changes:

```bash
python scripts/generate_stubs.py --frontend-dir /path/to/rfhub/frontend
```

Then bump the version in `pyproject.toml` and publish a new release.

## License

MIT — see [LICENSE](LICENSE).
