repics
EPICS Channel Access and pvAccess for Python, built on epics-rs. No libca, no libpvxs: the protocol stacks are the Rust crates, compiled into one extension module.
from repics import ca
ca.caput("SIM:ao", 2.5)
v = ca.caget("SIM:ai", form="ctrl")
print(v, v.units, v.severity, v.timestamp)
with ca.camonitor("SIM:cnt", print):
...
from repics.ca.asyncio import caget, camonitor
async def main():
v = await caget(["SIM:ai", "SIM:long"])
sub = await camonitor("SIM:cnt", print)
Every read returns an augmented value: a float/int/str/numpy.ndarray
subclass carrying name, status, severity, timestamp, raw_stamp, and
for form="ctrl" the units, precision, limits and enum strings.
Channel Access
caget, caput, camonitor, cainfo and connect take one PV name or a
sequence and return a result of the same shape; a sequence is worked on
concurrently, connect included, against one timeout. A failure raises
CaError (CaTimeout, CaDisconnected); with throw=False it is returned
instead as a CaNothing, whose ok is False and whose errorcode is the
libca ECA_* status. cainfo returns a CAInfo.
formpicks the metadata class:"plain","sts","time"(default),"gr","ctrl".datatypeoverrides the wire type:str,int,float, a numpy dtype, aDBR_*code, orDBR_CHAR_STR/DBR_CHAR_BYTES/DBR_CHAR_UNICODEto read a char waveform as text,DBR_ENUM_STRto read an enum as its label.countis 0 for the server's current element count, negative for the full native count, else a cap.camonitor(pv, callback, ...)returns aSubscriptionat once and connects in the background;connect_timeoutreports aCaNothingwithECA_TIMEOUTif it passes, and the monitor keeps waiting. Callbacks run on one dispatcher thread (ca) or one task per event loop (ca.asyncio), in arrival order. Withall_updates=False(default) updates that queued while the callback ran collapse into the latest anddropped_callbackscounts them.notify_disconnect=Truedelivers aCaNothingwithECA_DISCONNon disconnect; the monitor resumes on reconnection.repics.pv.PVis a pyepics-shaped object (get,put,add_callback,auto_monitor,char_value,units, ...) over one subscription; it raises on failure like the rest of the package.
A CA server hosts bare value channels — caget, caput and camonitor
reach them, but there is no record, no RPC and no per-channel connect edge,
so the only handler is put:
from repics.ca.server import SharedPV, Server
pv = SharedPV(initial=1.0)
@pv.put
def onput(pv, op):
pv.post(op.value()) # accept: publish to monitors
op.done() # or op.done(error="...") to reject
Server.forever(providers=[{"X:val": pv}])
A SharedPV with no handler refuses every put. post() fans a new value out
to monitors; values are plain scalars, strings, lists or 1-D numpy arrays.
repics.ca.server.asyncio.SharedPV is the asyncio flavour, whose handler may
be a coroutine. Handlers run on Python-owned threads (or the event loop),
never on the network runtime.
pvAccess
The API follows p4p: Context,
Value/Type, the nt wrappers, and SharedPV/StaticProvider/Server.
from repics.pva import Context
from repics.pva.nt import NTScalar, NTURI
with Context("pva") as ctxt:
v = ctxt.get("SIM:ai") # augmented, as for CA
ctxt.put("SIM:ao", 2.5)
r = ctxt.rpc("SIM:sum", NTURI([("a", "d"), ("b", "d")]).wrap("SIM:sum", kws={"a": 1, "b": 2}))
with ctxt.monitor("SIM:cnt", print, notify_disconnect=True):
...
from repics.pva.server import SharedPV, Server
pv = SharedPV(nt=NTScalar("d"), initial=1.0)
@pv.put
def onput(pv, op):
pv.post(op.value())
op.done()
Server.forever(providers=[{"SIM:ao": pv}])
repics.pva.asyncio.Context and repics.pva.server.asyncio.SharedPV are
the asyncio flavours; handlers there may be coroutines. Put and RPC handlers
run on Python-owned threads (or the event loop), never on the network runtime,
and a slow monitor consumer squashes updates in Rust instead of queueing
Python objects. NTNDArray reads are zero-copy views of the received buffer.
Performance
The extension owns one tokio runtime with one worker thread (override with
REPICS_WORKERS); every blocking call releases the GIL and waits for its
future, every ca.asyncio call returns a future that is already done when the
answer is in hand. Metadata is attached to a value lazily, list operations
run in Rust as one concurrent batch, and all monitors of a front end feed
one bounded queue that is drained in batches, so a callback costs one queue
pop rather than one thread wake-up.
bench/bench_ca.py runs repics, pyepics and aioca in separate processes
against the same softioc-rs. On a Xeon Gold 6542Y, pinned to four cores
with the SMT siblings kept busy so the cores hold their clock
(--pin 2,3,4,5 --hot; unpinned, schedutil parks the ping-ponging
threads at 800 MHz and every library halves):
| repics | repics.ca.asyncio | pyepics | aioca | |
|---|---|---|---|---|
| get, median / p99 | 35 / 42 us | 72 / 103 us | 85 / 107 us | 59 / 69 us |
| get 10 000-double waveform | 166 / 183 us | 161 / 179 us | 182 / 269 us | 109 / 171 us |
| get list of 100 PVs, one call | 1167 / 1201 us | 932 / 976 us | 3284 / 3369 us | 3043 / 3633 us |
| put wait=True | 39 / 44 us | 56 / 63 us | 165 / 180 us | 66 / 77 us |
| put wait=False | 160 439 /s | 144 256 /s | 13 704 /s | 83 536 /s |
| monitor, 1 PV, CPU per callback | 8.3 us | 17.6 us | 32.2 us | 24.2 us |
| monitor, 100 PVs, CPU per callback | 10.3 us | 22.2 us | 19.9 us | 23.2 us |
Monitor rows are under a 20 000-put storm from a separate writer process; every client received 20 099–20 100 of the 20 100 events on the 100-PV row.
bench/bench_pva.py does the same for pvAccess. Client rows run each
library in its own process against one repics thread SharedPV server;
server rows run each server flavour in its own process, measured by the
p4p thread client. Same machine, same pinning:
| client | repics | repics.asyncio | p4p | p4p.asyncio |
|---|---|---|---|---|
| get, median / p99 | 47 / 54 us | 69 / 92 us | 99 / 131 us | 99 / 132 us |
| get 10 000-double array | 90 / 101 us | 105 / 136 us | 122 / 184 us | 122 / 143 us |
| get list of 100 PVs, one call | 2235 / 3628 us | 4108 / 6173 us | 3980 / 4349 us | 4630 / 5110 us |
| put wait=True | 145 / 158 us | 194 / 238 us | 150 / 186 us | 156 / 198 us |
| put wait=False | 7111 /s | 5145 /s | 6506 /s | 6367 /s |
| monitor, 1 PV, CPU per callback | 30.6 us | 61.7 us | 34.1 us | 41.3 us |
| monitor, 100 PVs, CPU per callback | 34.6 us | 71.3 us | 54.9 us | 59.3 us |
| server | repics | repics.asyncio | p4p | p4p.asyncio |
|---|---|---|---|---|
| get, median / p99 | 94 / 115 us | 99 / 128 us | 99 / 133 us | 94 / 116 us |
| put wait=True, through the put handler | 147 / 170 us | 187 / 239 us | 153 / 189 us | 148 / 177 us |
| post storm, 1 PV: delivered of 20 001, server CPU per post | 15 842, 13.4 us | 17 161, 13.8 us | 8 673, 5.9 us | 8 318, 5.7 us |
| post storm, 100 PVs: delivered of 20 100, server CPU per post | 18 390, 12.6 us | 19 571, 12.5 us | 20 100, 6.7 us | 20 098, 6.6 us |
pvAccess has no fire-and-forget put: every put is a completed round trip,
and with the default get=True the current value is read first. repics
and pvxs both do this as one two-phase put operation, the readback riding
the put's own channel op, so the put rows are on par. The client monitor
rows are under a storm from a separate repics writer running four threads
of put(wait=True), about 14 000 puts/s, and every client received
19 857 or more of the 20 001 events, so the CPU column is the comparison.
The server storm is one thread posting as fast as post() returns; the
repics server delivers about twice as many posts as pvxs and spends
about 13 us of CPU per post, delivery included, where pvxs spends about
6 us and, on one PV, squashes away more than half of them. Medians of the
blocking repics client moved between 44 and 79 us on get across runs
of the same command (thread placement inside the four cores); each table
is one run.
Reference documentation: docs/index.md.
Building
pip install maturin
maturin develop # into the active environment
pytest # needs `softioc-rs` on PATH or REPICS_SOFTIOC=<path>
softioc-rs is cargo install epics-ca-rs --bin softioc-rs.
Status
Channel Access client and server, blocking and asyncio. pvAccess client
and server, blocking and asyncio; tested against p4p on both sides of
the wire (pytest tests/pva needs p4p installed).
Metadata
Release files for repics 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| repics-0.2.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| repics-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| repics-0.2.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64), macOS 10.12+ x86-64 | Details |
Total release size: 23.4 MB
Release files / repics-0.2.0-cp310-abi3-win_amd64.whl
| Download URL | repics-0.2.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 6.3 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
adaa2b549be8ec5d11a3084797ee9a86d418e6b7170b591823b982f8c8b786a1
|
|
BLAKE2b-256 checksum How to use checksums |
0f6ebae271c4db5e3e5e17563c8b3a89b7ab3114752d36287854475b72d48eb4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency logRelease files / repics-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | repics-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 6.0 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
5daf1e3fd18e5d1de9348e8fb6638536e9629453e5881409e54c6f5d4c89dd64
|
|
BLAKE2b-256 checksum How to use checksums |
13138748b4d887146308d83a421aa0cc2b90178f35e13eacd293652bf2fe542a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency logRelease files / repics-0.2.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | repics-0.2.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
|---|---|
| Size | 11.0 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
3295b39283ccd0989690f04a1a2d1a474f4ab5061e36e0cb5ca8c43c3df160a5
|
|
BLAKE2b-256 checksum How to use checksums |
340ff2670fc260057ffeaf5060d3fc96400926c7691146751ea104a0cede7b4b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency log