Skip to main content

caselaw-access

Client SDK for the Case Law Explorer access service. One call decides authentication, endpoint scope, and rate limit, so a product does not implement any of them itself.

Install

pip install caselaw-access

For local development against unreleased changes, build a wheel and install it directly:

pip wheel . --no-deps -w dist
pip install dist/caselaw_access-*.whl

Prefer that over vendoring the wheel into a consumer repo: pip caches by version, so a rebuilt wheel with an unchanged version number is silently ignored.

Configuration

Everything comes from the environment:

Variable Meaning Default
ACCESS_SERVICE_URL Base URL of the access service. Empty disables all checks.
ACCESS_PROJECT Project slug registered in the access console. citations-api
ACCESS_SERVICE_TIMEOUT_SECONDS Per-check timeout. 5
ACCESS_FAIL_OPEN Waive quota for already-validated credentials when the service is unreachable. Never waives authentication. false
REQUIRE_AUTH / ACCESS_ENFORCE Master switch for enforcement. Unset on a connected deployment is an error, not an opt-out. unset
ACCESS_IDENTITY_CACHE_SECONDS How long a validated credential stays eligible for fail-open. 300

Checks are only sent when the service URL is set and enforcement is on. That combination is what lets a service run locally, and its tests run in CI, without the control plane.

Direct use

from caselaw_access import AccessClient, RateLimited, PermissionDenied

client = AccessClient.from_env()

try:
    decision = client.check(token, endpoint="echr", method="POST")
except RateLimited as exc:
    ...  # exc.status == 429, exc.headers() has Retry-After
except PermissionDenied as exc:
    ...  # exc.status == 403

An allowed Decision carries the resolved principal, the matched plan, the granted scopes, and the remaining quota. decision.headers() returns the X-RateLimit-Remaining-* headers to attach to the response.

http.server handlers

from caselaw_access.http_server import AccessGuard

guard = AccessGuard(send_error=my_json_writer)

class handler(BaseHTTPRequestHandler):
    @guard.protect("echr", methods=["POST"])
    def do_POST(self):
        ...

The decorated method runs only when the call is allowed; a denial is written before the body executes. The decision is attached to the handler as access_decision and access_principal, and the headers to emit as access_headers.

The verb reported to the service is the request's real method (handler.command). methods only shapes the CORS header on an error response.

Failure semantics

Situation Result
No or malformed Authorization header 401, service never called
Service answers 401 401 with the service's code
Service answers 403, or allowed: false with a scope error 403
Service answers 429, or allowed: false with rate_limit_exceeded 429 with Retry-After
Service unreachable, times out, or answers 4xx/5xx it cannot decide on 503, or allowed when ACCESS_FAIL_OPEN=true and this credential was recently validated for this endpoint

Connection failures are retried because the request provably never arrived. Read timeouts are not retried: the service may already have counted the call against the caller's quota, and a retry would charge them twice.

Behaviour is pinned by the shared conformance suite in ../contract, which this package runs in tests/test_conformance.py.

Releasing

Released by GitHub Actions via PyPI trusted publishing — no API token anywhere. Bump version in pyproject.toml, commit, push, then:

git tag python-v0.2.0 && git push origin python-v0.2.0

The workflow runs this suite, including the shared conformance cases, before uploading, and refuses to publish if the tag and pyproject.toml disagree. See ../README.md for the full procedure.

Framework adapters

The client itself is framework-agnostic — check() takes a token and returns a decision. These wrap it so guarding a route is one import and one decorator. Each imports its own framework, so install the extra you need:

pip install "caselaw-access[flask]"     # or [fastapi], or [django]

Flask@protect goes below the route decorator, so Flask registers the guarded function:

from caselaw_access.flask import protect

@app.get("/search")
@protect("search")
def search():
    ...

FastAPI / Starlette — as a route dependency, or as a parameter when the endpoint wants the decision:

from caselaw_access.fastapi import Access, requires

@app.get("/search", dependencies=[requires("search")])
async def search():
    ...

@app.get("/quota")
async def quota(access: Access("quota")):
    return {"remaining": access.remaining_minute}

Django — on a view, or through method_decorator for class-based views and DRF's APIView:

from caselaw_access.django import protect

@protect("search")
def search(request):
    ...

http.server — see caselaw_access.http_server, which additionally takes the writer your API already uses for error bodies.

Whichever you use, publish the scopes once at startup, after the modules holding your routes have been imported:

from caselaw_access import runtime

runtime.publish_manifest()

The registry is shared, so an app that mixes adapters — a Flask blueprint beside a plain http.server handler during a migration — publishes one manifest covering all of it.

On a denial the response is already written when the handler is skipped: status, JSON body, and the Retry-After and X-RateLimit-* headers. Those headers are the part hand-written glue reliably drops, because nothing fails when they are missing — the caller simply never learns when to retry.

What fail-open does, and does not, do

/v1/check answers two questions at once: who is this caller, and do they have quota left. Waiving both during an outage means any non-empty bearer string is admitted — an authentication bypass dressed up as an availability feature.

So ACCESS_FAIL_OPEN=true waives quota only, and only for a credential this process has already watched the service accept for that same endpoint within ACCESS_IDENTITY_CACHE_SECONDS. Anything else is denied exactly as if the policy were fail-closed.

In practice: an outage does not interrupt traffic that was already flowing, and does not let anything new in. The cost is that a credential revoked during an outage keeps working until its cache entry expires — set ACCESS_IDENTITY_CACHE_SECONDS=0 to disable the mechanism entirely, which makes ACCESS_FAIL_OPEN a no-op.

The pair is keyed on credential and endpoint on purpose. Keying on the credential alone would let an outage promote a caller onto an endpoint they were never entitled to, turning a quota waiver into an authorization bypass.

Enforcement must be stated

Leaving REQUIRE_AUTH / ACCESS_ENFORCE unset while a project and a secret are configured raises access_enforcement_not_configured rather than running inert. A project secret is issued once, by hand, for a specific deployment, so carrying one is unambiguous intent — an absent flag is a forgotten variable, and guessing wrong means enforcing nothing while looking fully configured.

Set it explicitly to false to run inert on purpose.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

caselaw_access-0.3.1.tar.gz (28.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

caselaw_access-0.3.1-py3-none-any.whl (24.6 kB view details)

Uploaded Python 3

File details

Details for the file caselaw_access-0.3.1.tar.gz.

File metadata

  • Download URL: caselaw_access-0.3.1.tar.gz
  • Upload date:
  • Size: 28.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for caselaw_access-0.3.1.tar.gz
Algorithm Hash digest
SHA256 afde1fc327de26e3f982e6519dd959e625f4cb99eab4579fe450b1693167ce62
MD5 4437bac12e87470901497ee30b298083
BLAKE2b-256 ebd49f5ad508ef11695cd7c2478df8b69580d6045717de92cead7a31a1685f8a

See more details on using hashes here.

Provenance

The following attestation bundles were made for caselaw_access-0.3.1.tar.gz:

Publisher: publish-python.yml on davidwickerhf/caselaw-access

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file caselaw_access-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: caselaw_access-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 24.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for caselaw_access-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 50250d9931ed2ecb0c97ceb2987d70f9de2999dcacb5e3a94448e0e082861581
MD5 1854f1e37f14368c752788662ea4952a
BLAKE2b-256 8eb32a064aef3d9e1a1efe2db69404f2db5aa304a36235f737d64e418da8bd75

See more details on using hashes here.

Provenance

The following attestation bundles were made for caselaw_access-0.3.1-py3-none-any.whl:

Publisher: publish-python.yml on davidwickerhf/caselaw-access

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.0

2 files

This release

0.3.1 This release

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page