Metadata-Version: 2.4
Name: rftools-io
Version: 0.4.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.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.12
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.17,>=0.16; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: jsonschema>=4.18; 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 (5 calls/month) without copying it anywhere:

```bash
rftools login
```

This prints a page and a code. Open the page, sign in or sign up, and approve the
code. The key is saved to `~/.config/rftools/credentials.json` (`%APPDATA%\rftools\`
on Windows, readable by you only), and every later `rftools.Client()` and `rftools`
command reads it from there. The key is never printed. From Python, the same thing is
`rftools.Client().login()`.

Or set a key you already have. The variable takes precedence over the saved file:

```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, ...}
```

## Solving for an Input

Most calculators go forward: inputs to outputs. `solve()` goes the other way — given
every other input and a target value for one output, it finds the value of one input
that reaches it, in one metered call.

```python
result = rftools.Client(api_key="rfc_live_xxx").solve(
    "microstrip-impedance",
    {"substrateHeight": 1.6, "dielectricConstant": 4.2, "copperThickness": 35},
    "traceWidth", "impedance", 50.0,
    grid=0.001,   # round to a manufacturing grid; omit for the unrounded solution
)
print(result.value, result.reached)   # 3.052 True
print(result.result["impedance"])    # the forward result at exactly that width
print(result.result.provenance.formula_ref)
```

`grid` and `range` (a `(low, high)` pair narrowing the search) are each sent only when
given. When no value in the search range reaches the target, the call still succeeds:
`.reached` is `False` and `.value` is the closest point found, with its own forward
`.result`. See [Error Handling](#error-handling) below — `solve()` raises the same typed
errors as `calculate()`.

## 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
```

A key is sent as `Authorization: Bearer <key>` and as `X-API-Key`. The service accepts
either on every route.

## Usage

The monthly allowance belongs to your account. Every key on the account draws on it.

```python
usage = client.usage()             # GET /v1/usage, which is never metered
print(usage.used, "of", usage.allowance, "used;", usage.remaining, "left")
print("resets", usage.reset_at)
for key in usage.keys:
    print(key.key_id, key.label, key.used)

result = client.calculate("vswr-return-loss", {"vswr": 2.5})
print(result.usage.remaining)      # from this response's X-Usage-* headers
print(client.last_usage)           # the latest metered response's, refusals included
```

`batch()` results carry `.usage` too, and so does a job submitted with a key
(`job.usage`).

## Provenance

A calculator result says how it was computed. `provenance` is None until the service
sends it.

```python
p = result.provenance
print(p.method, p.version)          # calculator:microstrip-impedance api@4ce2c2e0a1b2
print(p.formula_ref)                # Hammerstad & Jensen (1980); Wadell, ...
for a in p.assumptions:
    print(a.code, a.text)
print(p.valid_range.status)         # inside | outside | unknown
print(p.valid_range.outside)        # the inputs outside their stated bounds
```

`BatchResultItem.provenance` is the same per item. A member missing from an older
result reads as None and is listed in `p.missing`. The contract is vendored as
`rftools/schemas/result-provenance.schema.json`.

## 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'

# Solve one input for a target output (one metered call)
rftools solve microstrip-impedance --for traceWidth --target impedance=50 \
    --grid 0.001 --param substrateHeight=1.6 --param dielectricConstant=4.2 \
    --param copperThickness=35

# Narrow the search range instead of (or as well as) rounding to a grid
rftools solve microstrip-impedance --for traceWidth --target impedance=50 \
    --range 0.1 10.0 --param substrateHeight=1.6

# 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

# Get an API key by approving a code on rftools.io (saved; never printed)
rftools login
rftools login --client-id my-tool --no-browser

# 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"{e.used} of {e.limit} calls used; resets {e.reset_at}. Plans: {e.upgrade_url}")
except RateLimitError as e:
    print(f"More than {e.limit} requests in {e.window_seconds}s. Retry after {e.retry_after}s")
except AuthError as e:
    print(f"Invalid or missing API key. Get one: {e.key_url or 'rftools login'}")
except NotFound:
    print("Unknown calculator slug")
except ValidationError as e:
    print(f"Bad inputs: {e.failures}")
```

| Exception | When |
|---|---|
| `AuthError` | Missing, unknown or revoked API key (HTTP 401); `.key_url` is where to get one when the service names it. Also HTTP 403, and an upload attempted with no key, which is raised locally. A spent allowance is never an `AuthError` |
| `QuotaError` | The account's monthly allowance is spent (HTTP 402, on every metered route). Carries `.reason`, `.limit`, `.used`, `.reset_at`, `.upgrade_url`, `.overage_url`, `.retry_after` |
| `RateLimitError` | Too many requests too fast (HTTP 429). Carries `.limit`, `.window_seconds`, `.retry_after` |
| `LoginError` | `login()` got no key; `.error` is `access_denied` or `expired_token` |
| `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. Every exception raised for an HTTP
response carries `.status_code` and `.error_kind` (the service's `errorKind`). Pre-0.2
names (`RFToolsError`, `NotFoundError`) still work as aliases.

Before the API's metering release, a spent allowance on `POST /v1/calculate` came back
as HTTP 401. It is now 402 on every metered route, so it raises `QuotaError`.

## 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` or run `rftools login`. 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).
