Auth and cache¶
Credentials¶
connecto reads tokens from wherever they already live. See Credentials for the guided version, including what to do when something 401s.
cn.auth_status() # validates against every service
cn.auth_status(validate=False) # instant, presence-only
cn.set_token("cave", "<token>")
auth
¶
Credentials.
A facade, not a competing secret store. connecto reads tokens from wherever they already live and never breaks an existing caveclient or neuprint-python setup.
Resolution order, per service:
- an explicit
token=argument $CONNECTO_<SERVICE>_TOKEN- the service's own environment variable (
$NEUPRINT_APPLICATION_CREDENTIALS) ~/.connecto/config.toml- the service's native secret store (for CAVE, whatever
caveclientitself resolves)
Never the reverse - a token you already have working must keep working.
Two things beyond mere lookup live here, and they matter more than the lookup does:
- :func:
auth_errorsturns the 401s and 403s that the upstream libraries throw into a :class:~connecto.exceptions.ConnectoAuthErrorthat names the service, the server, which source the token came from, and the actual fix. - :func:
auth_statusvalidates - it asks each service who you are, rather than merely noting that some string exists. "Present" and "works" are different claims, and a status function that conflates them will cheerfully confirm the wrong hypothesis for someone whose token expired last week.
auth_status
¶
Which credentials connecto can see, whether they work, and who you are.
validate=True (the default) asks each service. It costs about a second, and
it is the whole point: a presence-only check reports found for a token that
expired last week, which is precisely the wrong answer for the one person who
ever runs this function.
Source code in connecto/auth.py
get_token
¶
Resolve a token for service ("cave", "neuprint", "clio", "seatable").
Source code in connecto/auth.py
set_token
¶
Store a token.
With write_native=True (the default) the token is written to both
connecto's config and the service's native location, so that caveclient,
cloud-volume and connecto can never disagree about which token is current -
which is the failure mode fafbseg papers over by reading from cloud-volume and
writing via caveclient.
Source code in connecto/auth.py
validate_token
¶
Ask the service whether a token actually works, and who it belongs to.
One cheap request (~0.4s). This is the difference between "a string exists" and "you are authenticated", and only the second one is worth reporting.
Source code in connecto/auth.py
auth_errors
¶
Translate upstream 401/403s into something the user can act on.
caveclient raises its own AuthException when no token is configured, and a
bare requests.HTTPError when the server rejects one. neuprint-python (and
connecto's own server_datasets) raise a bare 401 either way, so a missing
token and a wrong token are indistinguishable. Both become a
:class:ConnectoAuthError here, and the three kinds stay distinct.
Source code in connecto/auth.py
Servers¶
CAVE answers 503 for hours at a time while it builds a new materialization. These are
the two functions that make that survivable — and
When the server is down is the guide.
cn.server_status("flywire") # which services are answering, right now
cn.wait_until_available("flywire") # block until they are (explicitly — never automatic)
servers
¶
Server outages, and how to say something useful about them.
CAVE deployments answer 503 while they build a new materialization version. That
is not a blip: it runs for hours. caveclient retries such a request three times
over about 0.7 seconds (status_forcelist=(502, 503, 504), backoff_factor=0.1)
and then raises - so what reaches the user is a requests.HTTPError whose message
carries a page of nginx HTML, and whose only real content is the number 503.
Two questions matter at that moment, and the traceback answers neither.
Is it just this service, or is everything down? A materialization takes out the materialize service and leaves the chunkedgraph, skeleton and info services answering normally. A dead VPN takes out all four. Those have completely different fixes, and one cheap probe of each service tells you which you are looking at. That distinction is the main thing this module exists to provide, and connecto is well placed to provide it: it already knows every service its backends depend on.
How long? Nobody can say - but hours is the right order of magnitude, and
someone who knows that will go and do something else rather than hammer the server
in a while True. So connecto does not silently retry 503s for you: no honest
backoff outlasts a materialization, and a library that sleeps for hours pretending
to work is worse than one that tells you to come back later. What it offers instead
is :func:wait_until_available, which does the waiting explicitly, and says so.
One thing deliberately not attempted: guessing which of your calls would still
succeed. It is tempting to print "segmentation still works!", but it is not reliably
true - segmentation.update_ids needs a materialization timestamp, and every
version lookup goes through the materialize service, so even a pinned
ds.at(783) hits it. Claiming otherwise would send people down a path that dead-ends
one call later. This module reports which services answer, and what each one backs;
the mapping from that to your code is left honest rather than clever.
server_status
¶
Which servers behind dataset are answering, right now.
Probes each service that the dataset's backend depends on and reports what it said. The point is the shape of the result rather than any single row: one service down means that service is having a bad day (on CAVE, almost always a materialization in progress); every service down means the deployment or your own network is gone, which is a different problem with a different fix.
Parameters¶
dataset : str | Dataset
A registered dataset name ("flywire") or a dataset object.
timeout : int
Seconds to wait for each probe.
Returns¶
pandas.DataFrame
One row per service: service, server, status, http, seconds, backs.
status is OK, UNAVAILABLE, ERROR, UNREACHABLE or
NO AUTH - never a bare boolean, because "answered" and "answered
usefully" are different claims.
Examples¶
co.server_status("flywire") # doctest: +SKIP service server status http seconds backs 0 info global.daf-apis.com OK 200 0.31 client setup, ... 1 materialize prod.flywire-daf.com UNAVAILABLE 503 0.44 annotations, ...
Source code in connecto/servers.py
wait_until_available
¶
wait_until_available(dataset, *, service: str | None = None, interval: int = 300, timeout: int = 6 * 3600, verbose: bool = True)
Block until dataset's servers are serving again.
This is the loop you would otherwise write yourself around a materialization, with a poll interval that will not get you rate-limited. It is a separate, explicit call rather than an automatic retry precisely because it can take hours: a library that quietly sleeps that long, pretending to work, is worse than one that raises and lets you decide.
Parameters¶
dataset : str | Dataset
service : str, optional
Wait only for this service (e.g. "materialize"). By default,
waits until every service the dataset needs is answering.
interval : int
Seconds between polls. Floored at 30 - a materialization does not
finish in less time than that, and hammering a struggling server is
not neighbourly.
timeout : int
Give up after this many seconds. Default 6 hours.
Returns¶
pandas.DataFrame The final (healthy) status table.
Raises¶
TimeoutError
If the servers are still down when timeout expires.
Source code in connecto/servers.py
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
Cache¶
Keyed on (dataset, backend, version, query) — so a cached frame can never be served for
a different materialization. See Caching.
cache
¶
On-disk cache.
Layout::
~/.connecto/cache/<dataset>/<version>/<kind>_<hash>.feather
~/.connecto/cache/_http/<sha1(url)>.<ext>
Staleness is handled by what goes into the key, and that differs by what the source
is (see :func:connecto.sources.freshness and Annotations._table):
- A materialization is frozen, so a CAVE annotation table and a neuPrint version string are content-stable at a given version - the version in the key is enough, and a frozen dataset caches forever.
- The live sources are not: FlyTable (SeaTable) is edited daily and is
independent of any materialization, and a neuPrint instance can be re-curated under
the same version tag. For those a freshness token joins the key - SeaTable's
COUNT(*)/MAX(_mtime), neuPrint'slastDatabaseEdit- so it turns over exactly when the source does.CacheEntry.supersedethen drops the file it replaced, so a live source keeps one entry, not one per edit.
Frames are written as Feather, falling back to pickle for the ones Arrow cannot
hold - a SeaTable table with a mixed int/str object column, say. Without
that fallback such a frame silently never cached: the write raised, the entry was
deleted, and the next call re-downloaded the lot (aedes' FlyTable annotations,
17k rows, on every .annotations.get()). Pickle round-trips the frame exactly,
so a cache hit is byte-for-byte what a miss would have returned.
We cache annotation tables, ROI hierarchies and HTTP downloads - things that are
large, slow, and fetched over and over. We deliberately do not cache
connectivity or synapse queries by default: the key space is unbounded and it
would quietly fill the user's disk. Pass cache=True to opt in.
cache_dir
¶
clear
¶
size
¶
enabled
¶
download
¶
Download a URL into the cache and return the local path.
The filename is derived from a hash of the whole URL, not its basename, so
two sources that both serve annotations.tsv don't collide.
Source code in connecto/cache.py
Exceptions¶
exceptions
¶
Exceptions raised by connecto.
ConnectoError
¶
Bases: Exception
Base class for all connecto errors.
ConnectoServerError
¶
Bases: ConnectoError
The server could not serve the request - and it is not your fault.
Three kinds, because they call for three different responses:
UNAVAILABLE
HTTP 502/503/504. The service is deliberately refusing work. On CAVE this
is overwhelmingly a materialization in progress, which runs for hours,
not seconds. The single most useful thing to tell someone here is that
waiting in a tight loop will not outlast it - go and do something else -
which is the opposite of what a bare 503 traceback suggests.
ERROR
HTTP 500. Something broke server-side. Retrying rarely helps; this is worth
reporting to whoever runs the server.
UNREACHABLE
No HTTP response at all: DNS, TLS, timeout, no route. Very often this is the
user's own network rather than the server, and saying so saves them filing a
bug against the wrong project.
Source code in connecto/exceptions.py
ConnectoAuthError
¶
Bases: ConnectoError
A credential problem, stated in terms the user can act on.
Three kinds, because they have three different fixes:
MISSING
No token found anywhere. Go and get one.
INVALID
A token was found and the server rejected it (HTTP 401). The useful
thing to say here is which of the five possible sources it came from -
an env var, a config file, or one of three secret files - because that is
the one the user has to go and fix.
FORBIDDEN
The token is fine; you just aren't allowed near this resource (HTTP 403).
Usually a missing group membership. A new token will not help, and telling
someone to get one sends them down the wrong path entirely.
Source code in connecto/exceptions.py
CapabilityError
¶
Bases: ConnectoError, NotImplementedError, AttributeError
A dataset does not support a requested feature.
Subclasses AttributeError on purpose: this makes hasattr(ds, "segmentation")
return False for datasets without a segmentation, so duck-typed feature
detection works, while direct access still raises with a useful message.
NoSuchDatasetError
¶
NoSuchBodyError
¶
Bases: ConnectoError, KeyError
The requested neuron does not exist in the volume being read.
Also a KeyError, because that is what "this ID is not in there" already means
in Python, so except KeyError around a per-neuron loop keeps working.
NoSuchVersionError
¶
Bases: ConnectoError, ValueError
The requested version does not exist for this dataset.
AmbiguousVersionError
¶
Bases: ConnectoError, ValueError
No single version contains all the requested IDs.
Raised by version="auto" when the given IDs never co-existed in any
materialization.