Skip to content

API Reference

The full public surface, grouped by area. Everything here is importable from jero (test helpers from jero.testing).

App & wiring

Bases: ABC

Subclass and override wire to open resources and include resources/endpoints.

The app owns the two exit stacks. Parameterize with a factory class — class MyApp(BaseApp[MyFactory]) — and the app builds it at construction, injecting the stacks the factory's __init__ names (es for the ExitStack, aes for the AsyncExitStack). The built factory is then self._factory (typed as MyFactory) inside wire, and any resource it registers on those stacks is closed at shutdown.

Pass factory= to supply a prebuilt factory instead of building one — the seam for tests, which inject a create_autospec stand-in (MyApp(factory=mock_factory)) so the real services are never constructed.

Reverse-routed Location / Link URLs are relative unless the environment sets JERO_BASE_URL (a static public origin) or JERO_TRUST_FORWARDED (rebuild the origin per request from X-Forwarded-*); see :func:_forwarded_config_from_env.

Source code in jero/core.py
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
class BaseApp[FactoryT = None](ABC):
    """Subclass and override ``wire`` to open resources and include resources/endpoints.

    The app owns the two exit stacks. Parameterize with a factory class —
    ``class MyApp(BaseApp[MyFactory])`` — and the app builds it at construction,
    injecting the stacks the factory's ``__init__`` names (``es`` for the
    ExitStack, ``aes`` for the AsyncExitStack). The built factory is then
    ``self._factory`` (typed as ``MyFactory``) inside ``wire``, and any resource
    it registers on those stacks is closed at shutdown.

    Pass ``factory=`` to supply a prebuilt factory instead of building one — the
    seam for tests, which inject a ``create_autospec`` stand-in
    (``MyApp(factory=mock_factory)``) so the real services are never constructed.

    Reverse-routed ``Location`` / ``Link`` URLs are relative unless the environment sets
    ``JERO_BASE_URL`` (a static public origin) or ``JERO_TRUST_FORWARDED`` (rebuild the
    origin per request from ``X-Forwarded-*``); see :func:`_forwarded_config_from_env`.
    """

    def __init__(self, *, factory: FactoryT | None = None) -> None:
        self.__static: _StaticRoutes = {}
        self.__dynamic: _DynamicRoutes = {}
        self.__websocket_static: dict[str, _WebSocketHandler] = {}
        self.__websocket_dynamic: dict[int, list[_WebSocketPattern]] = {}
        self.__allowed: _AllowedMethods = {}
        self.__allow_cache: dict[str, bytes] = {}
        self.__decoders: dict[type[Struct], Decoder[Struct]] = {}
        self.__operations: list[OperationSpec] = []  # captured for the OpenAPI document
        self.__openapi: _OpenAPIConfig | None = None  # set by _include_openapi, built at __finalize
        base_url, trust_forwarded = _forwarded_config_from_env()
        self.__reverser = _Reverser(base_url=base_url, trust_forwarded=trust_forwarded)
        self.__exceptions = _ExceptionHandlers(self.__reverser)
        # The tail for responses no route owns (unrouted 404, 405): the app-wide CORS
        # default and global middleware headers, filled at __finalize.
        self.__app_tail = _RouteTail()
        self.__includes: list[_IncludeRecord] = []
        self.__cors_default: CompiledCORS | None = None  # set by _include_cors
        # route handler -> its resolved CORS policy, for answering preflights; built
        # at __finalize once inheritance is resolved.
        self.__route_cors: dict[_Handler, CompiledCORS] = {}
        self.__middleware: list[CompiledMiddleware] = []  # global, in registration order
        # The pre-routing middleware machinery (global intercept table + observes);
        # stays None unless global middleware defines those hooks, so the disabled
        # per-request cost is one attribute load.
        self.__pre: _GlobalMiddleware | None = None
        self.__stack = ExitStack()
        self.__astack = AsyncExitStack()
        self.__factory: FactoryT = factory if factory is not None else self.__make_factory()

    @property
    def _factory(self) -> FactoryT:
        """The built factory, read inside ``wire``. Set once at construction (read-only)."""
        return self.__factory

    def _enter[T](self, cm: AbstractContextManager[T]) -> T:
        """Open a sync context manager, closed at shutdown in reverse order."""
        return self.__stack.enter_context(cm)

    async def _aenter[T](self, cm: AbstractAsyncContextManager[T]) -> T:
        """Open an async context manager, closed at shutdown in reverse order."""
        return await self.__astack.enter_async_context(cm)

    def __decoder(self, struct_type: type[Struct]) -> Decoder[Struct]:
        """The reusable typed JSON decoder for ``struct_type``, built once per app.

        Decoders are keyed by type, so models shared across handlers (a ``WidgetIn``
        used by both ``create`` and ``update_full``) share one decoder. Populated only at
        wiring time; the binder holds the resolved decoder, so the request path does
        no lookup.
        """
        if struct_type not in self.__decoders:
            self.__decoders[struct_type] = Decoder(struct_type)
        return self.__decoders[struct_type]

    def __resolve_factory_type(self) -> type | None:
        """The factory class from ``BaseApp[...]``, or None if unparameterized."""
        for base in get_original_bases(type(self)):
            if get_origin(base) is BaseApp:
                args = get_args(base)
                if args and isinstance(args[0], type) and args[0] is not type(None):
                    return args[0]
        return None

    def __make_factory(self) -> FactoryT:
        factory_type = self.__resolve_factory_type()
        if factory_type is None:
            return cast("FactoryT", None)
        return cast("FactoryT", _instantiate_factory(factory_type, self.__stack, self.__astack))

    @abstractmethod
    async def wire(self) -> None:
        """Override to open resources (via ``_enter`` / ``_aenter``) and include them.

        Runs once at startup. Anything entered via the helpers is torn
        down (in reverse order) at shutdown.

        Abstract: every ``BaseApp`` subclass must implement it. A subclass that
        omits it is flagged at its instantiation site by the type checker.
        """

    def __register(self, method: HTTPMethod, segments: list[_Segment], handler: _Handler) -> None:
        params = tuple((i, value) for i, (is_param, value) in enumerate(segments) if is_param)
        if not params:
            route_path = "/".join(value for _, value in segments)
            if (method, route_path) in self.__static:
                raise WiringError(f"{method} {route_path} is already registered")
            self.__static[(method, route_path)] = handler
            self.__allowed.setdefault(route_path, []).append(method)
            return

        statics = tuple((i, value) for i, (is_param, value) in enumerate(segments) if not is_param)
        bucket = self.__dynamic.setdefault((method, len(segments)), [])
        if any(pattern.statics == statics for pattern in bucket):
            raise WiringError(f"{method} {_template_str(segments)} is already registered")
        bucket.append(_Pattern(statics, params, handler))

    def __register_websocket(self, segments: list[_Segment], handler: _WebSocketHandler) -> None:
        params = tuple((i, value) for i, (is_param, value) in enumerate(segments) if is_param)
        if not params:
            route_path = "/".join(value for _, value in segments)
            if route_path in self.__websocket_static:
                raise WiringError(f"WebSocket {route_path} is already registered")
            self.__websocket_static[route_path] = handler
            return
        statics = tuple((i, value) for i, (is_param, value) in enumerate(segments) if not is_param)
        bucket = self.__websocket_dynamic.setdefault(len(segments), [])
        if any(pattern.statics == statics for pattern in bucket):
            raise WiringError(f"WebSocket {_template_str(segments)} is already registered")
        bucket.append(_WebSocketPattern(statics, params, handler))

    @staticmethod
    def __check_user_source(
        resource_cls: type,
        name: str,
        sources: Sources,
        auth: _CompiledAuth | None,
    ) -> None:
        """Validate a handler's ``user`` annotation against the route's authenticator — that
        there is one, that the Struct matches, and that its optionality agrees.

        A handler on an anonymous-accepting route must declare ``user``: with nothing to
        check, a handler that ignores the auth result would serve anonymous callers with no
        sign of it at the mount or in the signature. Behind a gating authenticator, omitting
        ``user`` stays fine — the gate has already run."""
        user_type = sources.user
        if user_type is None:
            if auth is not None and auth.reports_absence:
                raise WiringError(
                    f"{resource_cls.__name__}.{name} declares no 'user', but "
                    f"{auth.owner}.authenticate returns {auth.returns.__name__} | None, so "
                    f"this route serves anonymous callers — declare "
                    f"'user: {auth.returns.__name__} | None' and handle None, or mount it "
                    f"behind an authenticator that returns {auth.returns.__name__} to gate it",
                )
            return
        if auth is None:
            raise WiringError(
                f"{resource_cls.__name__}.{name} declares 'user' but no auth was given",
            )
        if not issubclass(auth.returns, user_type):
            raise WiringError(
                f"{resource_cls.__name__}.{name}: 'user' expects {user_type.__name__} "
                f"but {auth.owner}.authenticate returns {auth.returns.__name__}",
            )
        if auth.reports_absence and not sources.user_optional:
            raise WiringError(
                f"{resource_cls.__name__}.{name}: 'user' must be annotated "
                f"'{user_type.__name__} | None' — {auth.owner}.authenticate returns "
                f"'{user_type.__name__} | None', so a caller may arrive anonymous",
            )
        if sources.user_optional and not auth.reports_absence:
            raise WiringError(
                f"{resource_cls.__name__}.{name}: 'user' must be annotated "
                f"'{user_type.__name__}' — {auth.owner}.authenticate returns "
                f"'{user_type.__name__}', so an unauthenticated caller never reaches the "
                f"handler; return '{user_type.__name__} | None' from it to accept anonymous "
                f"callers",
            )

    def __include(
        self,
        obj: Resource | Endpoint,
        methods: dict[str, _Verb],
        *,
        auth: "Auth[Any, Any] | CookieAuth[Any, Any] | HybridAuth[Any, Any, Any] | None",
        cors: CORS | None,
        middleware: Sequence[object],
    ) -> None:
        cls = type(obj)
        if cors is not None and not isinstance(cast("object", cors), CORS):
            raise WiringError(
                f"{cls.__name__}: cors= must be a CORS policy (or CORS.OFF), "
                f"got {type(cors).__name__}",
            )
        # Explicit policies are validated and compiled here, at the include call —
        # only what depends on other registrations (inheriting an omitted cors=, the
        # global middleware every route picks up) waits for __finalize.
        cors_off = cors is CORS.OFF
        compiled_cors = CompiledCORS(cors) if cors is not None and not cors_off else None
        compiled_middleware = tuple(CompiledMiddleware(m) for m in middleware)
        # One shared tail per include: every route (and sender) the include registers
        # holds this instance, and __finalize fills it in place once coverage is known.
        tail = _RouteTail()
        record = _IncludeRecord(
            tail=tail,
            routes=[],
            cors=compiled_cors,
            cors_off=cors_off,
            middleware=compiled_middleware,
        )
        path = getattr(cls, "path", None)
        if path is None:
            raise WiringError(
                f"{cls.__name__}: no path — declare it on the class, "
                f"e.g. `class {cls.__name__}(..., path='/...')`.",
            )
        template = _parse_template(path)
        # The authenticator's declared return type is the policy: `-> TUser | None` accepts
        # anonymous callers, `-> TUser` gates. Never inferred from anything else.
        compiled_auth = _CompiledAuth(auth) if auth is not None else None
        auth_mode: AuthMode = None
        if compiled_auth is not None:
            auth_mode = "optional" if compiled_auth.reports_absence else "required"
        # An authed route with no declared scheme derives one from what authenticate()
        # binds — see _derive_security_scheme; it may derive nothing, which is only a
        # problem for an app that calls _include_openapi (operation_input raises there).
        security_scheme: SecurityScheme | None = None
        if compiled_auth is not None:
            declared: object = getattr(type(auth), "openapi_security", None)
            if declared is None:
                security_scheme = _derive_security_scheme(compiled_auth)
            elif isinstance(declared, SecurityScheme):
                security_scheme = declared
            else:
                raise WiringError(
                    f"{type(auth).__name__}: openapi_security must be SecurityScheme, "
                    f"got {type(declared).__name__}",
                )

        registered = False
        for name, verb in methods.items():
            fn = getattr(obj, name, None)
            if fn is None:
                continue
            sources = _bind_sources(cls, name, fn, verb, self.__decoder)
            self.__check_user_source(cls, name, sources, compiled_auth)
            segments = _route_segments(
                cls, name, template, sources.path, extends_path=verb.extends_path
            )
            # A NoContent/Created/Accepted return fixes its own status regardless of the
            # verb's default (204/201/202); a union's members carry their own already.
            status = _effective_status(sources.return_kind, verb.success_status)
            handler = _Route(
                fn,
                status,
                sources=sources,
                auth=compiled_auth,
                reverser=self.__reverser,
                exceptions=self.__exceptions,
                tail=tail,
            )
            self.__register(verb.method, segments, handler)
            record.routes.append((verb.method, handler))
            self.__reverser.register(
                fn.__func__, cls.ref, name, _RouteRef(tuple(segments), sources.path)
            )
            self.__operations.append(
                OperationSpec(
                    path=_template_str(segments),
                    method=verb.method.lower(),
                    success_status=status,
                    sources=sources,
                    auth_mode=auth_mode,
                    security_scheme=security_scheme,
                    class_meta=cls.meta,
                    op_meta=getattr(cls, f"meta_{name}", None),
                    operation_id_default=f"{cls.__name__}_{_camel(name)}",
                    auth_owner=compiled_auth.owner if compiled_auth is not None else None,
                )
            )
            registered = True

        if not registered:
            raise WiringError(f"{cls.__name__} defines none of: {', '.join(methods)}")
        self.__includes.append(record)

    def _include_exception_handler[
        E: Exception,
    ](self, handler: ExceptionHandler[E]) -> None:
        """Register a structurally typed custom exception handler.

        The exception, JSON body, and typed-header types are inferred from the concrete
        ``handle_exception`` signature and validated once during wiring.
        """
        self.__exceptions.register(handler)

    def _include_error_adapter(self, adapter: ErrorBodyAdapter[Any]) -> None:
        """Replace the Problem family's wire body app-wide with ``adapter``'s composition.

        Call inside ``wire``, at most once. Every Problem-family error — the framework's
        built-ins (404/405/422/500, …) and your own ``HTTPError`` subclasses, including
        those returned by exception handlers — is rendered through ``adapter.compose``
        instead of RFC 9457 Problem Details, and the derived OpenAPI error responses
        document the adapter's body. ``StructHTTPError``\\ s render themselves.
        """
        # The isinstance guards untyped callers; cast first so it isn't statically vacuous.
        if not isinstance(cast("object", adapter), ErrorBodyAdapter):
            raise WiringError(
                "_include_error_adapter requires an ErrorBodyAdapter instance, "
                f"got {type(adapter).__name__}",
            )
        if getattr(type(adapter), "body_type", None) is None:
            raise WiringError(
                f"{type(adapter).__name__} never bound a concrete body Struct; "
                "parameterize the class: ErrorBodyAdapter[YourBody]",
            )
        if self.__exceptions.adapter is not None:
            existing = type(self.__exceptions.adapter).__name__
            raise WiringError(
                f"an error body adapter ({existing}) is already registered; an app has at most one",
            )
        self.__exceptions.adapter = adapter

    def _include_cors(self, cors: CORS) -> None:
        """Serve cross-origin browser callers app-wide with one default :class:`CORS` policy.

        Call inside ``wire``, at most once (order among the ``include_*`` calls doesn't
        matter). Every include inherits the policy unless it passes its own ``cors=`` —
        a different policy overrides it, ``CORS.OFF`` removes it. Skipping
        ``_include_cors`` entirely means no CORS anywhere except includes that opt in
        with their own ``cors=``.

        The policy is compiled at wiring: a wildcard origin becomes constant header
        pairs in covered routes' responses (free per request), an origin allow-list one
        set lookup + origin echo. Preflights (``OPTIONS`` with
        ``Access-Control-Request-Method``) are answered per (path, requested method) on
        the existing OPTIONS branch, so two verbs on one path may carry two policies.
        Error responses carry the failing route's pairs — a browser page must be able
        to *read* the 401/422 problem body; unrouted 404s carry this app default.
        """
        if not isinstance(cast("object", cors), CORS):
            raise WiringError(f"_include_cors requires a CORS policy, got {type(cors).__name__}")
        if cors is CORS.OFF:
            raise WiringError(
                "CORS.OFF is the per-include opt-out; an app that wants no CORS default "
                "simply does not call _include_cors",
            )
        if self.__cors_default is not None:
            raise WiringError("a CORS default is already registered; an app has at most one")
        self.__cors_default = CompiledCORS(cors)

    def _include_middleware(self, middleware: object) -> None:
        """Register one middleware app-wide: every route (current and later includes) is
        covered, and its intercepts run *pre-routing* — they can answer requests no
        route serves, which is how an OPTIONS-scoped intercept answers preflights for
        paths that would 404.

        The middleware is a structurally typed object — no base class; its hooks
        (``response_headers`` attribute or method, ``intercept`` + ``intercept_methods``,
        ``observe``) are introspected and validated here, fail-loud, and compiled into
        the covered routes at wiring (see :class:`~jero.CORS` for the same idea as a
        built-in). Register order is run order, and globals run before include-scoped
        middleware. Only what a middleware defines costs anything: a constant
        ``response_headers`` is baked into route header blocks for free, an off-scope
        verb never reaches an ``intercept``.
        """
        self.__middleware.append(CompiledMiddleware(middleware))

    # Three overloads rather than one three-way-union signature: a Union of several
    # generic Protocols, each binding its own subset of type parameters, is more than
    # mypy's structural-match inference can solve in one shot (it falls back to `Never`
    # for every parameter and rejects any concrete auth class); overloads give each
    # shape its own clean inference, and every other checker (pyright/ty/zuban/pyrefly)
    # handles both forms equally well. Runtime dispatch is unaffected — the
    # undecorated implementation below is what actually runs.
    @overload
    def _include_resource[THeaders: Struct, TUser: Struct](
        self,
        resource: Resource,
        *,
        auth: Auth[THeaders, TUser] | None = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None: ...
    @overload
    def _include_resource[TCookies: Struct, TUser: Struct](
        self,
        resource: Resource,
        *,
        auth: CookieAuth[TCookies, TUser] | None = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None: ...
    @overload
    def _include_resource[THeaders: Struct, TCookies: Struct, TUser: Struct](
        self,
        resource: Resource,
        *,
        auth: HybridAuth[THeaders, TCookies, TUser] | None = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None: ...
    def _include_resource(
        self,
        resource: Resource,
        *,
        auth: "Auth[Any, Any] | CookieAuth[Any, Any] | HybridAuth[Any, Any, Any] | None" = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None:
        """Register a ``Resource``'s CRUD methods as routes, optionally behind ``auth``.

        An authenticator returning ``TUser`` gates every method: no valid credentials, no
        handler. One returning ``TUser | None`` accepts anonymous callers instead — see
        :meth:`_include_endpoint`.

        ``cors=`` sets this include's :class:`CORS` policy: omitted inherits the
        ``_include_cors`` default, a policy overrides it, ``CORS.OFF`` opts out.
        ``middleware=`` adds include-scoped middleware on top of any registered with
        :meth:`_include_middleware` — scope is deployment policy, so it lives here at
        the mount, not on the class. Scoped intercepts run post-resolve, pre-auth.
        """
        self.__include(resource, Resource.METHODS, auth=auth, cors=cors, middleware=middleware)

    @overload
    def _include_endpoint[THeaders: Struct, TUser: Struct](
        self,
        endpoint: Endpoint,
        *,
        auth: Auth[THeaders, TUser] | None = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None: ...
    @overload
    def _include_endpoint[TCookies: Struct, TUser: Struct](
        self,
        endpoint: Endpoint,
        *,
        auth: CookieAuth[TCookies, TUser] | None = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None: ...
    @overload
    def _include_endpoint[THeaders: Struct, TCookies: Struct, TUser: Struct](
        self,
        endpoint: Endpoint,
        *,
        auth: HybridAuth[THeaders, TCookies, TUser] | None = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None: ...
    def _include_endpoint(
        self,
        endpoint: Endpoint,
        *,
        auth: "Auth[Any, Any] | CookieAuth[Any, Any] | HybridAuth[Any, Any, Any] | None" = None,
        cors: CORS | None = None,
        middleware: Sequence[object] = (),
    ) -> None:
        """Register an ``Endpoint``'s verb methods as routes, optionally behind ``auth``.

        An authenticator returning ``TUser`` gates every verb: no valid credentials, no
        handler. One returning ``TUser | None`` makes credentials an *input* — a caller
        presenting none is served with ``user=None``, *invalid* credentials are still a 401,
        and every handler on the route must declare ``user: TUser | None`` (all checked at
        startup).

        ``cors=`` sets this include's :class:`CORS` policy: omitted inherits the
        ``_include_cors`` default, a policy overrides it, ``CORS.OFF`` opts out.
        ``middleware=`` adds include-scoped middleware on top of any registered with
        :meth:`_include_middleware` — scope is deployment policy, so it lives here at
        the mount, not on the class. Scoped intercepts run post-resolve, pre-auth.
        """
        self.__include(endpoint, Endpoint.METHODS, auth=auth, cors=cors, middleware=middleware)

    @overload
    def _include_websocket[THeaders: Struct, TUser: Struct](
        self,
        endpoint: WebSocketEndpoint,
        *,
        auth: Auth[THeaders, TUser] | None = None,
        max_frame_size: int = 1024 * 1024,
        middleware: Sequence[object] = (),
    ) -> None: ...
    @overload
    def _include_websocket[TCookies: Struct, TUser: Struct](
        self,
        endpoint: WebSocketEndpoint,
        *,
        auth: CookieAuth[TCookies, TUser] | None = None,
        max_frame_size: int = 1024 * 1024,
        middleware: Sequence[object] = (),
    ) -> None: ...
    @overload
    def _include_websocket[THeaders: Struct, TCookies: Struct, TUser: Struct](
        self,
        endpoint: WebSocketEndpoint,
        *,
        auth: HybridAuth[THeaders, TCookies, TUser] | None = None,
        max_frame_size: int = 1024 * 1024,
        middleware: Sequence[object] = (),
    ) -> None: ...
    def _include_websocket(
        self,
        endpoint: WebSocketEndpoint,
        *,
        auth: "Auth[Any, Any] | CookieAuth[Any, Any] | HybridAuth[Any, Any, Any] | None" = None,
        max_frame_size: int = 1024 * 1024,
        middleware: Sequence[object] = (),
    ) -> None:
        """Register one typed WebSocket protocol and compile its handshake contract."""
        if not isinstance(max_frame_size, int) or isinstance(max_frame_size, bool):
            raise WiringError("max_frame_size must be a positive integer")
        if max_frame_size < 1:
            raise WiringError("max_frame_size must be a positive integer")
        cls = type(endpoint)
        path = getattr(cls, "path", None)
        if path is None:
            raise WiringError(
                f"{cls.__name__}: no path — declare it on the class, "
                f"e.g. `class {cls.__name__}(WebSocketEndpoint, path='/...')`.",
            )
        fn = getattr(endpoint, "handle", None)
        if fn is None:
            raise WiringError(f"{cls.__name__} must define handle")
        sources, inbound, outbound = _bind_websocket_sources(cls, fn)
        compiled_auth = _CompiledAuth(auth) if auth is not None else None
        self.__check_user_source(cls, "handle", sources, compiled_auth)
        tail = _RouteTail()
        intercepts: list[_WebSocketInterceptRunner] = []
        for item in middleware:
            compiled = CompiledMiddleware(item)
            if compiled.intercept is None:
                continue
            if "GET" not in compiled.intercept_methods:
                raise WiringError(
                    f"{compiled.owner}.intercept can never run on a WebSocket handshake: "
                    f"intercept_methods is {compiled.intercept_methods!r}, expected GET",
                )
            intercepts.append(
                _InterceptRunner(
                    compiled.intercept,
                    _intercept_sender(
                        compiled.owner,
                        compiled.intercept,
                        self.__reverser,
                        self.__exceptions,
                        tail,
                    ),
                )
            )
        segments = _route_segments(
            cls, "handle", _parse_template(path), sources.path, extends_path=False
        )
        route = _WebSocketRoute(
            fn,
            sources=sources,
            inbound=inbound,
            outbound=outbound,
            auth=compiled_auth,
            exceptions=self.__exceptions,
            intercepts=tuple(intercepts),
            tail=tail,
            max_frame_size=max_frame_size,
        )
        self.__register_websocket(segments, route)

    def _include_openapi(
        self,
        *,
        title: str,
        version: str,
        description: str | None = None,
        openapi_path: str = "/openapi.json",
        docs_path: str | None = "/docs",
        servers: Sequence[str] = (),
        tags: Sequence[Tag] = (),
        docs_html: str | None = None,
        favicon: Path | str | None = None,
        scalar_config: ScalarConfig | None = None,
    ) -> None:
        """Serve an auto-generated OpenAPI 3.1 document and a docs UI.

        Call inside ``wire`` (order among the ``include_*`` calls doesn't matter — the
        document is built once after wiring completes). ``openapi_path`` serves the JSON
        spec; ``docs_path`` serves a Scalar UI pointed at it (pass ``None`` to omit the
        UI, or ``docs_html`` to replace the page — e.g. for offline / strict-CSP hosting).

        ``scalar_config`` is a typed :class:`~jero.ScalarConfig` tuning the Scalar UI — e.g.
        ``scalar_config=ScalarConfig(hide_models=True)`` drops the global Models list, or set a
        ``theme`` / ``layout``. Only its set fields are sent, so Scalar's own defaults apply
        otherwise. For options ``ScalarConfig`` doesn't model, supply a full ``docs_html``
        page instead (``scalar_config`` is ignored when ``docs_html`` is given).

        ``favicon`` gives the docs page an icon. A ``Path`` (the primary case) is read
        once at wiring — a missing/unreadable file or an unsupported suffix
        (``.ico``/``.png``/``.svg``) is a ``WiringError`` — and served as a precomputed
        response at ``/favicon.ico``; no runtime file I/O. A ``str`` is a URL (a
        ``data:`` URI works too), emitted verbatim in the page's ``<link rel="icon">``
        with nothing served. Like the spec routes, ``/favicon.ico`` never appears in the
        generated document. A custom ``docs_html`` page is never modified — reference
        the favicon yourself there.

        ``tags`` declares document-level ``Tag``\\ s to describe operation groups and pin the
        order they appear in the docs UI. Operations may also define/use tags via their
        ``meta`` (a bare name, or a ``Tag`` with a description); all are merged here. The one
        rule: describing the same tag name two different ways is a startup ``WiringError``.

        The spec is derived from your wired resources/endpoints: their typed sources
        (path/query/header params, request bodies), return types (responses), auth
        (security), ``msgspec.Meta`` field constraints, and the metadata you declare —
        ``OperationMeta`` (summary/description/tags/responses) and a model's ``ModelMeta``.
        Docstrings are never published; public prose is always explicit.
        """
        self.__openapi = _OpenAPIConfig(
            title=title,
            version=version,
            description=description,
            servers=tuple(servers),
            tags=tuple(tags),
            openapi_path=openapi_path,
            docs_path=docs_path,
        )
        # The framework routes register through an include record like any other
        # include, so they are covered by the app's CORS default and middleware (a
        # cross-origin tool must be able to fetch the spec; a global security-headers
        # middleware must decorate the docs page). They just never appear in the
        # generated document.
        tail = _RouteTail()
        record = _IncludeRecord(tail=tail, routes=[], cors=None, cors_off=False, middleware=())
        favicon_href: str | None = None
        if isinstance(favicon, Path):
            body, content_type = _favicon_payload(favicon)
            favicon_handler = _static_bytes_handler(body, content_type, tail)
            self.__register("GET", _parse_template("/favicon.ico"), favicon_handler)
            record.routes.append(("GET", favicon_handler))
            favicon_href = "/favicon.ico"
        elif favicon is not None:
            favicon_href = favicon
        doc_handler = _json_doc_handler(self.__openapi, tail)
        self.__register("GET", _parse_template(openapi_path), doc_handler)
        record.routes.append(("GET", doc_handler))
        if docs_path is not None:
            page = (
                docs_html
                if docs_html is not None
                else _scalar_html(title, openapi_path, favicon_href, scalar_config)
            )
            page_handler = _static_bytes_handler(page.encode(), b"text/html; charset=utf-8", tail)
            self.__register("GET", _parse_template(docs_path), page_handler)
            record.routes.append(("GET", page_handler))
        self.__includes.append(record)

    async def _create_background_tasks(
        self,
        *,
        maxsize: int = 1024,
        drain_timeout: float | None = 30.0,
        allow_one_to_many: bool = False,
    ) -> BackgroundTasks:
        """Build a :class:`BackgroundTasks` queue bound to the app's lifecycle.

        Sugar for ``await self._aenter(BackgroundTasks(...))``: the worker starts at
        startup and drains/stops at shutdown. Call inside ``wire`` *after* the services
        its handlers use, so reverse-order shutdown drains the queue before those
        services are torn down.
        """
        return await self._aenter(
            BackgroundTasks(
                maxsize=maxsize,
                drain_timeout=drain_timeout,
                allow_one_to_many=allow_one_to_many,
            )
        )

    def __build_openapi_document(self, config: _OpenAPIConfig) -> bytes:
        """Translate the captured operations into the OpenAPI document, encoded as JSON."""
        operations = tuple(
            operation_input(spec, self.__exceptions.adapter) for spec in self.__operations
        )
        schemes: dict[str, SecurityScheme] = {}
        for spec in self.__operations:
            scheme = spec.security_scheme
            if scheme is None:
                continue
            existing = schemes.get(scheme.scheme_name)
            if existing is not None and existing != scheme:
                raise WiringError(
                    f"two different OpenAPI security schemes share the name "
                    f"{scheme.scheme_name!r}; give each a distinct scheme_name",
                )
            schemes[scheme.scheme_name] = scheme
        info = Info(
            title=config.title,
            version=config.version,
            description=config.description,
            servers=config.servers,
            tags=_assemble_openapi_tags(config.tags, operations),
        )
        try:
            document = build_openapi(info, operations, schemes)
        except OpenAPINameConflictError as exc:
            # A ModelMeta(name=...) override collided with another component's name.
            raise WiringError(str(exc)) from exc
        except KeyError as exc:
            # msgspec.json.schema_components keys components by type name and raises KeyError
            # when two distinct Structs share a name in the same module.
            raise WiringError(
                f"could not generate the OpenAPI schema for {exc}; two different msgspec "
                f"Structs likely share a name in the same module — rename one",
            ) from exc
        return msgspec_encoder.encode(document)

    def __resolve_dynamic(self, method: str, path: str) -> tuple[_Handler, dict[str, str]] | None:
        # Static routes never reach here: __call__ resolves them with an inlined dict
        # lookup. The cast is paid only on this, the dynamic path.
        segments = path.split("/")
        verb = cast("HTTPMethod", method)
        for pattern in self.__dynamic.get((verb, len(segments)), ()):
            # Inlines pattern.matches (kept for the cold Allow path): a genexpr per
            # candidate is measurable here. unquote only when a segment is escaped.
            for i, value in pattern.statics:
                if segments[i] != value:
                    break
            else:
                values: dict[str, str] = {}
                for i, name in pattern.params:
                    segment = segments[i]
                    values[name] = unquote(segment) if "%" in segment else segment
                return pattern.handler, values
        return None

    def __resolve_websocket_dynamic(
        self, path: str
    ) -> tuple[_WebSocketHandler, dict[str, str]] | None:
        segments = path.split("/")
        for pattern in self.__websocket_dynamic.get(len(segments), ()):
            for index, value in pattern.statics:
                if segments[index] != value:
                    break
            else:
                values: dict[str, str] = {}
                for index, name in pattern.params:
                    segment = segments[index]
                    values[name] = unquote(segment) if "%" in segment else segment
                return pattern.handler, values
        return None

    def __allowed_methods(self, path: str) -> tuple[HTTPMethod, ...]:
        allowed = list(self.__allowed.get(path, ()))
        segments = path.split("/")
        for (method, count), bucket in self.__dynamic.items():
            if (
                count == len(segments)
                and method not in allowed
                and any(pattern.matches(segments) for pattern in bucket)
            ):
                allowed.append(method)
        return tuple(allowed)

    def __allow_for(self, path: str) -> bytes | None:
        """The Allow header for a path, or None if no route shape matches it."""
        cached = self.__allow_cache.get(path)
        if cached is not None:
            return cached
        allowed = self.__allowed_methods(path)
        return _allow_header(allowed) if allowed else None

    def __preflight_pairs(self, scope: Scope, path: str) -> list[tuple[bytes, bytes]] | None:
        """The CORS pairs for a preflight OPTIONS, or None when the request isn't one
        (no ``Access-Control-Request-Method``) or nothing answers it.

        The requested method selects *which route's* policy replies — the answer is per
        (path, requested method), so ``GET`` public / ``POST`` restricted on one path
        works. Preflights carry no credentials, so this runs before any auth would."""
        requested = requested_method(scope)
        if requested is None:
            return None
        # HEAD is served from GET routes, but the *policy* check still sees "HEAD".
        verb = "GET" if requested == "HEAD" else requested
        handler = self.__static.get((verb, path))
        if handler is None:
            resolved = self.__resolve_dynamic(verb, path)
            if resolved is None:
                return None
            handler = resolved[0]
        cors = self.__route_cors.get(handler)
        if cors is None:
            return None
        return cors.preflight_pairs(scope, requested)

    def __log_openapi_docs(self, config: _OpenAPIConfig) -> None:
        """Announce where the docs/spec are served, once, at startup.

        jero is the ASGI app, not the server, so it can't know the bound host/port — the
        URL is absolute only when ``JERO_BASE_URL`` names the public origin, otherwise the
        path is relative (the server prints its own ``Listening at`` line with the host).
        """
        base = (os.environ.get("JERO_BASE_URL") or "").rstrip("/")
        if config.docs_path is not None:
            logger.info("Serving API docs at %s%s", base, config.docs_path)
        else:
            logger.info("Serving OpenAPI spec at %s%s", base, config.openapi_path)

    def __fill_tail(
        self,
        tail: _RouteTail,
        cors: CompiledCORS | None,
        middlewares: Sequence[CompiledMiddleware],
    ) -> None:
        """Fill one response-header tail: the CORS policy first, then each middleware's
        constant pairs and ``response_headers`` method hooks (globals before scoped, in
        registration order). Duplicate *constant* names across contributors fail loud."""
        claims: dict[str, str] = {}
        if cors is not None:
            _claim_header_names(claims, "CORS", cors.constant_pairs)
            tail.pairs += cors.constant_pairs
            if cors.dynamic is not None:
                tail.dynamic += (cors.dynamic,)
        for mw in middlewares:
            if mw.constant_headers is not None:
                pairs = _constant_middleware_pairs(mw.constant_headers)
                _claim_header_names(claims, mw.owner, pairs)
                tail.pairs += pairs
            if mw.headers_hook is not None:
                tail.dynamic += (_HeadersTailHook(mw.headers_hook),)
        tail.active = bool(tail.pairs or tail.dynamic)

    def __build_global_middleware(self) -> None:
        """Build the pre-routing machinery: the verb-keyed global intercept table and
        the global observes. ``__pre`` stays None when neither exists — a middleware
        with only header tiers has no pre-routing presence at all."""
        table: dict[str, list[_InterceptRunner]] = {}
        for mw in self.__middleware:
            if mw.intercept is None:
                continue
            runner = _InterceptRunner(
                mw.intercept,
                _intercept_sender(
                    mw.owner, mw.intercept, self.__reverser, self.__exceptions, self.__app_tail
                ),
            )
            for method in mw.intercept_methods:
                table.setdefault(method, []).append(runner)
        observes = tuple(mw.observe for mw in self.__middleware if mw.observe is not None)
        if table or observes:
            self.__pre = _GlobalMiddleware(
                {method: tuple(runners) for method, runners in table.items()}, observes
            )

    @staticmethod
    def __check_scoped_intercepts(record: _IncludeRecord) -> None:
        """A scoped intercept none of the include's wire methods can ever trigger is a
        dead registration — fail loud rather than silently never running.

        A *partial* overlap stays legal: one middleware class declaring
        ``("GET", "POST")`` is reusable across a GET-only and a POST-only include, each
        picking up the verbs it serves. The exception is ``OPTIONS``, which is answered
        before routing on every path — an OPTIONS entry is dead on *any* include, so it
        is rejected outright rather than left to overlap luck."""
        served = {method for method, _ in record.routes}
        if "GET" in served:
            served.add("HEAD")
        for mw in record.middleware:
            if mw.intercept is None:
                continue
            if "OPTIONS" in mw.intercept_methods:
                raise WiringError(
                    f"{mw.owner}.intercept declares OPTIONS in intercept_methods, but "
                    f"OPTIONS never reaches include-scoped middleware — intercept it "
                    f"globally via _include_middleware",
                )
            if not served.intersection(mw.intercept_methods):
                raise WiringError(
                    f"{mw.owner}.intercept can never run on this include: it declares "
                    f"intercept_methods {mw.intercept_methods!r} but the include serves "
                    f"{', '.join(sorted(served))}",
                )

    def __swap_handler(self, old: _Handler, new: _Handler) -> None:
        """Replace one compiled route in the routing tables (wiring-time only) — how a
        covered route's ``_CoveredRoute`` wrapper takes its place, so uncovered routes
        never pay so much as a branch for middleware."""
        for key, handler in self.__static.items():
            if handler is old:
                self.__static[key] = new
        for bucket in self.__dynamic.values():
            for index, pattern in enumerate(bucket):
                if pattern.handler is old:
                    bucket[index] = _Pattern(pattern.statics, pattern.params, new)

    def __cover_route(
        self,
        record: _IncludeRecord,
        verb: HTTPMethod,
        route: _Handler,
        scoped_runners: Sequence[tuple[CompiledMiddleware, _InterceptRunner]],
        observes: tuple[ObserveHook, ...],
        *,
        headers_hooks: bool,
    ) -> _Handler:
        """Wrap one route in a ``_CoveredRoute`` when any middleware hook covers it
        (an intercept scoped to one of its wire methods, an observe, or a dynamic
        ``response_headers`` needing the ``received_at`` stamp); uncovered routes are
        returned untouched."""
        wire_methods = ("GET", "HEAD") if verb == "GET" else (verb,)
        table: dict[str, list[_InterceptRunner]] = {}
        for mw, runner in scoped_runners:
            for method in mw.intercept_methods:
                if method in wire_methods:
                    table.setdefault(method, []).append(runner)
        if not table and not observes and not headers_hooks:
            return route
        compiled = _RouteMiddleware(
            {method: tuple(runners) for method, runners in table.items()} if table else None,
            observes,
        )
        covered = _CoveredRoute(route, compiled, self.__exceptions, record.tail)
        self.__swap_handler(route, covered)
        return covered

    def __finalize_tails(self) -> None:
        """Resolve every include's response-header tail (the CORS policy it declares or
        inherits, plus middleware header tiers), the middleware coverage of each route,
        and the app-level tail + pre-routing table for unrouted responses. One pass
        after wiring, so ``_include_cors`` / ``_include_middleware`` and the include
        calls compose in any order."""
        default = self.__cors_default
        # Unrouted responses (404, 405) carry the app default and global middleware only.
        self.__fill_tail(self.__app_tail, default, self.__middleware)
        self.__build_global_middleware()
        global_observes = self.__pre.observes if self.__pre is not None else ()
        for record in self.__includes:
            self.__check_scoped_intercepts(record)
            cors = None if record.cors_off else (record.cors or default)
            middlewares = (*self.__middleware, *record.middleware)
            self.__fill_tail(record.tail, cors, middlewares)
            headers_hooks = any(mw.headers_hook is not None for mw in middlewares)
            scoped_runners = [
                (
                    mw,
                    _InterceptRunner(
                        mw.intercept,
                        _intercept_sender(
                            mw.owner, mw.intercept, self.__reverser, self.__exceptions, record.tail
                        ),
                    ),
                )
                for mw in record.middleware
                if mw.intercept is not None
            ]
            observes = global_observes + tuple(
                mw.observe for mw in record.middleware if mw.observe is not None
            )
            for verb, route in record.routes:
                handler = self.__cover_route(
                    record, verb, route, scoped_runners, observes, headers_hooks=headers_hooks
                )
                if cors is not None:
                    self.__route_cors[handler] = cors

    def __finalize(self) -> None:
        """Precompute Allow headers, resolve response-header tails, and build the OpenAPI
        document; runs once after wiring."""
        self.__finalize_tails()
        self.__allow_cache = {
            path: _allow_header(self.__allowed_methods(path)) for path in self.__allowed
        }
        if self.__openapi is not None:
            self.__openapi.payload = self.__build_openapi_document(self.__openapi)
            self.__log_openapi_docs(self.__openapi)

    async def __close_resources(self) -> None:
        await self.__astack.aclose()
        self.__stack.close()

    async def __handle_lifespan(self, receive: Receive, send: Send) -> None:
        await receive()  # lifespan.startup
        try:
            await self.wire()
            self.__finalize()  # builds the OpenAPI doc; can raise WiringError (e.g. tag conflict)
        except BaseException as exc:
            await self.__close_resources()  # release anything entered before the failure
            await send(
                {
                    "type": "lifespan.startup.failed",
                    "message": f"{type(exc).__name__}: {exc}",
                },
            )
            raise
        await send({"type": "lifespan.startup.complete"})

        await receive()  # lifespan.shutdown
        try:
            await self.__close_resources()
        except BaseException as exc:
            await send(
                {
                    "type": "lifespan.shutdown.failed",
                    "message": f"{type(exc).__name__}: {exc}",
                },
            )
            raise
        await send({"type": "lifespan.shutdown.complete"})

    async def __intercept_global(
        self,
        pre: _GlobalMiddleware,
        runners: tuple[_InterceptRunner, ...],
        scope: Scope,
        receive: Receive,
        send: Send,
    ) -> bool:
        """Run one verb's global intercepts, pre-routing (they can answer requests no
        route serves — preflights for paths that would 404). True when one answered:
        the response (or its error) has left and the global observes have seen it;
        False falls through to routing untouched."""
        received_at = perf_counter()
        scope["jero.received_at"] = received_at
        if scope["method"] == "HEAD":
            send = _SuppressBody(send)
        capture: _StatusCapture | None = None
        if pre.observes:
            send = capture = _StatusCapture(send)
        answered = await _run_intercepts(
            runners, scope, receive, send, exceptions=self.__exceptions, tail=self.__app_tail
        )
        if answered and capture is not None:
            duration = capture.started_at - received_at if capture.started_at else 0.0
            for observe in pre.observes:
                await observe(scope, capture.status, duration)
        return answered

    async def __fallthrough(self, scope: Scope, send: Send, method: str, path: str) -> None:
        """Answer a request no route serves. No route owns these responses, so the 404
        and 405 problems carry the app-level tail (the CORS default and global
        middleware headers); the OPTIONS answer adds the CORS preflight block when the
        requested method's route has a policy."""
        allow = self.__allow_for(path)
        error: HTTPError
        if allow is None:
            error = NotFoundError()
            await _send_json(
                send,
                error.status,
                self.__exceptions.encode_error(error),
                self.__app_tail.contained_extra(scope),
            )
        elif method == "OPTIONS":
            headers = [(b"allow", allow)]
            if self.__route_cors:
                preflight = self.__preflight_pairs(scope, path)
                if preflight is not None:
                    headers += preflight
            await send({"type": "http.response.start", "status": 204, "headers": headers})
            await send({"type": "http.response.body", "body": b""})
        else:
            error = MethodNotAllowedError()
            extra = self.__app_tail.contained_extra(scope)
            await _send_json(
                send,
                error.status,
                self.__exceptions.encode_error(error),
                [(b"allow", allow)] + (extra if extra is not None else []),
            )

    async def __handle_websocket(self, scope: Scope, receive: Receive, send: Send) -> None:
        connect = await receive()
        if connect["type"] != "websocket.connect":
            await send({"type": "websocket.close", "code": 1008, "reason": "bad handshake"})
            return
        # WebSocket handshakes are GET requests for middleware verb scoping. Global
        # intercepts run before routing/auth exactly as on the HTTP path; observe is
        # deliberately absent for sockets in v1.
        scope["method"] = "GET"
        pre = self.__pre
        if pre is not None:
            runners = pre.intercepts.get("GET")
            if runners is not None and await _run_intercepts(
                runners,
                scope,
                receive,
                _WebSocketDenialSend(send, _supports_websocket_denial(scope)),
                exceptions=self.__exceptions,
                tail=self.__app_tail,
            ):
                return
        path: str = scope["path"]
        handler = self.__websocket_static.get(path)
        path_values: dict[str, str] = {}
        if handler is None:
            resolved = self.__resolve_websocket_dynamic(path)
            if resolved is not None:
                handler, path_values = resolved
        if handler is not None:
            await handler(scope, receive, send, path_values)
            return
        error = NotFoundError()
        payload = self.__exceptions.encode_error(error)
        await _send_websocket_rejection(scope, send, error.status, payload)

    async def __handle_non_http(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] == "lifespan":
            await self.__handle_lifespan(receive, send)
            return
        if scope["type"] == "websocket":
            await self.__handle_websocket(scope, receive, send)
            return
        raise RuntimeError(f"unsupported scope type {scope['type']!r}")

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.__handle_non_http(scope, receive, send)
            return

        # HTTP is the hot path; inlined here (was _handle_http) to save a coroutine hop.
        method: str = scope["method"]
        path: str = scope["path"]
        # Global intercepts run pre-routing; the table is verb-keyed, so an off-scope
        # method costs one dict hit and apps without them one attribute load.
        pre = self.__pre
        if pre is not None:
            runners = pre.intercepts.get(method)
            if runners is not None and await self.__intercept_global(
                pre, runners, scope, receive, send
            ):
                return
        verb = "GET" if method == "HEAD" else method
        # A static hit is the hottest path of all: one dict lookup, inlined here to skip
        # the resolver call (a non-route verb simply misses).
        handler = self.__static.get((verb, path))
        path_values: dict[str, str] = {}
        if handler is None:
            resolved = self.__resolve_dynamic(verb, path)
            if resolved is not None:
                handler, path_values = resolved
        if handler is not None:
            await handler(
                scope, receive, _SuppressBody(send) if method == "HEAD" else send, path_values
            )
            return
        if pre is None or not pre.observes:
            await self.__fallthrough(scope, send, method, path)
            return
        # Global observes see fallthrough answers too — capture the outcome around it.
        received_at = perf_counter()
        scope["jero.received_at"] = received_at
        capture = _StatusCapture(send)
        await self.__fallthrough(scope, capture, method, path)
        duration = capture.started_at - received_at if capture.started_at else 0.0
        for observe in pre.observes:
            await observe(scope, capture.status, duration)

_factory property

The built factory, read inside wire. Set once at construction (read-only).

_aenter(cm) async

Open an async context manager, closed at shutdown in reverse order.

Source code in jero/core.py
3415
3416
3417
async def _aenter[T](self, cm: AbstractAsyncContextManager[T]) -> T:
    """Open an async context manager, closed at shutdown in reverse order."""
    return await self.__astack.enter_async_context(cm)

_create_background_tasks(*, maxsize=1024, drain_timeout=30.0, allow_one_to_many=False) async

Build a :class:BackgroundTasks queue bound to the app's lifecycle.

Sugar for await self._aenter(BackgroundTasks(...)): the worker starts at startup and drains/stops at shutdown. Call inside wire after the services its handlers use, so reverse-order shutdown drains the queue before those services are torn down.

Source code in jero/core.py
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
async def _create_background_tasks(
    self,
    *,
    maxsize: int = 1024,
    drain_timeout: float | None = 30.0,
    allow_one_to_many: bool = False,
) -> BackgroundTasks:
    """Build a :class:`BackgroundTasks` queue bound to the app's lifecycle.

    Sugar for ``await self._aenter(BackgroundTasks(...))``: the worker starts at
    startup and drains/stops at shutdown. Call inside ``wire`` *after* the services
    its handlers use, so reverse-order shutdown drains the queue before those
    services are torn down.
    """
    return await self._aenter(
        BackgroundTasks(
            maxsize=maxsize,
            drain_timeout=drain_timeout,
            allow_one_to_many=allow_one_to_many,
        )
    )

_enter(cm)

Open a sync context manager, closed at shutdown in reverse order.

Source code in jero/core.py
3411
3412
3413
def _enter[T](self, cm: AbstractContextManager[T]) -> T:
    """Open a sync context manager, closed at shutdown in reverse order."""
    return self.__stack.enter_context(cm)

_include_cors(cors)

Serve cross-origin browser callers app-wide with one default :class:CORS policy.

Call inside wire, at most once (order among the include_* calls doesn't matter). Every include inherits the policy unless it passes its own cors= — a different policy overrides it, CORS.OFF removes it. Skipping _include_cors entirely means no CORS anywhere except includes that opt in with their own cors=.

The policy is compiled at wiring: a wildcard origin becomes constant header pairs in covered routes' responses (free per request), an origin allow-list one set lookup + origin echo. Preflights (OPTIONS with Access-Control-Request-Method) are answered per (path, requested method) on the existing OPTIONS branch, so two verbs on one path may carry two policies. Error responses carry the failing route's pairs — a browser page must be able to read the 401/422 problem body; unrouted 404s carry this app default.

Source code in jero/core.py
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
def _include_cors(self, cors: CORS) -> None:
    """Serve cross-origin browser callers app-wide with one default :class:`CORS` policy.

    Call inside ``wire``, at most once (order among the ``include_*`` calls doesn't
    matter). Every include inherits the policy unless it passes its own ``cors=`` —
    a different policy overrides it, ``CORS.OFF`` removes it. Skipping
    ``_include_cors`` entirely means no CORS anywhere except includes that opt in
    with their own ``cors=``.

    The policy is compiled at wiring: a wildcard origin becomes constant header
    pairs in covered routes' responses (free per request), an origin allow-list one
    set lookup + origin echo. Preflights (``OPTIONS`` with
    ``Access-Control-Request-Method``) are answered per (path, requested method) on
    the existing OPTIONS branch, so two verbs on one path may carry two policies.
    Error responses carry the failing route's pairs — a browser page must be able
    to *read* the 401/422 problem body; unrouted 404s carry this app default.
    """
    if not isinstance(cast("object", cors), CORS):
        raise WiringError(f"_include_cors requires a CORS policy, got {type(cors).__name__}")
    if cors is CORS.OFF:
        raise WiringError(
            "CORS.OFF is the per-include opt-out; an app that wants no CORS default "
            "simply does not call _include_cors",
        )
    if self.__cors_default is not None:
        raise WiringError("a CORS default is already registered; an app has at most one")
    self.__cors_default = CompiledCORS(cors)

_include_endpoint(endpoint, *, auth=None, cors=None, middleware=())

_include_endpoint(
    endpoint: Endpoint,
    *,
    auth: Auth[THeaders, TUser] | None = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None
_include_endpoint(
    endpoint: Endpoint,
    *,
    auth: CookieAuth[TCookies, TUser] | None = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None
_include_endpoint(
    endpoint: Endpoint,
    *,
    auth: HybridAuth[THeaders, TCookies, TUser]
    | None = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None

Register an Endpoint's verb methods as routes, optionally behind auth.

An authenticator returning TUser gates every verb: no valid credentials, no handler. One returning TUser | None makes credentials an input — a caller presenting none is served with user=None, invalid credentials are still a 401, and every handler on the route must declare user: TUser | None (all checked at startup).

cors= sets this include's :class:CORS policy: omitted inherits the _include_cors default, a policy overrides it, CORS.OFF opts out. middleware= adds include-scoped middleware on top of any registered with :meth:_include_middleware — scope is deployment policy, so it lives here at the mount, not on the class. Scoped intercepts run post-resolve, pre-auth.

Source code in jero/core.py
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
def _include_endpoint(
    self,
    endpoint: Endpoint,
    *,
    auth: "Auth[Any, Any] | CookieAuth[Any, Any] | HybridAuth[Any, Any, Any] | None" = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None:
    """Register an ``Endpoint``'s verb methods as routes, optionally behind ``auth``.

    An authenticator returning ``TUser`` gates every verb: no valid credentials, no
    handler. One returning ``TUser | None`` makes credentials an *input* — a caller
    presenting none is served with ``user=None``, *invalid* credentials are still a 401,
    and every handler on the route must declare ``user: TUser | None`` (all checked at
    startup).

    ``cors=`` sets this include's :class:`CORS` policy: omitted inherits the
    ``_include_cors`` default, a policy overrides it, ``CORS.OFF`` opts out.
    ``middleware=`` adds include-scoped middleware on top of any registered with
    :meth:`_include_middleware` — scope is deployment policy, so it lives here at
    the mount, not on the class. Scoped intercepts run post-resolve, pre-auth.
    """
    self.__include(endpoint, Endpoint.METHODS, auth=auth, cors=cors, middleware=middleware)

_include_error_adapter(adapter)

Replace the Problem family's wire body app-wide with adapter's composition.

Call inside wire, at most once. Every Problem-family error — the framework's built-ins (404/405/422/500, …) and your own HTTPError subclasses, including those returned by exception handlers — is rendered through adapter.compose instead of RFC 9457 Problem Details, and the derived OpenAPI error responses document the adapter's body. StructHTTPError\ s render themselves.

Source code in jero/core.py
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
def _include_error_adapter(self, adapter: ErrorBodyAdapter[Any]) -> None:
    """Replace the Problem family's wire body app-wide with ``adapter``'s composition.

    Call inside ``wire``, at most once. Every Problem-family error — the framework's
    built-ins (404/405/422/500, …) and your own ``HTTPError`` subclasses, including
    those returned by exception handlers — is rendered through ``adapter.compose``
    instead of RFC 9457 Problem Details, and the derived OpenAPI error responses
    document the adapter's body. ``StructHTTPError``\\ s render themselves.
    """
    # The isinstance guards untyped callers; cast first so it isn't statically vacuous.
    if not isinstance(cast("object", adapter), ErrorBodyAdapter):
        raise WiringError(
            "_include_error_adapter requires an ErrorBodyAdapter instance, "
            f"got {type(adapter).__name__}",
        )
    if getattr(type(adapter), "body_type", None) is None:
        raise WiringError(
            f"{type(adapter).__name__} never bound a concrete body Struct; "
            "parameterize the class: ErrorBodyAdapter[YourBody]",
        )
    if self.__exceptions.adapter is not None:
        existing = type(self.__exceptions.adapter).__name__
        raise WiringError(
            f"an error body adapter ({existing}) is already registered; an app has at most one",
        )
    self.__exceptions.adapter = adapter

_include_exception_handler(handler)

Register a structurally typed custom exception handler.

The exception, JSON body, and typed-header types are inferred from the concrete handle_exception signature and validated once during wiring.

Source code in jero/core.py
3643
3644
3645
3646
3647
3648
3649
3650
3651
def _include_exception_handler[
    E: Exception,
](self, handler: ExceptionHandler[E]) -> None:
    """Register a structurally typed custom exception handler.

    The exception, JSON body, and typed-header types are inferred from the concrete
    ``handle_exception`` signature and validated once during wiring.
    """
    self.__exceptions.register(handler)

_include_middleware(middleware)

Register one middleware app-wide: every route (current and later includes) is covered, and its intercepts run pre-routing — they can answer requests no route serves, which is how an OPTIONS-scoped intercept answers preflights for paths that would 404.

The middleware is a structurally typed object — no base class; its hooks (response_headers attribute or method, intercept + intercept_methods, observe) are introspected and validated here, fail-loud, and compiled into the covered routes at wiring (see :class:~jero.CORS for the same idea as a built-in). Register order is run order, and globals run before include-scoped middleware. Only what a middleware defines costs anything: a constant response_headers is baked into route header blocks for free, an off-scope verb never reaches an intercept.

Source code in jero/core.py
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
def _include_middleware(self, middleware: object) -> None:
    """Register one middleware app-wide: every route (current and later includes) is
    covered, and its intercepts run *pre-routing* — they can answer requests no
    route serves, which is how an OPTIONS-scoped intercept answers preflights for
    paths that would 404.

    The middleware is a structurally typed object — no base class; its hooks
    (``response_headers`` attribute or method, ``intercept`` + ``intercept_methods``,
    ``observe``) are introspected and validated here, fail-loud, and compiled into
    the covered routes at wiring (see :class:`~jero.CORS` for the same idea as a
    built-in). Register order is run order, and globals run before include-scoped
    middleware. Only what a middleware defines costs anything: a constant
    ``response_headers`` is baked into route header blocks for free, an off-scope
    verb never reaches an ``intercept``.
    """
    self.__middleware.append(CompiledMiddleware(middleware))

_include_openapi(*, title, version, description=None, openapi_path='/openapi.json', docs_path='/docs', servers=(), tags=(), docs_html=None, favicon=None, scalar_config=None)

Serve an auto-generated OpenAPI 3.1 document and a docs UI.

Call inside wire (order among the include_* calls doesn't matter — the document is built once after wiring completes). openapi_path serves the JSON spec; docs_path serves a Scalar UI pointed at it (pass None to omit the UI, or docs_html to replace the page — e.g. for offline / strict-CSP hosting).

scalar_config is a typed :class:~jero.ScalarConfig tuning the Scalar UI — e.g. scalar_config=ScalarConfig(hide_models=True) drops the global Models list, or set a theme / layout. Only its set fields are sent, so Scalar's own defaults apply otherwise. For options ScalarConfig doesn't model, supply a full docs_html page instead (scalar_config is ignored when docs_html is given).

favicon gives the docs page an icon. A Path (the primary case) is read once at wiring — a missing/unreadable file or an unsupported suffix (.ico/.png/.svg) is a WiringError — and served as a precomputed response at /favicon.ico; no runtime file I/O. A str is a URL (a data: URI works too), emitted verbatim in the page's <link rel="icon"> with nothing served. Like the spec routes, /favicon.ico never appears in the generated document. A custom docs_html page is never modified — reference the favicon yourself there.

tags declares document-level Tag\ s to describe operation groups and pin the order they appear in the docs UI. Operations may also define/use tags via their meta (a bare name, or a Tag with a description); all are merged here. The one rule: describing the same tag name two different ways is a startup WiringError.

The spec is derived from your wired resources/endpoints: their typed sources (path/query/header params, request bodies), return types (responses), auth (security), msgspec.Meta field constraints, and the metadata you declare — OperationMeta (summary/description/tags/responses) and a model's ModelMeta. Docstrings are never published; public prose is always explicit.

Source code in jero/core.py
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
def _include_openapi(
    self,
    *,
    title: str,
    version: str,
    description: str | None = None,
    openapi_path: str = "/openapi.json",
    docs_path: str | None = "/docs",
    servers: Sequence[str] = (),
    tags: Sequence[Tag] = (),
    docs_html: str | None = None,
    favicon: Path | str | None = None,
    scalar_config: ScalarConfig | None = None,
) -> None:
    """Serve an auto-generated OpenAPI 3.1 document and a docs UI.

    Call inside ``wire`` (order among the ``include_*`` calls doesn't matter — the
    document is built once after wiring completes). ``openapi_path`` serves the JSON
    spec; ``docs_path`` serves a Scalar UI pointed at it (pass ``None`` to omit the
    UI, or ``docs_html`` to replace the page — e.g. for offline / strict-CSP hosting).

    ``scalar_config`` is a typed :class:`~jero.ScalarConfig` tuning the Scalar UI — e.g.
    ``scalar_config=ScalarConfig(hide_models=True)`` drops the global Models list, or set a
    ``theme`` / ``layout``. Only its set fields are sent, so Scalar's own defaults apply
    otherwise. For options ``ScalarConfig`` doesn't model, supply a full ``docs_html``
    page instead (``scalar_config`` is ignored when ``docs_html`` is given).

    ``favicon`` gives the docs page an icon. A ``Path`` (the primary case) is read
    once at wiring — a missing/unreadable file or an unsupported suffix
    (``.ico``/``.png``/``.svg``) is a ``WiringError`` — and served as a precomputed
    response at ``/favicon.ico``; no runtime file I/O. A ``str`` is a URL (a
    ``data:`` URI works too), emitted verbatim in the page's ``<link rel="icon">``
    with nothing served. Like the spec routes, ``/favicon.ico`` never appears in the
    generated document. A custom ``docs_html`` page is never modified — reference
    the favicon yourself there.

    ``tags`` declares document-level ``Tag``\\ s to describe operation groups and pin the
    order they appear in the docs UI. Operations may also define/use tags via their
    ``meta`` (a bare name, or a ``Tag`` with a description); all are merged here. The one
    rule: describing the same tag name two different ways is a startup ``WiringError``.

    The spec is derived from your wired resources/endpoints: their typed sources
    (path/query/header params, request bodies), return types (responses), auth
    (security), ``msgspec.Meta`` field constraints, and the metadata you declare —
    ``OperationMeta`` (summary/description/tags/responses) and a model's ``ModelMeta``.
    Docstrings are never published; public prose is always explicit.
    """
    self.__openapi = _OpenAPIConfig(
        title=title,
        version=version,
        description=description,
        servers=tuple(servers),
        tags=tuple(tags),
        openapi_path=openapi_path,
        docs_path=docs_path,
    )
    # The framework routes register through an include record like any other
    # include, so they are covered by the app's CORS default and middleware (a
    # cross-origin tool must be able to fetch the spec; a global security-headers
    # middleware must decorate the docs page). They just never appear in the
    # generated document.
    tail = _RouteTail()
    record = _IncludeRecord(tail=tail, routes=[], cors=None, cors_off=False, middleware=())
    favicon_href: str | None = None
    if isinstance(favicon, Path):
        body, content_type = _favicon_payload(favicon)
        favicon_handler = _static_bytes_handler(body, content_type, tail)
        self.__register("GET", _parse_template("/favicon.ico"), favicon_handler)
        record.routes.append(("GET", favicon_handler))
        favicon_href = "/favicon.ico"
    elif favicon is not None:
        favicon_href = favicon
    doc_handler = _json_doc_handler(self.__openapi, tail)
    self.__register("GET", _parse_template(openapi_path), doc_handler)
    record.routes.append(("GET", doc_handler))
    if docs_path is not None:
        page = (
            docs_html
            if docs_html is not None
            else _scalar_html(title, openapi_path, favicon_href, scalar_config)
        )
        page_handler = _static_bytes_handler(page.encode(), b"text/html; charset=utf-8", tail)
        self.__register("GET", _parse_template(docs_path), page_handler)
        record.routes.append(("GET", page_handler))
    self.__includes.append(record)

_include_resource(resource, *, auth=None, cors=None, middleware=())

_include_resource(
    resource: Resource,
    *,
    auth: Auth[THeaders, TUser] | None = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None
_include_resource(
    resource: Resource,
    *,
    auth: CookieAuth[TCookies, TUser] | None = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None
_include_resource(
    resource: Resource,
    *,
    auth: HybridAuth[THeaders, TCookies, TUser]
    | None = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None

Register a Resource's CRUD methods as routes, optionally behind auth.

An authenticator returning TUser gates every method: no valid credentials, no handler. One returning TUser | None accepts anonymous callers instead — see :meth:_include_endpoint.

cors= sets this include's :class:CORS policy: omitted inherits the _include_cors default, a policy overrides it, CORS.OFF opts out. middleware= adds include-scoped middleware on top of any registered with :meth:_include_middleware — scope is deployment policy, so it lives here at the mount, not on the class. Scoped intercepts run post-resolve, pre-auth.

Source code in jero/core.py
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
def _include_resource(
    self,
    resource: Resource,
    *,
    auth: "Auth[Any, Any] | CookieAuth[Any, Any] | HybridAuth[Any, Any, Any] | None" = None,
    cors: CORS | None = None,
    middleware: Sequence[object] = (),
) -> None:
    """Register a ``Resource``'s CRUD methods as routes, optionally behind ``auth``.

    An authenticator returning ``TUser`` gates every method: no valid credentials, no
    handler. One returning ``TUser | None`` accepts anonymous callers instead — see
    :meth:`_include_endpoint`.

    ``cors=`` sets this include's :class:`CORS` policy: omitted inherits the
    ``_include_cors`` default, a policy overrides it, ``CORS.OFF`` opts out.
    ``middleware=`` adds include-scoped middleware on top of any registered with
    :meth:`_include_middleware` — scope is deployment policy, so it lives here at
    the mount, not on the class. Scoped intercepts run post-resolve, pre-auth.
    """
    self.__include(resource, Resource.METHODS, auth=auth, cors=cors, middleware=middleware)

_include_websocket(endpoint, *, auth=None, max_frame_size=1024 * 1024, middleware=())

_include_websocket(
    endpoint: WebSocketEndpoint,
    *,
    auth: Auth[THeaders, TUser] | None = None,
    max_frame_size: int = 1024 * 1024,
    middleware: Sequence[object] = (),
) -> None
_include_websocket(
    endpoint: WebSocketEndpoint,
    *,
    auth: CookieAuth[TCookies, TUser] | None = None,
    max_frame_size: int = 1024 * 1024,
    middleware: Sequence[object] = (),
) -> None
_include_websocket(
    endpoint: WebSocketEndpoint,
    *,
    auth: HybridAuth[THeaders, TCookies, TUser]
    | None = None,
    max_frame_size: int = 1024 * 1024,
    middleware: Sequence[object] = (),
) -> None

Register one typed WebSocket protocol and compile its handshake contract.

Source code in jero/core.py
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
def _include_websocket(
    self,
    endpoint: WebSocketEndpoint,
    *,
    auth: "Auth[Any, Any] | CookieAuth[Any, Any] | HybridAuth[Any, Any, Any] | None" = None,
    max_frame_size: int = 1024 * 1024,
    middleware: Sequence[object] = (),
) -> None:
    """Register one typed WebSocket protocol and compile its handshake contract."""
    if not isinstance(max_frame_size, int) or isinstance(max_frame_size, bool):
        raise WiringError("max_frame_size must be a positive integer")
    if max_frame_size < 1:
        raise WiringError("max_frame_size must be a positive integer")
    cls = type(endpoint)
    path = getattr(cls, "path", None)
    if path is None:
        raise WiringError(
            f"{cls.__name__}: no path — declare it on the class, "
            f"e.g. `class {cls.__name__}(WebSocketEndpoint, path='/...')`.",
        )
    fn = getattr(endpoint, "handle", None)
    if fn is None:
        raise WiringError(f"{cls.__name__} must define handle")
    sources, inbound, outbound = _bind_websocket_sources(cls, fn)
    compiled_auth = _CompiledAuth(auth) if auth is not None else None
    self.__check_user_source(cls, "handle", sources, compiled_auth)
    tail = _RouteTail()
    intercepts: list[_WebSocketInterceptRunner] = []
    for item in middleware:
        compiled = CompiledMiddleware(item)
        if compiled.intercept is None:
            continue
        if "GET" not in compiled.intercept_methods:
            raise WiringError(
                f"{compiled.owner}.intercept can never run on a WebSocket handshake: "
                f"intercept_methods is {compiled.intercept_methods!r}, expected GET",
            )
        intercepts.append(
            _InterceptRunner(
                compiled.intercept,
                _intercept_sender(
                    compiled.owner,
                    compiled.intercept,
                    self.__reverser,
                    self.__exceptions,
                    tail,
                ),
            )
        )
    segments = _route_segments(
        cls, "handle", _parse_template(path), sources.path, extends_path=False
    )
    route = _WebSocketRoute(
        fn,
        sources=sources,
        inbound=inbound,
        outbound=outbound,
        auth=compiled_auth,
        exceptions=self.__exceptions,
        intercepts=tuple(intercepts),
        tail=tail,
        max_frame_size=max_frame_size,
    )
    self.__register_websocket(segments, route)

wire() abstractmethod async

Override to open resources (via _enter / _aenter) and include them.

Runs once at startup. Anything entered via the helpers is torn down (in reverse order) at shutdown.

Abstract: every BaseApp subclass must implement it. A subclass that omits it is flagged at its instantiation site by the type checker.

Source code in jero/core.py
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
@abstractmethod
async def wire(self) -> None:
    """Override to open resources (via ``_enter`` / ``_aenter``) and include them.

    Runs once at startup. Anything entered via the helpers is torn
    down (in reverse order) at shutdown.

    Abstract: every ``BaseApp`` subclass must implement it. A subclass that
    omits it is flagged at its instantiation site by the type checker.
    """

Base for an app's factory. Subclass and add create_* methods that build services with self._enter / self._aenter.

Under an app, the app injects its exit stacks (es / aes); anything opened via the helpers is closed when the app shuts down. Standalone — scripts, cron jobs, notebooks — enter :meth:open instead and the factory owns the same lifecycle itself.

Source code in jero/core.py
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
class BaseFactory:
    """Base for an app's factory. Subclass and add ``create_*`` methods that
    build services with ``self._enter`` / ``self._aenter``.

    Under an app, the app injects its exit stacks (``es`` / ``aes``); anything
    opened via the helpers is closed when the app shuts down. Standalone —
    scripts, cron jobs, notebooks — enter :meth:`open` instead and the factory
    owns the same lifecycle itself.
    """

    def __init__(self, es: ExitStack, aes: AsyncExitStack) -> None:
        self.__stack = es
        self.__astack = aes

    def _enter[T](self, cm: AbstractContextManager[T]) -> T:
        """Open a sync context manager on the factory's injected stack — under an app
        that is the app's stack, so the resource is closed at the app's shutdown."""
        return self.__stack.enter_context(cm)

    async def _aenter[T](self, cm: AbstractAsyncContextManager[T]) -> T:
        """Open an async context manager on the factory's injected stack — under an app
        that is the app's stack, so the resource is closed at the app's shutdown."""
        return await self.__astack.enter_async_context(cm)

    @classmethod
    @asynccontextmanager
    async def open(cls) -> AsyncGenerator[Self]:
        """Use the factory's service graph standalone, with real lifecycle::

            async with Factory.open() as factory:
                service = await factory.create_widget_service()

        Creates a fresh exit-stack pair, builds the factory on them exactly as an
        app does at startup, and unwinds on exit — async resources first, each
        stack in reverse order, even if the block raises. (``FactoryHarness`` is
        this, bridged onto a background loop for synchronous tests.)
        """
        with ExitStack() as stack:
            async with AsyncExitStack() as astack:
                yield _instantiate_factory(cls, stack, astack)

_aenter(cm) async

Open an async context manager on the factory's injected stack — under an app that is the app's stack, so the resource is closed at the app's shutdown.

Source code in jero/core.py
3316
3317
3318
3319
async def _aenter[T](self, cm: AbstractAsyncContextManager[T]) -> T:
    """Open an async context manager on the factory's injected stack — under an app
    that is the app's stack, so the resource is closed at the app's shutdown."""
    return await self.__astack.enter_async_context(cm)

_enter(cm)

Open a sync context manager on the factory's injected stack — under an app that is the app's stack, so the resource is closed at the app's shutdown.

Source code in jero/core.py
3311
3312
3313
3314
def _enter[T](self, cm: AbstractContextManager[T]) -> T:
    """Open a sync context manager on the factory's injected stack — under an app
    that is the app's stack, so the resource is closed at the app's shutdown."""
    return self.__stack.enter_context(cm)

open() async classmethod

Use the factory's service graph standalone, with real lifecycle::

async with Factory.open() as factory:
    service = await factory.create_widget_service()

Creates a fresh exit-stack pair, builds the factory on them exactly as an app does at startup, and unwinds on exit — async resources first, each stack in reverse order, even if the block raises. (FactoryHarness is this, bridged onto a background loop for synchronous tests.)

Source code in jero/core.py
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
@classmethod
@asynccontextmanager
async def open(cls) -> AsyncGenerator[Self]:
    """Use the factory's service graph standalone, with real lifecycle::

        async with Factory.open() as factory:
            service = await factory.create_widget_service()

    Creates a fresh exit-stack pair, builds the factory on them exactly as an
    app does at startup, and unwinds on exit — async resources first, each
    stack in reverse order, even if the block raises. (``FactoryHarness`` is
    this, bridged onto a background loop for synchronous tests.)
    """
    with ExitStack() as stack:
        async with AsyncExitStack() as astack:
            yield _instantiate_factory(cls, stack, astack)

Bases: TypeError

A router does not meet the framework contract. Raised at startup.

Lives here, on the shared wiring leaf, so both :mod:jero.core and the sender-free :mod:jero._exception_handlers (which validates handlers at wiring time) can raise it without importing each other.

Source code in jero/_wiring_types.py
47
48
49
50
51
52
53
class WiringError(TypeError):
    """A router does not meet the framework contract. Raised at startup.

    Lives here, on the shared wiring leaf, so both :mod:`jero.core` and the sender-free
    :mod:`jero._exception_handlers` (which validates handlers at wiring time) can raise it
    without importing each other.
    """

Routing

Bases: _Routable

One REST resource: subclass and define any of the CRUD methods.

read_one is the item route (its path may extend the mount with the item id); read_many is the collection (its path is exact).

Optional OpenAPI metadata is declared at class definition: meta applies to every operation, meta_<op> to one (meta_create, meta_read_one, …).

Source code in jero/core.py
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
class Resource(_Routable):
    """One REST resource: subclass and define any of the CRUD methods.

    ``read_one`` is the item route (its ``path`` may extend the mount with
    the item id); ``read_many`` is the collection (its path is exact).

    Optional OpenAPI metadata is declared at class definition: ``meta`` applies to every
    operation, ``meta_<op>`` to one (``meta_create``, ``meta_read_one``, …).
    """

    METHODS: ClassVar[dict[str, _Verb]] = {
        "create": _Verb("POST", 201, extends_path=True),
        "read_one": _Verb("GET", 200, extends_path=True),
        "read_many": _Verb("GET", 200, extends_path=False),
        "update_full": _Verb("PUT", 200, extends_path=True),
        "update_partial": _Verb("PATCH", 200, extends_path=True),
        "delete": _Verb("DELETE", 200, extends_path=True),
    }

    meta: ClassVar[ResourceMeta | None] = None
    meta_create: ClassVar[OperationMeta | None] = None
    meta_read_one: ClassVar[OperationMeta | None] = None
    meta_read_many: ClassVar[OperationMeta | None] = None
    meta_update_full: ClassVar[OperationMeta | None] = None
    meta_update_partial: ClassVar[OperationMeta | None] = None
    meta_delete: ClassVar[OperationMeta | None] = None

    def __init_subclass__(
        cls,
        *,
        path: str,
        ref: str | None = None,
        meta: ResourceMeta | None = None,
        meta_create: OperationMeta | None = None,
        meta_read_one: OperationMeta | None = None,
        meta_read_many: OperationMeta | None = None,
        meta_update_full: OperationMeta | None = None,
        meta_update_partial: OperationMeta | None = None,
        meta_delete: OperationMeta | None = None,
        **kwargs: object,
    ) -> None:
        # path / ref handling lives on _Routable
        super().__init_subclass__(path=path, ref=ref, **kwargs)
        _validate_meta(
            cls,
            meta,
            ResourceMeta,
            {
                "meta_create": meta_create,
                "meta_read_one": meta_read_one,
                "meta_read_many": meta_read_many,
                "meta_update_full": meta_update_full,
                "meta_update_partial": meta_update_partial,
                "meta_delete": meta_delete,
            },
        )
        cls.meta = meta
        cls.meta_create = meta_create
        cls.meta_read_one = meta_read_one
        cls.meta_read_many = meta_read_many
        cls.meta_update_full = meta_update_full
        cls.meta_update_partial = meta_update_partial
        cls.meta_delete = meta_delete

Bases: _Routable

One HTTP endpoint at a single path: subclass and define any of get / post / put / patch / delete.

Unlike :class:Resource there are no CRUD semantics — the method name is the verb, every verb returns 200, and the path is exact (no trailing extension). A different path is a different Endpoint.

Optional OpenAPI metadata is declared at class definition: meta applies to every operation, meta_<verb> to one (meta_get, meta_post, …).

Source code in jero/core.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
class Endpoint(_Routable):
    """One HTTP endpoint at a single path: subclass and define any of
    ``get`` / ``post`` / ``put`` / ``patch`` / ``delete``.

    Unlike :class:`Resource` there are no CRUD semantics — the method name
    *is* the verb, every verb returns 200, and the path is exact (no
    trailing extension). A different path is a different ``Endpoint``.

    Optional OpenAPI metadata is declared at class definition: ``meta`` applies to every
    operation, ``meta_<verb>`` to one (``meta_get``, ``meta_post``, …).
    """

    METHODS: ClassVar[dict[str, _Verb]] = {
        "get": _Verb("GET", 200, extends_path=False),
        "post": _Verb("POST", 200, extends_path=False),
        "put": _Verb("PUT", 200, extends_path=False),
        "patch": _Verb("PATCH", 200, extends_path=False),
        "delete": _Verb("DELETE", 200, extends_path=False),
    }

    meta: ClassVar[EndpointMeta | None] = None
    meta_get: ClassVar[OperationMeta | None] = None
    meta_post: ClassVar[OperationMeta | None] = None
    meta_put: ClassVar[OperationMeta | None] = None
    meta_patch: ClassVar[OperationMeta | None] = None
    meta_delete: ClassVar[OperationMeta | None] = None

    def __init_subclass__(
        cls,
        *,
        path: str,
        ref: str | None = None,
        meta: EndpointMeta | None = None,
        meta_get: OperationMeta | None = None,
        meta_post: OperationMeta | None = None,
        meta_put: OperationMeta | None = None,
        meta_patch: OperationMeta | None = None,
        meta_delete: OperationMeta | None = None,
        **kwargs: object,
    ) -> None:
        # path / ref handling lives on _Routable
        super().__init_subclass__(path=path, ref=ref, **kwargs)
        _validate_meta(
            cls,
            meta,
            EndpointMeta,
            {
                "meta_get": meta_get,
                "meta_post": meta_post,
                "meta_put": meta_put,
                "meta_patch": meta_patch,
                "meta_delete": meta_delete,
            },
        )
        cls.meta = meta
        cls.meta_get = meta_get
        cls.meta_post = meta_post
        cls.meta_put = meta_put
        cls.meta_patch = meta_patch
        cls.meta_delete = meta_delete

Bases: Struct

OpenAPI metadata shared by all of a Resource's operations.

tags are the groups every operation belongs to — a bare str name or a Tag that defines it with a description (see :class:EndpointMeta). responses declares extra/override responses applied to every operation (a blanket 401, say); a per-operation OperationMeta overrides it. exceptions declares jero error classes every operation can raise — derived entirely from the class; a per-operation OperationMeta extends it (both remain raiseable).

Source code in jero/_wiring_types.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
class ResourceMeta(Struct):
    """OpenAPI metadata shared by all of a ``Resource``'s operations.

    ``tags`` are the groups every operation belongs to — a bare ``str`` name or a ``Tag``
    that defines it with a description (see :class:`EndpointMeta`). ``responses`` declares
    extra/override responses applied to every operation (a blanket ``401``, say); a
    per-operation ``OperationMeta`` overrides it. ``exceptions`` declares jero error
    classes every operation can raise — derived entirely from the class; a
    per-operation ``OperationMeta`` *extends* it (both remain raiseable).
    """

    tags: Sequence[str | Tag] = ()
    responses: Sequence[ResponseSpec] = ()
    exceptions: Sequence[type[BaseHTTPError]] = ()

Bases: Struct

OpenAPI metadata shared by all of an Endpoint's operations.

tags are the groups this endpoint belongs to. An entry is a bare str (the tag name — the OpenAPI operation-tag shape; it picks up a description if one is defined for that name, else stands alone) or a Tag to define the name with a description inline (hoisted to the document's tag list). responses declares extra/override responses applied to every operation (a blanket 401, say); a per-operation OperationMeta overrides it. exceptions declares jero error classes every operation can raise — their status, body schema, and description derive from the class; a per-operation OperationMeta extends it (both remain raiseable).

Source code in jero/_wiring_types.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
class EndpointMeta(Struct):
    """OpenAPI metadata shared by all of an ``Endpoint``'s operations.

    ``tags`` are the groups this endpoint belongs to. An entry is a bare ``str`` (the tag
    name — the OpenAPI operation-tag shape; it picks up a description if one is defined for
    that name, else stands alone) or a ``Tag`` to define the name *with* a description
    inline (hoisted to the document's tag list). ``responses`` declares extra/override
    responses applied to every operation (a blanket ``401``, say); a per-operation
    ``OperationMeta`` overrides it. ``exceptions`` declares jero error classes every
    operation can raise — their status, body schema, and description derive from the
    class; a per-operation ``OperationMeta`` *extends* it (both remain raiseable).
    """

    tags: Sequence[str | Tag] = ()
    responses: Sequence[ResponseSpec] = ()
    exceptions: Sequence[type[BaseHTTPError]] = ()

Bases: Struct

OpenAPI metadata for a single operation (meta_get, meta_create, …).

operation_id lives here, never on the class-level meta — operation ids must be unique, so they can't sensibly cascade to every operation. summary / description are the operation's prose (explicit — docstrings are never published). responses declares extra responses or overrides a derived one by reusing its status. exceptions declares jero error classes this operation can raise — status, body schema, and description all derive from the class; entries extend the class-level meta's (both remain raiseable), several sharing a status document as a oneOf, and an explicit responses entry for the same status wins.

tags (bare str names or describing Tag\ s) cascade from the class-level meta by the container type: a list extends the class tags (meta_get=OperationMeta(tags=["unsafe"]) -> class tags + unsafe), a non-empty tuple replaces them (tags=("admin",) -> just admin); the default () inherits.

Source code in jero/_wiring_types.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
class OperationMeta(Struct):
    """OpenAPI metadata for a single operation (``meta_get``, ``meta_create``, …).

    ``operation_id`` lives here, never on the class-level ``meta`` — operation ids must
    be unique, so they can't sensibly cascade to every operation. ``summary`` /
    ``description`` are the operation's prose (explicit — docstrings are never published).
    ``responses`` declares extra responses or overrides a derived one by reusing its status.
    ``exceptions`` declares jero error classes this operation can raise — status, body
    schema, and description all derive from the class; entries *extend* the class-level
    ``meta``'s (both remain raiseable), several sharing a status document as a ``oneOf``,
    and an explicit ``responses`` entry for the same status wins.

    ``tags`` (bare ``str`` names or describing ``Tag``\\ s) cascade from the class-level
    ``meta`` by the *container type*: a ``list`` extends the class tags
    (``meta_get=OperationMeta(tags=["unsafe"])`` -> class tags + ``unsafe``), a non-empty
    ``tuple`` replaces them (``tags=("admin",)`` -> just ``admin``); the default ``()``
    inherits.
    """

    tags: Sequence[str | Tag] = ()
    operation_id: str | None = None
    summary: str | None = None
    description: str | None = None
    responses: Sequence[ResponseSpec] = ()
    exceptions: Sequence[type[BaseHTTPError]] = ()

Bases: Struct

A response to document on an operation, declared in *Meta.responses.

Use it for responses the framework can't infer — a domain 409, a 429, a richer error model — or to override a derived entry by reusing its status.

model is the response body Struct. content_type defaults to application/json when a model is given; set it (with no model) to document a schemaless body of another media type (e.g. text/csv). With neither, the response has no body.

Source code in jero/openapi.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class ResponseSpec(Struct):
    """A response to document on an operation, declared in ``*Meta.responses``.

    Use it for responses the framework can't infer — a domain ``409``, a ``429``, a
    richer error model — or to override a derived entry by reusing its ``status``.

    ``model`` is the response body Struct. ``content_type`` defaults to
    ``application/json`` when a ``model`` is given; set it (with no ``model``) to document
    a schemaless body of another media type (e.g. ``text/csv``). With neither, the
    response has no body.
    """

    status: int
    description: str
    model: type[Struct] | None = None
    content_type: str | None = None

Bases: Struct

A document-level tag — the only place a tag carries a description (docs UIs render it as the blurb under the tag's section). Declare them on _include_openapi(tags=...) to describe and order the groups; operations reference a tag by its name.

Source code in jero/openapi.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
class Tag(Struct):
    """A document-level tag — the only place a tag carries a ``description`` (docs UIs render
    it as the blurb under the tag's section). Declare them on ``_include_openapi(tags=...)``
    to describe and order the groups; operations reference a tag by its ``name``."""

    name: str
    description: str | None = None

    def to_openapi(self) -> dict[str, Any]:
        """Render the OpenAPI ``tags`` entry for this tag."""
        entry: dict[str, Any] = {"name": self.name}
        if self.description is not None:
            entry["description"] = self.description
        return entry

to_openapi()

Render the OpenAPI tags entry for this tag.

Source code in jero/openapi.py
148
149
150
151
152
153
def to_openapi(self) -> dict[str, Any]:
    """Render the OpenAPI ``tags`` entry for this tag."""
    entry: dict[str, Any] = {"name": self.name}
    if self.description is not None:
        entry["description"] = self.description
    return entry

Requests

Bases: Struct

The read-only, typed request view a middleware hook receives.

headers is the hook's own Struct: annotate request: Request[MyHeaders] and wiring compiles a scanner that binds exactly the header keys the Struct's fields name (origin -> origin, x_trace_id -> x-trace-id), exactly like auth's headers parameter. Give a field a | None = None default when the header may be absent; a missing required header is a 400.

method is the wire method — a HEAD request reads "HEAD" even though routing serves it from a GET handler. received_at is time.perf_counter() at dispatch, stamped only on routes a dynamic hook covers (0.0 otherwise).

Source code in jero/_middleware.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Request[H: Struct = NoHeaders](Struct, frozen=True):
    """The read-only, typed request view a middleware hook receives.

    ``headers`` is the *hook's own* Struct: annotate ``request: Request[MyHeaders]``
    and wiring compiles a scanner that binds exactly the header keys the Struct's
    fields name (``origin`` -> ``origin``, ``x_trace_id`` -> ``x-trace-id``), exactly
    like auth's ``headers`` parameter. Give a field a ``| None = None`` default when
    the header may be absent; a missing required header is a 400.

    ``method`` is the wire method — a HEAD request reads ``"HEAD"`` even though
    routing serves it from a GET handler. ``received_at`` is ``time.perf_counter()``
    at dispatch, stamped only on routes a dynamic hook covers (``0.0`` otherwise).
    """

    method: HTTPMethod
    path: str
    headers: H
    received_at: float

Immutable, case-insensitive view of the request headers, preserving as-sent names and order.

For forwarding the whole header bag upstream or for diagnostics — not for reading values you act on (model those in a typed headers Struct). Lookups are case-insensitive (raw["X-Trace-Id"] == raw["x-traceid"]); iteration, items and repr keep the casing as sent. Registers as Mapping[str, str] so it drops straight into niquests(headers=...); pass :meth:multi_items instead when repeated headers must survive.

Source code in jero/headers.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@dataclass(frozen=True, slots=True, repr=False)
class RawHeaders:
    """Immutable, case-insensitive view of the request headers, preserving as-sent
    names and order.

    For forwarding the whole header bag upstream or for diagnostics — *not* for
    reading values you act on (model those in a typed ``headers`` Struct). Lookups
    are case-insensitive (``raw["X-Trace-Id"] == raw["x-traceid"]``); iteration,
    ``items`` and ``repr`` keep the casing as sent. Registers as
    ``Mapping[str, str]`` so it drops straight into ``niquests(headers=...)``; pass
    :meth:`multi_items` instead when repeated headers must survive.
    """

    _pairs: list[tuple[str, str]]  # decoded, original casing, in order

    def _unique(self) -> list[tuple[str, str]]:
        """First-seen pair for each name, compared case-insensitively (Mapping contract)."""
        seen: set[str] = set()
        out: list[tuple[str, str]] = []
        for name, value in self._pairs:
            lower = name.lower()
            if lower not in seen:
                seen.add(lower)
                out.append((name, value))
        return out

    def __getitem__(self, key: str) -> str:
        lower = key.lower()
        for name, value in self._pairs:
            if name.lower() == lower:
                return value
        raise KeyError(key)

    def get(self, key: str, default: str | None = None) -> str | None:
        """The first value for ``key`` (case-insensitive), or ``default`` if absent."""
        try:
            return self[key]
        except KeyError:
            return default

    def getlist(self, key: str) -> list[str]:
        """Every value sent under ``key`` (case-insensitive), in order."""
        lower = key.lower()
        return [value for name, value in self._pairs if name.lower() == lower]

    def __contains__(self, key: object) -> bool:
        if not isinstance(key, str):
            return False
        lower = key.lower()
        return any(name.lower() == lower for name, _ in self._pairs)

    def __iter__(self) -> Iterator[str]:
        return (name for name, _ in self._unique())

    def keys(self) -> list[str]:
        """Unique header names, first-seen casing."""
        return [name for name, _ in self._unique()]

    def values(self) -> list[str]:
        """The value of the first occurrence of each unique header name."""
        return [value for _, value in self._unique()]

    def items(self) -> list[tuple[str, str]]:
        """First-seen ``(name, value)`` pair per unique header name (Mapping contract)."""
        return self._unique()

    def multi_items(self) -> list[tuple[str, str]]:
        """Every header pair, repeats included — use for faithful forwarding."""
        return list(self._pairs)

    def __len__(self) -> int:
        return len(self._unique())

    def __repr__(self) -> str:
        return f"RawHeaders({self._pairs!r})"

_unique()

First-seen pair for each name, compared case-insensitively (Mapping contract).

Source code in jero/headers.py
22
23
24
25
26
27
28
29
30
31
def _unique(self) -> list[tuple[str, str]]:
    """First-seen pair for each name, compared case-insensitively (Mapping contract)."""
    seen: set[str] = set()
    out: list[tuple[str, str]] = []
    for name, value in self._pairs:
        lower = name.lower()
        if lower not in seen:
            seen.add(lower)
            out.append((name, value))
    return out

get(key, default=None)

The first value for key (case-insensitive), or default if absent.

Source code in jero/headers.py
40
41
42
43
44
45
def get(self, key: str, default: str | None = None) -> str | None:
    """The first value for ``key`` (case-insensitive), or ``default`` if absent."""
    try:
        return self[key]
    except KeyError:
        return default

getlist(key)

Every value sent under key (case-insensitive), in order.

Source code in jero/headers.py
47
48
49
50
def getlist(self, key: str) -> list[str]:
    """Every value sent under ``key`` (case-insensitive), in order."""
    lower = key.lower()
    return [value for name, value in self._pairs if name.lower() == lower]

items()

First-seen (name, value) pair per unique header name (Mapping contract).

Source code in jero/headers.py
69
70
71
def items(self) -> list[tuple[str, str]]:
    """First-seen ``(name, value)`` pair per unique header name (Mapping contract)."""
    return self._unique()

keys()

Unique header names, first-seen casing.

Source code in jero/headers.py
61
62
63
def keys(self) -> list[str]:
    """Unique header names, first-seen casing."""
    return [name for name, _ in self._unique()]

multi_items()

Every header pair, repeats included — use for faithful forwarding.

Source code in jero/headers.py
73
74
75
def multi_items(self) -> list[tuple[str, str]]:
    """Every header pair, repeats included — use for faithful forwarding."""
    return list(self._pairs)

values()

The value of the first occurrence of each unique header name.

Source code in jero/headers.py
65
66
67
def values(self) -> list[str]:
    """The value of the first occurrence of each unique header name."""
    return [value for _, value in self._unique()]

Bases: Struct

The empty headers Struct — the default H of :class:Request, for hooks that bind no request headers (request: Request is Request[NoHeaders]).

Source code in jero/_middleware.py
45
46
47
class NoHeaders(Struct, frozen=True):
    """The empty headers Struct — the default ``H`` of :class:`Request`, for hooks
    that bind no request headers (``request: Request`` is ``Request[NoHeaders]``)."""

Bases: Struct

One multipart form part with envelope metadata.

Source code in jero/forms.py
11
12
13
14
15
16
17
class FormPart[T, H: Struct | None = None](Struct):
    """One multipart form part with envelope metadata."""

    data: T
    content_type: str | None
    headers: H
    raw_headers: _RawHeaders

Bases: FormPart[bytes, H]

A file upload part with a required filename.

Source code in jero/forms.py
20
21
22
23
class FilePart[H: Struct | None = None](FormPart[bytes, H]):
    """A file upload part with a required filename."""

    filename: str

Responses

Bases: BaseResponse[H]

A Struct encoded as JSON; content-type defaults to application/json.

Source code in jero/core.py
290
291
292
293
294
@dataclass(kw_only=True, slots=True)
class JSONResponse[T: Struct, H: Struct | None = None](BaseResponse[H]):
    """A Struct encoded as JSON; content-type defaults to application/json."""

    json: T

Bases: BaseResponse[H]

Raw bytes; content-type defaults to application/octet-stream.

Source code in jero/core.py
283
284
285
286
287
@dataclass(kw_only=True, slots=True)
class BytesResponse[H: Struct | None = None](BaseResponse[H]):
    """Raw bytes; content-type defaults to application/octet-stream."""

    content: bytes

Bases: BaseResponse[H]

204, no body. Carries typed/raw headers, location, and links like any response — a 204 may legitimately carry a Location or Link (RFC 9110 §15.3.5). At 204 it emits neither content-type nor content-length, whatever headers supplies; override status_code to a status that permits them and the body is still empty, so content-length: 0 frames it. See :func:_no_content_headers.

Source code in jero/core.py
297
298
299
300
301
302
303
@dataclass(kw_only=True, slots=True)
class NoContent[H: Struct | None = None](BaseResponse[H]):
    """204, no body. Carries typed/raw headers, ``location``, and ``links`` like any
    response — a 204 may legitimately carry a ``Location`` or ``Link`` (RFC 9110 §15.3.5).
    At 204 it emits neither ``content-type`` nor ``content-length``, whatever ``headers``
    supplies; override ``status_code`` to a status that permits them and the body is still
    empty, so ``content-length: 0`` frames it. See :func:`_no_content_headers`."""

Bases: BaseResponse[H]

201 + a JSON body, whatever status the verb would otherwise default to.

Deliberately a sibling of :class:JSONResponse rather than a subclass — it promises a status JSONResponse does not, so it is not substitutable for one. As a subclass, -> JSONResponse[T] would statically accept a returned Created and then send the verb's status: a 200 from an object whose type says 201, invisible to every type checker. The repeated json field is the price of that being a type error instead.

Source code in jero/core.py
306
307
308
309
310
311
312
313
314
315
316
317
@dataclass(kw_only=True, slots=True)
class Created[T: Struct, H: Struct | None = None](BaseResponse[H]):
    """201 + a JSON body, whatever status the verb would otherwise default to.

    Deliberately a *sibling* of :class:`JSONResponse` rather than a subclass — it promises
    a status ``JSONResponse`` does not, so it is not substitutable for one. As a subclass,
    ``-> JSONResponse[T]`` would statically accept a returned ``Created`` and then send the
    verb's status: a 200 from an object whose type says 201, invisible to every type
    checker. The repeated ``json`` field is the price of that being a type error instead.
    """

    json: T

Bases: BaseResponse[H]

202 + a JSON body, whatever status the verb would otherwise default to.

A sibling of :class:JSONResponse, not a subclass, for the reason given on :class:Created.

Source code in jero/core.py
320
321
322
323
324
325
326
327
@dataclass(kw_only=True, slots=True)
class Accepted[T: Struct, H: Struct | None = None](BaseResponse[H]):
    """202 + a JSON body, whatever status the verb would otherwise default to.

    A sibling of :class:`JSONResponse`, not a subclass, for the reason given on
    :class:`Created`."""

    json: T

An RFC 9110 Location on a response — 201 Created, a redirect target, or the status URL on a 202. Build with a constructor; resolution happens at response send.

Source code in jero/links.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
@dataclass(frozen=True, slots=True)
class Location:
    """An RFC 9110 ``Location`` on a response — 201 Created, a redirect target, or the
    status URL on a 202. Build with a constructor; resolution happens at response send."""

    target: Target

    @classmethod
    def from_operation(
        cls, operation: Callable[..., object], *, path: Struct | None = None
    ) -> Self:
        """Point at a mounted operation; ``path`` (type-checked here) fills its slots."""
        _validate_operation_path(operation, path)
        return cls(OperationTarget(operation, path))

    @classmethod
    def from_url(cls, url: str) -> Self:
        """Point at a fully-qualified URL, used verbatim — never rewritten."""
        return cls(URLTarget(url))

    @classmethod
    def from_path(cls, path: str) -> Self:
        """Point at a root-relative path; it picks up the app's URL base (absolute origin /
        prefix) the same way a reversed operation does."""
        return cls(PathTarget(path))

    @classmethod
    def from_ref(cls, ref: str, *, path: Struct | None = None) -> Self:
        """Point at an operation by its class ``ref`` (``"name.operation"``) — the
        import-cycle hatch; prefer ``from_operation`` otherwise."""
        return cls(_parse_ref(ref, path))

from_operation(operation, *, path=None) classmethod

Point at a mounted operation; path (type-checked here) fills its slots.

Source code in jero/links.py
121
122
123
124
125
126
127
@classmethod
def from_operation(
    cls, operation: Callable[..., object], *, path: Struct | None = None
) -> Self:
    """Point at a mounted operation; ``path`` (type-checked here) fills its slots."""
    _validate_operation_path(operation, path)
    return cls(OperationTarget(operation, path))

from_path(path) classmethod

Point at a root-relative path; it picks up the app's URL base (absolute origin / prefix) the same way a reversed operation does.

Source code in jero/links.py
134
135
136
137
138
@classmethod
def from_path(cls, path: str) -> Self:
    """Point at a root-relative path; it picks up the app's URL base (absolute origin /
    prefix) the same way a reversed operation does."""
    return cls(PathTarget(path))

from_ref(ref, *, path=None) classmethod

Point at an operation by its class ref ("name.operation") — the import-cycle hatch; prefer from_operation otherwise.

Source code in jero/links.py
140
141
142
143
144
@classmethod
def from_ref(cls, ref: str, *, path: Struct | None = None) -> Self:
    """Point at an operation by its class ``ref`` (``"name.operation"``) — the
    import-cycle hatch; prefer ``from_operation`` otherwise."""
    return cls(_parse_ref(ref, path))

from_url(url) classmethod

Point at a fully-qualified URL, used verbatim — never rewritten.

Source code in jero/links.py
129
130
131
132
@classmethod
def from_url(cls, url: str) -> Self:
    """Point at a fully-qualified URL, used verbatim — never rewritten."""
    return cls(URLTarget(url))

An RFC 8288 web link. A list of links joins into one Link header. rel is required; title and media_type (emitted as type=) are optional.

Source code in jero/links.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
@dataclass(frozen=True, slots=True)
class Link:
    """An RFC 8288 web link. A list of links joins into one ``Link`` header. ``rel`` is
    required; ``title`` and ``media_type`` (emitted as ``type=``) are optional."""

    target: Target
    rel: str
    title: str | None = None
    media_type: str | None = None

    @classmethod
    def from_operation(
        cls,
        operation: Callable[..., object],
        *,
        rel: str,
        path: Struct | None = None,
        title: str | None = None,
        media_type: str | None = None,
    ) -> Self:
        """Link to a mounted operation with relation ``rel``; ``path`` fills its slots."""
        _validate_operation_path(operation, path)
        return cls(OperationTarget(operation, path), rel, title, media_type)

    @classmethod
    def from_url(
        cls, url: str, *, rel: str, title: str | None = None, media_type: str | None = None
    ) -> Self:
        """Link to a fully-qualified URL, used verbatim — never rewritten."""
        return cls(URLTarget(url), rel, title, media_type)

    @classmethod
    def from_path(
        cls, path: str, *, rel: str, title: str | None = None, media_type: str | None = None
    ) -> Self:
        """Link to a root-relative path; it picks up the app's URL base the same way a
        reversed operation does."""
        return cls(PathTarget(path), rel, title, media_type)

    @classmethod
    def from_ref(
        cls,
        ref: str,
        *,
        rel: str,
        path: Struct | None = None,
        title: str | None = None,
        media_type: str | None = None,
    ) -> Self:
        """Link to an operation by its class ``ref`` (``"name.operation"``) — the
        import-cycle hatch; prefer ``from_operation`` otherwise."""
        return cls(_parse_ref(ref, path), rel, title, media_type)

from_operation(operation, *, rel, path=None, title=None, media_type=None) classmethod

Link to a mounted operation with relation rel; path fills its slots.

Source code in jero/links.py
157
158
159
160
161
162
163
164
165
166
167
168
169
@classmethod
def from_operation(
    cls,
    operation: Callable[..., object],
    *,
    rel: str,
    path: Struct | None = None,
    title: str | None = None,
    media_type: str | None = None,
) -> Self:
    """Link to a mounted operation with relation ``rel``; ``path`` fills its slots."""
    _validate_operation_path(operation, path)
    return cls(OperationTarget(operation, path), rel, title, media_type)

from_path(path, *, rel, title=None, media_type=None) classmethod

Link to a root-relative path; it picks up the app's URL base the same way a reversed operation does.

Source code in jero/links.py
178
179
180
181
182
183
184
@classmethod
def from_path(
    cls, path: str, *, rel: str, title: str | None = None, media_type: str | None = None
) -> Self:
    """Link to a root-relative path; it picks up the app's URL base the same way a
    reversed operation does."""
    return cls(PathTarget(path), rel, title, media_type)

from_ref(ref, *, rel, path=None, title=None, media_type=None) classmethod

Link to an operation by its class ref ("name.operation") — the import-cycle hatch; prefer from_operation otherwise.

Source code in jero/links.py
186
187
188
189
190
191
192
193
194
195
196
197
198
@classmethod
def from_ref(
    cls,
    ref: str,
    *,
    rel: str,
    path: Struct | None = None,
    title: str | None = None,
    media_type: str | None = None,
) -> Self:
    """Link to an operation by its class ``ref`` (``"name.operation"``) — the
    import-cycle hatch; prefer ``from_operation`` otherwise."""
    return cls(_parse_ref(ref, path), rel, title, media_type)

from_url(url, *, rel, title=None, media_type=None) classmethod

Link to a fully-qualified URL, used verbatim — never rewritten.

Source code in jero/links.py
171
172
173
174
175
176
@classmethod
def from_url(
    cls, url: str, *, rel: str, title: str | None = None, media_type: str | None = None
) -> Self:
    """Link to a fully-qualified URL, used verbatim — never rewritten."""
    return cls(URLTarget(url), rel, title, media_type)

Streaming

Bases: _StreamingResponse[bytes, H]

A response streamed as raw bytes chunks (application/octet-stream by default; override via raw_headers).

Source code in jero/streaming.py
50
51
52
53
@dataclass(kw_only=True, slots=True)
class StreamingResponse[H: Struct | None = None](_StreamingResponse[bytes, H]):
    """A response streamed as raw ``bytes`` chunks (``application/octet-stream`` by
    default; override via ``raw_headers``)."""

Bases: _StreamingResponse[T, H]

A response streamed as newline-delimited JSON — one T Struct per line (application/x-ndjson).

Source code in jero/streaming.py
56
57
58
59
@dataclass(kw_only=True, slots=True)
class NDJSONStreamingResponse[T: Struct, H: Struct | None = None](_StreamingResponse[T, H]):
    """A response streamed as newline-delimited JSON — one ``T`` Struct per line
    (``application/x-ndjson``)."""

Bases: _StreamingResponse[T | ServerSentEvent[T], H]

A Server-Sent Events response (text/event-stream, GET-only). Yield a Struct/str (sent as data) or a :class:ServerSentEvent. keepalive, if set, emits a comment ping every N idle seconds.

Source code in jero/streaming.py
62
63
64
65
66
67
68
69
70
@dataclass(kw_only=True, slots=True)
class SSEResponse[T: Struct | str = str, H: Struct | None = None](
    _StreamingResponse[T | ServerSentEvent[T], H]
):
    """A Server-Sent Events response (``text/event-stream``, GET-only). Yield a
    Struct/str (sent as ``data``) or a :class:`ServerSentEvent`. ``keepalive``, if
    set, emits a comment ping every N idle seconds."""

    keepalive: float | None = None

One Server-Sent Event. Yield from an :class:SSEResponse stream to control the event / id / retry fields; data is a Struct (encoded as JSON) or a raw str.

Source code in jero/streaming.py
17
18
19
20
21
22
23
24
25
26
@dataclass(kw_only=True, slots=True)
class ServerSentEvent[T: Struct | str]:
    """One Server-Sent Event. Yield from an :class:`SSEResponse` stream to control
    the ``event`` / ``id`` / ``retry`` fields; ``data`` is a Struct (encoded as
    JSON) or a raw ``str``."""

    data: T
    event: str | None = None
    id: str | None = None
    retry: int | None = None

Cookies

One Set-Cookie response header, secure by default.

A bare SetCookie("session", token) is already Path=/; Secure; HttpOnly; SameSite=Lax — loosening any of that (http_only=False for a JS-readable cookie, secure=False) is explicit and visible in review. Modern browsers treat http://localhost as a trustworthy origin, so Secure cookies still work in local dev.

Validation runs at construction (ValueError on the offending attribute), not at emission — a rejected cookie never becomes a route's problem.

Source code in jero/cookies.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
@dataclass(frozen=True, slots=True)
class SetCookie:
    """One ``Set-Cookie`` response header, secure by default.

    A bare ``SetCookie("session", token)`` is already ``Path=/; Secure; HttpOnly;
    SameSite=Lax`` — loosening any of that (``http_only=False`` for a JS-readable
    cookie, ``secure=False``) is explicit and visible in review. Modern browsers treat
    ``http://localhost`` as a trustworthy origin, so ``Secure`` cookies still work in
    local dev.

    Validation runs at construction (``ValueError`` on the offending attribute), not at
    emission — a rejected cookie never becomes a route's problem.
    """

    name: str
    value: str = ""
    _: KW_ONLY
    max_age: int | None = None
    expires: datetime | None = None
    path: str | None = "/"
    domain: str | None = None
    secure: bool = True
    http_only: bool = True
    same_site: Literal["strict", "lax", "none"] | None = "lax"
    partitioned: bool = False

    def __post_init__(self) -> None:
        _validate_name(self.name)
        _validate_value(self.value)
        if self.max_age is not None and (
            isinstance(self.max_age, bool) or not isinstance(self.max_age, int)
        ):
            raise ValueError("SetCookie: max_age must be an int")
        if self.expires is not None and self.expires.tzinfo is None:
            raise ValueError("SetCookie: expires must be timezone-aware")
        if self.path is not None:
            _validate_path(self.path)
        if self.domain is not None:
            _validate_domain(self.domain)
        if self.same_site is not None:
            _validate_same_site(self.same_site)
        if self.same_site == "none" and not self.secure:
            raise ValueError("SetCookie: same_site='none' requires secure=True")
        if self.partitioned and not self.secure:
            raise ValueError("SetCookie: partitioned=True requires secure=True")
        if self.name.startswith("__Host-") and not (
            self.secure and self.path == "/" and self.domain is None
        ):
            raise ValueError(
                "SetCookie: a '__Host-' cookie requires secure=True, path='/', domain=None"
            )
        if self.name.startswith("__Secure-") and not self.secure:
            raise ValueError("SetCookie: a '__Secure-' cookie requires secure=True")

    @classmethod
    def expire(cls, name: str, *, path: str | None = "/", domain: str | None = None) -> "SetCookie":
        """A cookie that clears ``name`` on the browser: empty value, ``Max-Age=0``, and
        ``Expires`` at the Unix epoch (belt and braces for clients that ignore ``Max-Age``).

        ``path``/``domain`` must match the cookie being cleared — a browser only removes a
        cookie whose scope matches exactly.
        """
        return cls(
            name,
            max_age=0,
            expires=datetime.fromtimestamp(0, tz=UTC),
            path=path,
            domain=domain,
        )

expire(name, *, path='/', domain=None) classmethod

A cookie that clears name on the browser: empty value, Max-Age=0, and Expires at the Unix epoch (belt and braces for clients that ignore Max-Age).

path/domain must match the cookie being cleared — a browser only removes a cookie whose scope matches exactly.

Source code in jero/cookies.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
@classmethod
def expire(cls, name: str, *, path: str | None = "/", domain: str | None = None) -> "SetCookie":
    """A cookie that clears ``name`` on the browser: empty value, ``Max-Age=0``, and
    ``Expires`` at the Unix epoch (belt and braces for clients that ignore ``Max-Age``).

    ``path``/``domain`` must match the cookie being cleared — a browser only removes a
    cookie whose scope matches exactly.
    """
    return cls(
        name,
        max_age=0,
        expires=datetime.fromtimestamp(0, tz=UTC),
        path=path,
        domain=domain,
    )

Errors

Bases: Exception

The abstract root of jero's API errors: the HTTP contract without a wire body.

Every concrete error declares status as a class option — it drives the response status line and the OpenAPI docs. Subclass one of the two families, never this root directly: :class:HTTPError (RFC 9457 Problem Details, the blessed default) or :class:StructHTTPError (bring your own body Struct). except BaseHTTPError means "any jero error"; except HTTPError catches only the Problem family.

Source code in jero/errors.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class BaseHTTPError(Exception):
    """The abstract root of jero's API errors: the HTTP contract without a wire body.

    Every concrete error declares ``status`` as a class option — it drives the response
    status line and the OpenAPI docs. Subclass one of the two families, never this root
    directly: :class:`HTTPError` (RFC 9457 Problem Details, the blessed default) or
    :class:`StructHTTPError` (bring your own body Struct). ``except BaseHTTPError``
    means "any jero error"; ``except HTTPError`` catches only the Problem family.
    """

    status: ClassVar[int]

    def __init_subclass__(cls, *, status: object = None, _abstract: bool = False) -> None:
        super().__init_subclass__()
        if _abstract:
            return
        if BaseHTTPError in cls.__bases__:
            raise TypeError(
                f"{cls.__name__} subclasses BaseHTTPError directly; subclass HTTPError "
                "(Problem Details) or StructHTTPError (your own body Struct) instead",
            )
        if status is None:
            raise TypeError(f"{cls.__name__} is missing required class option 'status'")
        if not isinstance(status, int) or isinstance(status, bool) or not 400 <= status <= 599:
            raise TypeError(f"{cls.__name__} status must be an integer from 400 through 599")
        cls.status = status

    @property
    def response_body(self) -> Struct:
        """The Struct the framework encodes as this error's response body."""
        raise NotImplementedError

response_body property

The Struct the framework encodes as this error's response body.

Bases: BaseHTTPError

A static typed API error rendered as RFC 9457 Problem Details (the blessed default).

Subclasses declare their stable contract as class options::

class AuthenticationRequiredError(
    HTTPError,
    type="authentication-required",
    title="Authentication required",
    status=401,
): ...
Source code in jero/errors.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
class HTTPError(BaseHTTPError, _abstract=True):
    """A static typed API error rendered as RFC 9457 Problem Details (the blessed default).

    Subclasses declare their stable contract as class options::

        class AuthenticationRequiredError(
            HTTPError,
            type="authentication-required",
            title="Authentication required",
            status=401,
        ): ...
    """

    type: ClassVar[str]
    title: ClassVar[str]
    docs: ClassVar[str | None]

    def __init_subclass__(cls, **options: object) -> None:
        abstract = options.pop("_abstract", False)
        if abstract is True:
            super().__init_subclass__(_abstract=True)
            return

        error_type = _class_option(options, "type", "HTTPError")
        title = _class_option(options, "title", "HTTPError")
        status = _class_option(options, "status", "HTTPError")
        docs = options.pop("docs", None)
        if options:
            names = ", ".join(sorted(options))
            raise TypeError(f"unexpected HTTPError class option(s): {names}")
        if not isinstance(error_type, str) or not error_type.strip():
            raise TypeError("HTTPError type must be a non-blank string")
        if not isinstance(title, str) or not title:
            raise TypeError("HTTPError title must be a non-empty string")
        if docs is not None and not isinstance(docs, str):
            raise TypeError("HTTPError docs must be a string or None")

        super().__init_subclass__(status=status)
        cls.type = error_type
        cls.title = title
        cls.docs = docs

    def __init__(self) -> None:
        if not hasattr(type(self), "title"):
            raise TypeError("HTTPError must be subclassed with type, title, and status")
        super().__init__(self.title)

    @property
    def problem(self) -> Problem:
        """Build the typed wire body for this error occurrence."""
        return Problem(type=self.type, title=self.title, status=self.status, docs=self.docs)

    @property
    def response_body(self) -> Problem:
        """The Struct the framework encodes: this family's Problem body."""
        return self.problem

problem property

Build the typed wire body for this error occurrence.

response_body property

The Struct the framework encodes: this family's Problem body.

Bases: ParameterizedHTTPError[P], ABC

The ergonomic parameterized-error base used by dataclass error subclasses.

Source code in jero/errors.py
289
290
291
292
293
294
class DataclassHTTPError[P: Struct](ParameterizedHTTPError[P], ABC, _abstract=True):
    """The ergonomic parameterized-error base used by dataclass error subclasses."""

    @abstractmethod
    def __post_init__(self) -> None:
        """Build the params Struct by calling ``self._set_params(...)``."""

Bases: HTTPError

An API error whose detail is rendered from a typed params Struct.

Source code in jero/errors.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
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
class ParameterizedHTTPError[P: Struct](HTTPError, _abstract=True):
    """An API error whose detail is rendered from a typed params Struct."""

    detail_template: ClassVar[str]
    params_type: ClassVar[type[Struct]]

    params: P
    detail: str

    def __init_subclass__(
        cls,
        *,
        detail_template: str | None = None,
        **options: object,
    ) -> None:
        abstract = options.get("_abstract") is True
        super().__init_subclass__(**options)
        if abstract:
            return
        if detail_template is None:
            raise TypeError("ParameterizedHTTPError subclass requires detail_template")
        params_type = _resolve_params_type(cls)
        if params_type is None:
            raise TypeError("ParameterizedHTTPError subclass requires a concrete params Struct")

        param_names = {field.name for field in fields(params_type)}
        template_names = _template_placeholders(detail_template)
        if not template_names:
            raise TypeError("detail_template must reference at least one params field")
        unknown = template_names - param_names
        if unknown:
            names = ", ".join(sorted(unknown))
            raise TypeError(f"detail_template references unknown params field(s): {names}")

        cls.detail_template = detail_template
        cls.params_type = params_type

    def __init__(self, params: P) -> None:
        super().__init__()
        self._set_params(params)

    def _set_params(self, params: P) -> None:
        if not isinstance(params, self.params_type):
            raise TypeError(
                f"{type(self).__name__} params must be {self.params_type.__name__}, "
                f"got {type(params).__name__}",
            )
        self.params = params
        self.detail = self.detail_template.format(**asdict(params))
        Exception.__init__(self, self.detail)

    @property
    def problem(self) -> ParameterizedProblem[P]:
        """Build the typed wire body for this error occurrence."""
        return ParameterizedProblem(
            type=self.type,
            title=self.title,
            status=self.status,
            docs=self.docs,
            detail=self.detail,
            params=self.params,
        )

problem property

Build the typed wire body for this error occurrence.

Bases: BaseHTTPError

An API error generic over your own wire Struct B — the bring-your-own-body family. Class options declare how every field of B gets its value:

  • consts={"field": value} — a pinned constant: on the wire exactly as given, and an enum const in the OpenAPI schema.
  • templates={"field": "..."} — rendered at raise time from the raise-time params ({{brace}} escaping ships literal braces, per str.format).
  • status_field="field" — an existing int field fed the class's status (an enum const in the schema).
  • params_field="field" — a Struct-typed field the raise-time params nest into.
  • anything left over — fed by a same-named raise-time param.

Total coverage is enforced loud at class creation: every field of B has exactly one source. Raise-time params are one flat namespace (template placeholders, nested params-Struct fields, and leftover body fields); pass them as keyword arguments, or — the blessed, statically-typed form — decorate the subclass with @dataclass and declare them as fields, so the generated __init__ carries real names and types::

class QuotaBody(Struct, rename="camel"):
    error_code: str
    error_message: str
    status_code: int

@dataclass
class QuotaExceededError(
    StructHTTPError[QuotaBody],
    status=429,
    description="Quota exceeded",
    consts={"error_code": "quota-exceeded"},
    templates={"error_message": "Limit is {limit} requests per {window}"},
    status_field="status_code",
):
    limit: int
    window: str

raise QuotaExceededError(limit=100, window="minute")
# 429 {"errorCode": "quota-exceeded",
#      "errorMessage": "Limit is 100 requests per minute", "statusCode": 429}

description is the OpenAPI response description (explicit — docstrings are never published). The wire model — B's shape with const-fed fields narrowed to Literal types — is composed once here at class creation; the request path is construct-and-encode, and nothing you pass is ever mutated.

Source code in jero/errors.py
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
class StructHTTPError[B: Struct](BaseHTTPError, _abstract=True):
    """An API error generic over your own wire Struct ``B`` — the bring-your-own-body
    family. Class options declare how *every* field of ``B`` gets its value:

    - ``consts={"field": value}`` — a pinned constant: on the wire exactly as given,
      and an enum const in the OpenAPI schema.
    - ``templates={"field": "..."}`` — rendered at raise time from the raise-time params
      (``{{brace}}`` escaping ships literal braces, per ``str.format``).
    - ``status_field="field"`` — an existing int field fed the class's ``status``
      (an enum const in the schema).
    - ``params_field="field"`` — a Struct-typed field the raise-time params nest into.
    - anything left over — fed by a same-named raise-time param.

    Total coverage is enforced loud at class creation: every field of ``B`` has exactly
    one source. Raise-time params are one flat namespace (template placeholders, nested
    params-Struct fields, and leftover body fields); pass them as keyword arguments, or —
    the blessed, statically-typed form — decorate the subclass with ``@dataclass`` and
    declare them as fields, so the generated ``__init__`` carries real names and types::

        class QuotaBody(Struct, rename="camel"):
            error_code: str
            error_message: str
            status_code: int

        @dataclass
        class QuotaExceededError(
            StructHTTPError[QuotaBody],
            status=429,
            description="Quota exceeded",
            consts={"error_code": "quota-exceeded"},
            templates={"error_message": "Limit is {limit} requests per {window}"},
            status_field="status_code",
        ):
            limit: int
            window: str

        raise QuotaExceededError(limit=100, window="minute")
        # 429 {"errorCode": "quota-exceeded",
        #      "errorMessage": "Limit is 100 requests per minute", "statusCode": 429}

    ``description`` is the OpenAPI response description (explicit — docstrings are never
    published). The wire model — ``B``'s shape with const-fed fields narrowed to
    ``Literal`` types — is composed once here at class creation; the request path is
    construct-and-encode, and nothing you pass is ever mutated.
    """

    description: ClassVar[str]
    body_type: ClassVar[type[Struct]]
    wire_model: ClassVar[type[Struct]]
    consts: ClassVar[dict[str, object]]
    templates: ClassVar[dict[str, str]]
    status_field: ClassVar[str | None]
    params_field: ClassVar[str | None]
    params_struct: ClassVar[type[Struct] | None]
    param_body_fields: ClassVar[tuple[str, ...]]  # body fields fed by a same-named param
    param_names: ClassVar[frozenset[str]]  # all raise-time params (incl. template-only)
    _nested_params: ClassVar[tuple[str, ...]]  # the params_field Struct's field names

    params: dict[str, object]  # the bound raise-time params (set by _bind)

    def __init_subclass__(cls, **options: object) -> None:
        abstract = options.pop("_abstract", False)
        if abstract is True:
            super().__init_subclass__(_abstract=True)
            return

        status, spec = _parse_engine_options(options)
        super().__init_subclass__(status=status)

        body_type = _resolve_struct_arg(cls, StructHTTPError)
        if body_type is None:
            raise TypeError("StructHTTPError subclass requires a concrete body Struct")
        body_fields = {field.name: field for field in fields(body_type)}

        sources = _field_sources(cls.__name__, body_type.__name__, body_fields, spec)
        _validate_text_sources(cls.__name__, body_fields, spec)
        _validate_const_values(cls.__name__, body_fields, spec.consts)
        params_struct, nested_params = _params_nesting(cls.__name__, body_fields, spec)
        param_body_fields = tuple(name for name in body_fields if name not in sources)
        param_names = _validate_coverage(
            cls.__name__, spec, sources, nested_params, param_body_fields
        )
        cls.wire_model = _engine_wire_model(cls, body_type, body_fields, spec, cls.status)

        cls.description = spec.description
        cls.body_type = body_type
        cls.consts = spec.consts
        cls.templates = spec.templates
        cls.status_field = spec.status_field
        cls.params_field = spec.params_field
        cls.params_struct = params_struct
        cls.param_body_fields = param_body_fields
        cls.param_names = param_names
        cls._nested_params = nested_params

    def __init__(self, **params: object) -> None:
        if not hasattr(type(self), "body_type"):
            raise TypeError(
                "StructHTTPError must be subclassed with a body, status, and description"
            )
        expected = type(self).param_names
        missing = expected - set(params)
        unexpected = set(params) - expected
        if missing or unexpected:
            parts: list[str] = []
            if missing:
                parts.append(f"missing: {', '.join(sorted(missing))}")
            if unexpected:
                parts.append(f"unexpected: {', '.join(sorted(unexpected))}")
            raise TypeError(f"{type(self).__name__}() params — {'; '.join(parts)}")
        self._bind(params)

    def __post_init__(self) -> None:
        """The statically-typed tier: called by an ``@dataclass`` subclass's generated
        ``__init__``. The declared dataclass fields ARE the params — validated against
        what the class options require, so a missing or extra field fails on first
        raise."""
        declared = {field.name for field in dataclass_fields(cast(Any, self))}
        expected = set(type(self).param_names)
        if declared != expected:
            missing = ", ".join(sorted(expected - declared)) or "-"
            extra = ", ".join(sorted(declared - expected)) or "-"
            raise TypeError(
                f"{type(self).__name__} dataclass fields must match its params "
                f"(missing: {missing}; extra: {extra})",
            )
        self._bind({name: getattr(self, name) for name in declared})

    def _bind(self, params: dict[str, object]) -> None:
        # object.__setattr__, so frozen @dataclass subclasses can bind too.
        object.__setattr__(self, "params", params)
        Exception.__init__(self, self.description)

    def _variable_values(self) -> dict[str, object]:
        """The variable fields' values: same-named params, rendered templates, and the
        nested params Struct when ``params_field`` is declared."""
        values: dict[str, object] = {name: self.params[name] for name in self.param_body_fields}
        for name, template in self.templates.items():
            values[name] = template.format(**self.params)
        if self.params_field is not None and self.params_struct is not None:
            nested = {name: self.params[name] for name in self._nested_params}
            values[self.params_field] = self.params_struct(**nested)
        return values

    @property
    def body(self) -> B:
        """This occurrence as *your* body type — every field populated (consts, status,
        templates, params), typed as ``B`` for code that inspects the error."""
        values = self._variable_values() | self.consts
        if self.status_field is not None:
            values[self.status_field] = self.status
        return cast("B", self.body_type(**values))

    @property
    def response_body(self) -> Struct:
        """Compose the wire body fresh: params, rendered templates, and the nested params
        Struct; consts and the status arrive through the wire model's ``Literal``
        defaults. Nothing is ever mutated."""
        return self.wire_model(**self._variable_values())

body property

This occurrence as your body type — every field populated (consts, status, templates, params), typed as B for code that inspects the error.

response_body property

Compose the wire body fresh: params, rendered templates, and the nested params Struct; consts and the status arrive through the wire model's Literal defaults. Nothing is ever mutated.

_variable_values()

The variable fields' values: same-named params, rendered templates, and the nested params Struct when params_field is declared.

Source code in jero/errors.py
690
691
692
693
694
695
696
697
698
699
def _variable_values(self) -> dict[str, object]:
    """The variable fields' values: same-named params, rendered templates, and the
    nested params Struct when ``params_field`` is declared."""
    values: dict[str, object] = {name: self.params[name] for name in self.param_body_fields}
    for name, template in self.templates.items():
        values[name] = template.format(**self.params)
    if self.params_field is not None and self.params_struct is not None:
        nested = {name: self.params[name] for name in self._nested_params}
        values[self.params_field] = self.params_struct(**nested)
    return values

Bases: Struct

The wire representation of a static API error.

Source code in jero/errors.py
26
27
28
29
30
31
32
class Problem(Struct, kw_only=True, omit_defaults=True):
    """The wire representation of a static API error."""

    type: str
    title: str
    status: int
    docs: str | None = None

Bases: Problem

The wire representation of an API error with occurrence-specific context.

Source code in jero/errors.py
35
36
37
38
39
class ParameterizedProblem[P: Struct](Problem, kw_only=True, omit_defaults=True):
    """The wire representation of an API error with occurrence-specific context."""

    detail: str
    params: P

Bases: ABC

App-wide renderer for the Problem family: compose your own wire body from any HTTPError — the framework built-ins included — registered via BaseApp._include_error_adapter. StructHTTPError\ s render themselves, so every error has exactly one renderer.

status_field optionally names a field the framework adds (typed to the exact status) when composing the wire body; it must not exist on the body Struct, and the Struct compose returns is never mutated. Keep compose pure — it receives only the error; request-correlated data belongs in exception handlers, not here.

Source code in jero/errors.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
class ErrorBodyAdapter[B: Struct](ABC):
    """App-wide renderer for the Problem family: compose your own wire body from any
    ``HTTPError`` — the framework built-ins included — registered via
    ``BaseApp._include_error_adapter``. ``StructHTTPError``\\ s render themselves, so every
    error has exactly one renderer.

    ``status_field`` optionally names a field the framework *adds* (typed to the exact
    status) when composing the wire body; it must not exist on the body Struct, and the
    Struct ``compose`` returns is never mutated. Keep ``compose`` pure — it receives only
    the error; request-correlated data belongs in exception handlers, not here.
    """

    status_field: ClassVar[str | None] = None
    body_type: ClassVar[type[Struct]]
    _wire_models: ClassVar[dict[int, type[Struct]]]

    def __init_subclass__(cls) -> None:
        super().__init_subclass__()
        body_type = _resolve_struct_arg(cls, ErrorBodyAdapter)
        if body_type is None:
            # A generic intermediate (B still unbound) — concrete subclasses bind and
            # validate; registering an unbound adapter fails at _include_error_adapter.
            return
        cls.body_type = body_type
        cls.status_field = _validated_status_field(cls.status_field, body_type, "ErrorBodyAdapter")
        cls._wire_models = {}

    @abstractmethod
    def compose(self, error: HTTPError) -> B:
        """Build your body from a Problem-family error (its ``type``/``title``/``status``,
        and ``str(error)`` for the human message — the rendered detail when parameterized)."""

    def _wire_model_for(self, status: int) -> type[Struct]:
        """The model encoded/documented for ``status``: the body Struct itself, or — when
        ``status_field`` is declared — the composed per-status model, built once and cached."""
        if self.status_field is None:
            return self.body_type
        model = self._wire_models.get(status)
        if model is None:
            name = f"{self.body_type.__name__}{status}"
            model = _status_wire_model(name, self.body_type, self.status_field, status)
            self._wire_models[status] = model
        return model

    def compose_wire(self, error: HTTPError) -> Struct:
        """``compose`` plus the declared status splice, mutating nothing. Un-underscored:
        core calls it across the module boundary when rendering a Problem-family error."""
        body = self.compose(error)
        if not isinstance(body, self.body_type):
            raise TypeError(
                f"{type(self).__name__}.compose must return {self.body_type.__name__}, "
                f"got {type(body).__name__}",
            )
        if self.status_field is None:
            return body
        # Compose by field name: works for kw_only bodies, and a compose() returning a
        # body *subclass* contributes only the declared fields.
        values: dict[str, object] = {
            field.name: getattr(body, field.name) for field in fields(self.body_type)
        }
        values[self.status_field] = error.status
        return self._wire_model_for(error.status)(**values)

    def docs_model(self, status: int) -> type[Struct]:
        """The wire model documented for errors of ``status`` — what the OpenAPI build
        references for derived error responses once this adapter is registered."""
        return self._wire_model_for(status)

_wire_model_for(status)

The model encoded/documented for status: the body Struct itself, or — when status_field is declared — the composed per-status model, built once and cached.

Source code in jero/errors.py
750
751
752
753
754
755
756
757
758
759
760
def _wire_model_for(self, status: int) -> type[Struct]:
    """The model encoded/documented for ``status``: the body Struct itself, or — when
    ``status_field`` is declared — the composed per-status model, built once and cached."""
    if self.status_field is None:
        return self.body_type
    model = self._wire_models.get(status)
    if model is None:
        name = f"{self.body_type.__name__}{status}"
        model = _status_wire_model(name, self.body_type, self.status_field, status)
        self._wire_models[status] = model
    return model

compose(error) abstractmethod

Build your body from a Problem-family error (its type/title/status, and str(error) for the human message — the rendered detail when parameterized).

Source code in jero/errors.py
745
746
747
748
@abstractmethod
def compose(self, error: HTTPError) -> B:
    """Build your body from a Problem-family error (its ``type``/``title``/``status``,
    and ``str(error)`` for the human message — the rendered detail when parameterized)."""

compose_wire(error)

compose plus the declared status splice, mutating nothing. Un-underscored: core calls it across the module boundary when rendering a Problem-family error.

Source code in jero/errors.py
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
def compose_wire(self, error: HTTPError) -> Struct:
    """``compose`` plus the declared status splice, mutating nothing. Un-underscored:
    core calls it across the module boundary when rendering a Problem-family error."""
    body = self.compose(error)
    if not isinstance(body, self.body_type):
        raise TypeError(
            f"{type(self).__name__}.compose must return {self.body_type.__name__}, "
            f"got {type(body).__name__}",
        )
    if self.status_field is None:
        return body
    # Compose by field name: works for kw_only bodies, and a compose() returning a
    # body *subclass* contributes only the declared fields.
    values: dict[str, object] = {
        field.name: getattr(body, field.name) for field in fields(self.body_type)
    }
    values[self.status_field] = error.status
    return self._wire_model_for(error.status)(**values)

docs_model(status)

The wire model documented for errors of status — what the OpenAPI build references for derived error responses once this adapter is registered.

Source code in jero/errors.py
781
782
783
784
def docs_model(self, status: int) -> type[Struct]:
    """The wire model documented for errors of ``status`` — what the OpenAPI build
    references for derived error responses once this adapter is registered."""
    return self._wire_model_for(status)

Bases: Struct

Occurrence detail for the framework's decode/bind errors: the underlying msgspec (or multipart) message, so a 400/422 says what actually failed rather than nothing. The message is human-readable and names fields/paths, never submitted values — read type/status as the machine contract, not this string.

Source code in jero/errors.py
805
806
807
808
809
810
811
class ErrorReason(Struct):
    """Occurrence detail for the framework's decode/bind errors: the underlying msgspec
    (or multipart) message, so a 400/422 says what actually failed rather than nothing.
    The message is human-readable and names fields/paths, never submitted values — read
    ``type``/``status`` as the machine contract, not this string."""

    reason: str

A typed JSON response returned by a custom exception handler.

Unlike a normal response wrapper, status_code is required: exception handling has no operation-derived success status to fall back to. cookies works exactly as on a normal response — an auth failure that must also expire a stale session cookie is a real case.

Source code in jero/_exception_handlers.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@dataclass(kw_only=True, slots=True)
class ExceptionResponse[T: Struct, H: Struct | None = None]:
    """A typed JSON response returned by a custom exception handler.

    Unlike a normal response wrapper, ``status_code`` is required: exception handling
    has no operation-derived success status to fall back to. ``cookies`` works exactly
    as on a normal response — an auth failure that must also expire a stale session
    cookie is a real case.
    """

    status_code: int
    json: T
    headers: H | None = None
    raw_headers: RawHeaders | Mapping[str, str] | None = None
    location: Location | None = None
    links: Sequence[Link] = ()
    cookies: Sequence[SetCookie] = ()

    def __post_init__(self) -> None:
        if isinstance(self.status_code, bool) or not 400 <= self.status_code <= 599:
            raise ValueError("ExceptionResponse status_code must be from 400 through 599")

Shipped errors

Bases: HTTPError

Authentication credentials are absent or invalid.

Source code in jero/errors.py
834
835
836
837
838
839
840
class AuthenticationRequiredError(
    HTTPError,
    type="authentication-required",
    title="Authentication required",
    status=401,
):
    """Authentication credentials are absent or invalid."""

Bases: HTTPError

The request conflicts with the current state of the resource.

Source code in jero/errors.py
861
862
863
864
865
866
867
class ConflictError(
    HTTPError,
    type="conflict",
    title="The request conflicts with the current state of the resource",
    status=409,
):
    """The request conflicts with the current state of the resource."""

Bases: HTTPError

The caller is authenticated but not allowed to perform this operation.

Source code in jero/errors.py
852
853
854
855
856
857
858
class ForbiddenError(
    HTTPError,
    type="forbidden",
    title="The caller is authenticated but not allowed to perform this operation",
    status=403,
):
    """The caller is authenticated but not allowed to perform this operation."""

Bases: HTTPError

The resource existed but has been permanently removed.

Source code in jero/errors.py
870
871
872
873
874
875
876
class GoneError(
    HTTPError,
    type="gone",
    title="The resource existed but has been permanently removed",
    status=410,
):
    """The resource existed but has been permanently removed."""

Bases: HTTPError

An unexpected server-side failure whose internals are not exposed.

Source code in jero/errors.py
888
889
890
891
892
893
894
class InternalServerError(
    HTTPError,
    type="internal-server-error",
    title="Internal server error",
    status=500,
):
    """An unexpected server-side failure whose internals are not exposed."""

Bases: ParameterizedHTTPError[ErrorReason]

The request cannot be parsed or bound.

Source code in jero/errors.py
814
815
816
817
818
819
820
821
class MalformedRequestError(
    ParameterizedHTTPError[ErrorReason],
    type="malformed-request",
    title="Malformed request",
    status=400,
    detail_template="{reason}",
):
    """The request cannot be parsed or bound."""

Bases: HTTPError

The path exists but does not support the requested method.

Source code in jero/errors.py
796
797
798
799
800
801
802
class MethodNotAllowedError(
    HTTPError,
    type="method-not-allowed",
    title="Method not allowed",
    status=405,
):
    """The path exists but does not support the requested method."""

Bases: HTTPError

No route or resource matches the requested path.

Source code in jero/errors.py
787
788
789
790
791
792
793
class NotFoundError(
    HTTPError,
    type="not-found",
    title="Not found",
    status=404,
):
    """No route or resource matches the requested path."""

Bases: HTTPError

The caller has exceeded a rate limit.

Source code in jero/errors.py
879
880
881
882
883
884
885
class TooManyRequestsError(
    HTTPError,
    type="too-many-requests",
    title="The caller has exceeded a rate limit",
    status=429,
):
    """The caller has exceeded a rate limit."""

Bases: HTTPError

The request body does not use the media type required by the operation.

Source code in jero/errors.py
843
844
845
846
847
848
849
class UnsupportedMediaTypeError(
    HTTPError,
    type="unsupported-media-type",
    title="Unsupported media type",
    status=415,
):
    """The request body does not use the media type required by the operation."""

Bases: ParameterizedHTTPError[ErrorReason]

The request is syntactically valid but does not match its typed contract.

Source code in jero/errors.py
824
825
826
827
828
829
830
831
class ValidationFailedError(
    ParameterizedHTTPError[ErrorReason],
    type="validation-failed",
    title="Validation failed",
    status=422,
    detail_template="{reason}",
):
    """The request is syntactically valid but does not match its typed contract."""

Authentication

Bases: Protocol

Implement authenticate; raise an HTTPError subclass to reject.

headers is bound from the request headers into your declared Struct (header names map x-trace-id -> x_trace_id). The returned Struct is what handlers receive as user.

The return type is the route's auth policy. Declaring -> TUser gates the routes it is mounted on: a caller without valid credentials never reaches a handler. Declaring -> TUser | None makes credentials an input instead — returning None reports that the caller presented none, and the handler is invoked with user=None. Raising is rejection in both cases, so invalid credentials are always a 401. Handlers must match: user: TUser against the first, user: TUser | None against the second, checked at startup.

An app that wants both usually defines two authenticators over one shared resolution step (TokenAuth / OptionalTokenAuth), so which policy a route gets is visible in what its mount passes.

authenticate only sees credentials your THeaders Struct can bind: a Struct whose fields are all required makes a credential-less request a 401 before your code runs. Give the field a | None default (authorization: str | None = None) to have absence reach authenticate and become your decision — the same rule applies to a :class:CookieAuth/:class:HybridAuth cookie field.

Source code in jero/core.py
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
class Auth[THeaders: Struct, TUser: Struct](Protocol):
    """Implement ``authenticate``; raise an ``HTTPError`` subclass to reject.

    ``headers`` is bound from the request headers into your declared
    Struct (header names map ``x-trace-id`` -> ``x_trace_id``). The
    returned Struct is what handlers receive as ``user``.

    **The return type is the route's auth policy.** Declaring ``-> TUser`` gates the routes
    it is mounted on: a caller without valid credentials never reaches a handler. Declaring
    ``-> TUser | None`` makes credentials an *input* instead — returning ``None`` reports
    that the caller presented none, and the handler is invoked with ``user=None``. Raising
    is rejection in both cases, so *invalid* credentials are always a 401. Handlers must
    match: ``user: TUser`` against the first, ``user: TUser | None`` against the second,
    checked at startup.

    An app that wants both usually defines two authenticators over one shared resolution
    step (``TokenAuth`` / ``OptionalTokenAuth``), so which policy a route gets is visible in
    what its mount passes.

    ``authenticate`` only sees credentials your ``THeaders`` Struct can bind: a Struct
    whose fields are all required makes a credential-less request a 401 before your code
    runs. Give the field a ``| None`` default (``authorization: str | None = None``) to
    have absence reach ``authenticate`` and become your decision — the same rule applies
    to a :class:`CookieAuth`/:class:`HybridAuth` cookie field.
    """

    def authenticate(self, headers: THeaders) -> TUser | Awaitable[TUser | None] | None:
        """Validate ``headers`` and return the user Struct; raise ``HTTPError`` to reject.

        Return ``None`` only to report that no credentials were presented (see the class
        docstring); it is never a way to say "these credentials are bad".
        """
        ...  # pylint: disable=unnecessary-ellipsis  # Protocol stub; pyright needs the body

authenticate(headers)

Validate headers and return the user Struct; raise HTTPError to reject.

Return None only to report that no credentials were presented (see the class docstring); it is never a way to say "these credentials are bad".

Source code in jero/core.py
545
546
547
548
549
550
551
def authenticate(self, headers: THeaders) -> TUser | Awaitable[TUser | None] | None:
    """Validate ``headers`` and return the user Struct; raise ``HTTPError`` to reject.

    Return ``None`` only to report that no credentials were presented (see the class
    docstring); it is never a way to say "these credentials are bad".
    """
    ...  # pylint: disable=unnecessary-ellipsis  # Protocol stub; pyright needs the body

Bases: Protocol

Implement authenticate; raise an HTTPError subclass to reject.

Session-cookie auth: cookies binds from the request's Cookie header into your declared Struct, verbatim and case-sensitively (no snake_case mangle, unlike headers — see the cookies binding source). Otherwise identical to :class:Auth: the return type is the route's auth policy (-> TUser gates, -> TUser | None accepts anonymous callers), and both apply on a WebSocket handshake — the motivating case, since a browser's WebSocket API cannot set an Authorization header but always sends cookies.

Source code in jero/core.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
class CookieAuth[TCookies: Struct, TUser: Struct](Protocol):
    """Implement ``authenticate``; raise an ``HTTPError`` subclass to reject.

    Session-cookie auth: ``cookies`` binds from the request's ``Cookie`` header into your
    declared Struct, verbatim and case-sensitively (no snake_case mangle, unlike
    ``headers`` — see the ``cookies`` binding source). Otherwise identical to
    :class:`Auth`: the return type is the route's auth policy (``-> TUser`` gates,
    ``-> TUser | None`` accepts anonymous callers), and both apply on a WebSocket
    handshake — the motivating case, since a browser's WebSocket API cannot set an
    ``Authorization`` header but always sends cookies.
    """

    def authenticate(self, cookies: TCookies) -> TUser | Awaitable[TUser | None] | None:
        """Validate ``cookies`` and return the user Struct; raise ``HTTPError`` to reject.

        Return ``None`` only to report that no credentials were presented (see the class
        docstring); it is never a way to say "these credentials are bad".
        """
        ...  # pylint: disable=unnecessary-ellipsis  # Protocol stub; pyright needs the body

authenticate(cookies)

Validate cookies and return the user Struct; raise HTTPError to reject.

Return None only to report that no credentials were presented (see the class docstring); it is never a way to say "these credentials are bad".

Source code in jero/core.py
566
567
568
569
570
571
572
def authenticate(self, cookies: TCookies) -> TUser | Awaitable[TUser | None] | None:
    """Validate ``cookies`` and return the user Struct; raise ``HTTPError`` to reject.

    Return ``None`` only to report that no credentials were presented (see the class
    docstring); it is never a way to say "these credentials are bad".
    """
    ...  # pylint: disable=unnecessary-ellipsis  # Protocol stub; pyright needs the body

Bases: Protocol

Implement authenticate; raise an HTTPError subclass to reject.

Both headers and cookies bind — one app serving bearer-token API clients and cookie-authenticated browser clients on the same routes. Otherwise identical to :class:Auth: the return type is the route's auth policy.

Source code in jero/core.py
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
class HybridAuth[THeaders: Struct, TCookies: Struct, TUser: Struct](Protocol):
    """Implement ``authenticate``; raise an ``HTTPError`` subclass to reject.

    Both ``headers`` and ``cookies`` bind — one app serving bearer-token API clients and
    cookie-authenticated browser clients on the same routes. Otherwise identical to
    :class:`Auth`: the return type is the route's auth policy.
    """

    def authenticate(
        self, headers: THeaders, cookies: TCookies
    ) -> TUser | Awaitable[TUser | None] | None:
        """Validate ``headers``/``cookies`` and return the user Struct; raise to reject.

        Return ``None`` only to report that no credentials were presented (see the class
        docstring); it is never a way to say "these credentials are bad".
        """
        ...  # pylint: disable=unnecessary-ellipsis  # Protocol stub; pyright needs the body

authenticate(headers, cookies)

Validate headers/cookies and return the user Struct; raise to reject.

Return None only to report that no credentials were presented (see the class docstring); it is never a way to say "these credentials are bad".

Source code in jero/core.py
583
584
585
586
587
588
589
590
591
def authenticate(
    self, headers: THeaders, cookies: TCookies
) -> TUser | Awaitable[TUser | None] | None:
    """Validate ``headers``/``cookies`` and return the user Struct; raise to reject.

    Return ``None`` only to report that no credentials were presented (see the class
    docstring); it is never a way to say "these credentials are bad".
    """
    ...  # pylint: disable=unnecessary-ellipsis  # Protocol stub; pyright needs the body

Bases: Auth[THeaders, TUser]

An Auth whose operations advertise HTTP bearer in the OpenAPI spec.

Sugar over the openapi_security attribute the spec generator reads — subclass this instead of writing the attribute by hand. Implement authenticate as usual.

Source code in jero/core.py
594
595
596
597
598
599
600
601
class BearerAuth[THeaders: Struct, TUser: Struct](Auth[THeaders, TUser]):
    """An ``Auth`` whose operations advertise HTTP bearer in the OpenAPI spec.

    Sugar over the ``openapi_security`` attribute the spec generator reads — subclass
    this instead of writing the attribute by hand. Implement ``authenticate`` as usual.
    """

    openapi_security: ClassVar[SecurityScheme] = SecurityScheme.http_bearer()

Bases: Auth[THeaders, TUser]

An Auth whose operations advertise HTTP basic in the OpenAPI spec.

Source code in jero/core.py
604
605
606
607
class BasicAuth[THeaders: Struct, TUser: Struct](Auth[THeaders, TUser]):
    """An ``Auth`` whose operations advertise HTTP basic in the OpenAPI spec."""

    openapi_security: ClassVar[SecurityScheme] = SecurityScheme.http_basic()

Bases: Struct

One OpenAPI security scheme, declared on an authenticator via the openapi_security class attribute (the :class:~jero.BearerAuth / :class:~jero.BasicAuth bases set it for you).

Build one with a constructor rather than by hand. scheme_name is the key the scheme is registered under in components.securitySchemes and referenced by each operation's security. Note that a bearer token carried in a cookie is not an http/bearer scheme (that is the Authorization header specifically) — it is api_key(location="cookie").

Source code in jero/openapi.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
class SecurityScheme(Struct, frozen=True):
    """One OpenAPI security scheme, declared on an authenticator via the
    ``openapi_security`` class attribute (the :class:`~jero.BearerAuth` /
    :class:`~jero.BasicAuth` bases set it for you).

    Build one with a constructor rather than by hand. ``scheme_name`` is the key the
    scheme is registered under in ``components.securitySchemes`` and referenced by each
    operation's ``security``. Note that a bearer token carried in a *cookie* is not an
    ``http``/``bearer`` scheme (that is the ``Authorization`` header specifically) — it
    is ``api_key(location="cookie")``.
    """

    type: SchemeType
    scheme_name: str
    scheme: str | None = None  # http: "bearer" / "basic"
    bearer_format: str | None = None  # http bearer: e.g. "JWT"
    name: str | None = None  # apiKey: the header / query / cookie name
    location: ApiKeyLocation | None = None  # apiKey: the OpenAPI ``in`` (``in`` is a keyword)
    description: str | None = None

    @classmethod
    def http_bearer(
        cls,
        *,
        bearer_format: str | None = None,
        description: str | None = None,
        scheme_name: str = "bearerAuth",
    ) -> "SecurityScheme":
        """An ``Authorization: Bearer <token>`` scheme (the authed-route default)."""
        return cls(
            type="http",
            scheme="bearer",
            bearer_format=bearer_format,
            description=description,
            scheme_name=scheme_name,
        )

    @classmethod
    def http_basic(
        cls, *, description: str | None = None, scheme_name: str = "basicAuth"
    ) -> "SecurityScheme":
        """An ``Authorization: Basic <credentials>`` scheme."""
        return cls(type="http", scheme="basic", description=description, scheme_name=scheme_name)

    @classmethod
    def api_key(
        cls,
        *,
        name: str,
        location: ApiKeyLocation,
        description: str | None = None,
        scheme_name: str = "apiKeyAuth",
    ) -> "SecurityScheme":
        """A token carried in a named header, query param, or cookie."""
        return cls(
            type="apiKey",
            name=name,
            location=location,
            description=description,
            scheme_name=scheme_name,
        )

    def to_openapi(self) -> dict[str, Any]:
        """Render the OpenAPI ``securitySchemes`` entry for this scheme."""
        scheme: dict[str, Any]
        if self.type == "http":
            scheme = {"type": "http", "scheme": self.scheme}
            if self.bearer_format is not None:
                scheme["bearerFormat"] = self.bearer_format
        elif self.type == "apiKey":
            scheme = {"type": "apiKey", "in": self.location, "name": self.name}
        else:
            scheme = {"type": self.type}
        if self.description is not None:
            scheme["description"] = self.description
        return scheme

api_key(*, name, location, description=None, scheme_name='apiKeyAuth') classmethod

A token carried in a named header, query param, or cookie.

Source code in jero/openapi.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@classmethod
def api_key(
    cls,
    *,
    name: str,
    location: ApiKeyLocation,
    description: str | None = None,
    scheme_name: str = "apiKeyAuth",
) -> "SecurityScheme":
    """A token carried in a named header, query param, or cookie."""
    return cls(
        type="apiKey",
        name=name,
        location=location,
        description=description,
        scheme_name=scheme_name,
    )

http_basic(*, description=None, scheme_name='basicAuth') classmethod

An Authorization: Basic <credentials> scheme.

Source code in jero/openapi.py
66
67
68
69
70
71
@classmethod
def http_basic(
    cls, *, description: str | None = None, scheme_name: str = "basicAuth"
) -> "SecurityScheme":
    """An ``Authorization: Basic <credentials>`` scheme."""
    return cls(type="http", scheme="basic", description=description, scheme_name=scheme_name)

http_bearer(*, bearer_format=None, description=None, scheme_name='bearerAuth') classmethod

An Authorization: Bearer <token> scheme (the authed-route default).

Source code in jero/openapi.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@classmethod
def http_bearer(
    cls,
    *,
    bearer_format: str | None = None,
    description: str | None = None,
    scheme_name: str = "bearerAuth",
) -> "SecurityScheme":
    """An ``Authorization: Bearer <token>`` scheme (the authed-route default)."""
    return cls(
        type="http",
        scheme="bearer",
        bearer_format=bearer_format,
        description=description,
        scheme_name=scheme_name,
    )

to_openapi()

Render the OpenAPI securitySchemes entry for this scheme.

Source code in jero/openapi.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def to_openapi(self) -> dict[str, Any]:
    """Render the OpenAPI ``securitySchemes`` entry for this scheme."""
    scheme: dict[str, Any]
    if self.type == "http":
        scheme = {"type": "http", "scheme": self.scheme}
        if self.bearer_format is not None:
            scheme["bearerFormat"] = self.bearer_format
    elif self.type == "apiKey":
        scheme = {"type": "apiKey", "in": self.location, "name": self.name}
    else:
        scheme = {"type": self.type}
    if self.description is not None:
        scheme["description"] = self.description
    return scheme

Middleware & CORS

Bases: Struct

One cross-origin resource sharing policy.

Register an app-wide default with _include_cors(CORS(...)); override it per include with the cors= keyword on _include_resource / _include_endpoint, or opt a route out with cors=CORS.OFF. An include that passes nothing inherits the app default; an app that never calls _include_cors serves no CORS headers at all (pure opt-in).

allow_origins="*" compiles to constant header pairs in every covered route's response — free at request time. An explicit origin tuple compiles to an origin echo whose verdict is memoized per origin (and a Vary: Origin pair) instead. Combining an origin allow-list with allow_credentials=True echoes the origin with Access-Control-Allow-Credentials: true; combining credentials with "*" is spec-forbidden and fails wiring loud.

Source code in jero/_middleware.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
class CORS(Struct, frozen=True):
    """One cross-origin resource sharing policy.

    Register an app-wide default with ``_include_cors(CORS(...))``; override it per
    include with the ``cors=`` keyword on ``_include_resource`` / ``_include_endpoint``,
    or opt a route out with ``cors=CORS.OFF``. An include that passes nothing inherits
    the app default; an app that never calls ``_include_cors`` serves no CORS headers
    at all (pure opt-in).

    ``allow_origins="*"`` compiles to constant header pairs in every covered route's
    response — free at request time. An explicit origin tuple compiles to an origin
    echo whose verdict is memoized per origin (and a ``Vary: Origin`` pair) instead.
    Combining an
    origin allow-list with ``allow_credentials=True`` echoes the origin with
    ``Access-Control-Allow-Credentials: true``; combining credentials with ``"*"`` is
    spec-forbidden and fails wiring loud.
    """

    allow_origins: tuple[str, ...] | Literal["*"] = "*"
    allow_methods: tuple[HTTPMethod, ...] = ("GET", "POST", "PUT", "PATCH", "DELETE")
    allow_headers: tuple[str, ...] = ("content-type", "authorization")
    allow_credentials: bool = False
    max_age: int = 600

    # The per-include opt-out sentinel: ``cors=CORS.OFF`` removes an app-wide default
    # from that include's routes. Compared by identity, so it never collides with a
    # user's own all-default ``CORS()``.
    OFF: ClassVar["CORS"]

Background tasks

A queue of fire-and-forget background work, dispatched by item type.

Register one handler per Struct type (inferred from the handler's parameter); endpoints call :meth:add to enqueue an item, and a single serial worker dispatches each to its handler. Build it with self._create_background_tasks(...) inside wire (sugar for await self._aenter(BackgroundTasks(...))) so the worker starts at startup and drains/stops at shutdown.

Create it after the resources its handlers use, so reverse-order shutdown drains the queue before those resources are torn down.

Source code in jero/background.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
class BackgroundTasks:
    """A queue of fire-and-forget background work, dispatched by item type.

    Register one handler per ``Struct`` type (inferred from the handler's parameter);
    endpoints call :meth:`add` to enqueue an item, and a single serial worker dispatches
    each to its handler. Build it with ``self._create_background_tasks(...)`` inside
    ``wire`` (sugar for ``await self._aenter(BackgroundTasks(...))``) so the worker
    starts at startup and drains/stops at shutdown.

    Create it *after* the resources its handlers use, so reverse-order shutdown drains
    the queue before those resources are torn down.
    """

    def __init__(
        self,
        *,
        maxsize: int = 1024,
        drain_timeout: float | None = 30.0,
        allow_one_to_many: bool = False,
    ) -> None:
        self._queue: asyncio.Queue[Struct] = asyncio.Queue(maxsize)
        self._drain_timeout = drain_timeout
        self._allow_one_to_many = allow_one_to_many
        self._handlers: dict[type[Struct], list[Callable[[Any], Awaitable[None]]]] = {}
        self._worker: asyncio.Task[None] | None = None

    async def _run(self) -> None:
        """Pull items forever, dispatching each to its handler(s). Errors are isolated."""
        while True:
            item = await self._queue.get()
            try:
                handlers = self._handlers.get(type(item))
                if not handlers:
                    logger.error("background: no handler registered for %s", type(item).__name__)
                    continue
                for handler in handlers:
                    try:
                        await handler(item)
                    except Exception:  # pylint: disable=broad-exception-caught
                        logger.exception("background: handler failed for %s", type(item).__name__)
            finally:
                self._queue.task_done()

    def register[T: Struct](self, handler: Callable[[T], Awaitable[None]]) -> None:
        """Register a handler; its item type is inferred from its single parameter.

        One handler per type by default — a second for the same type is a ``WiringError``
        unless this was built with ``allow_one_to_many=True``.
        """
        item_type = _infer_item_type(handler)
        if item_type in self._handlers and not self._allow_one_to_many:
            raise WiringError(
                f"BackgroundTasks: a handler is already registered for {item_type.__name__!r}; "
                f"pass allow_one_to_many=True to register more than one",
            )
        self._handlers.setdefault(item_type, []).append(handler)

    async def add(self, item: Struct) -> None:
        """Enqueue an item for background processing (awaits if the queue is full)."""
        await self._queue.put(item)

    async def __aenter__(self) -> Self:
        self._worker = asyncio.create_task(self._run())
        return self

    async def __aexit__(self, *_exc: object) -> None:
        if self._worker is None:
            return
        if self._drain_timeout is not None:
            try:
                await asyncio.wait_for(self._queue.join(), self._drain_timeout)
            except TimeoutError:
                logger.warning(
                    "background: drain timed out after %ss; dropping %d queued item(s)",
                    self._drain_timeout,
                    self._queue.qsize(),
                )
        self._worker.cancel()
        with contextlib.suppress(asyncio.CancelledError):
            await self._worker

_run() async

Pull items forever, dispatching each to its handler(s). Errors are isolated.

Source code in jero/background.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
async def _run(self) -> None:
    """Pull items forever, dispatching each to its handler(s). Errors are isolated."""
    while True:
        item = await self._queue.get()
        try:
            handlers = self._handlers.get(type(item))
            if not handlers:
                logger.error("background: no handler registered for %s", type(item).__name__)
                continue
            for handler in handlers:
                try:
                    await handler(item)
                except Exception:  # pylint: disable=broad-exception-caught
                    logger.exception("background: handler failed for %s", type(item).__name__)
        finally:
            self._queue.task_done()

add(item) async

Enqueue an item for background processing (awaits if the queue is full).

Source code in jero/background.py
105
106
107
async def add(self, item: Struct) -> None:
    """Enqueue an item for background processing (awaits if the queue is full)."""
    await self._queue.put(item)

register(handler)

Register a handler; its item type is inferred from its single parameter.

One handler per type by default — a second for the same type is a WiringError unless this was built with allow_one_to_many=True.

Source code in jero/background.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def register[T: Struct](self, handler: Callable[[T], Awaitable[None]]) -> None:
    """Register a handler; its item type is inferred from its single parameter.

    One handler per type by default — a second for the same type is a ``WiringError``
    unless this was built with ``allow_one_to_many=True``.
    """
    item_type = _infer_item_type(handler)
    if item_type in self._handlers and not self._allow_one_to_many:
        raise WiringError(
            f"BackgroundTasks: a handler is already registered for {item_type.__name__!r}; "
            f"pass allow_one_to_many=True to register more than one",
        )
    self._handlers.setdefault(item_type, []).append(handler)

Models & codecs

Bases: Struct

jero's Struct base. Use it instead of msgspec.Struct so a model may carry an OpenAPI description via the meta= class keyword:

class Widget(Struct, meta=ModelMeta(description="A widget.")): ...

The meta is read by the OpenAPI generator (as __model_meta__); a wire field named meta is unaffected (the class keyword and a field are different namespaces).

Source code in jero/structs.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Struct(_Struct, metaclass=_MetaCarrier):  # pylint: disable=invalid-metaclass  # _MetaCarrier subclasses msgspec's C-extension StructMeta, opaque to astroid
    """jero's ``Struct`` base. Use it instead of ``msgspec.Struct`` so a model may carry
    an OpenAPI description via the ``meta=`` class keyword:

    ``class Widget(Struct, meta=ModelMeta(description="A widget.")): ...``

    The ``meta`` is read by the OpenAPI generator (as ``__model_meta__``); a wire field
    named ``meta`` is unaffected (the class *keyword* and a *field* are different
    namespaces).
    """

    __model_meta__: ClassVar[ModelMeta | None] = None

    def __init_subclass__(cls, *, meta: ModelMeta | None = None, **kwargs: Any) -> None:  # noqa: ANN401
        # Declared so the static checkers accept the ``meta=`` class keyword; the metaclass
        # actually consumes it at runtime, so this never receives it.
        super().__init_subclass__()

Bases: Struct

OpenAPI metadata for a model (a wire Struct), attached via the meta= class keyword of :class:~jero.Struct.

description becomes the model's schema description — explicit, never inferred from the class docstring. name overrides the key the model gets under components.schemas (and every $ref that points at it); use it to give a model a stable public name or to disambiguate two same-named Structs that would otherwise collide.

Source code in jero/openapi.py
125
126
127
128
129
130
131
132
133
134
135
136
137
class ModelMeta(Struct):
    """OpenAPI metadata for a model (a wire ``Struct``), attached via the ``meta=`` class
    keyword of :class:`~jero.Struct`.

    ``description`` becomes the model's schema description — explicit, never inferred from
    the class docstring. ``name`` overrides the key the model gets under
    ``components.schemas`` (and every ``$ref`` that points at it); use it to give a model a
    stable public name or to disambiguate two same-named Structs that would otherwise
    collide.
    """

    description: str | None = None
    name: str | None = None

OpenAPI

Bases: Struct

A typed subset of the Scalar docs-UI configuration, passed to :meth:~jero.BaseApp._include_openapi as scalar_config and forwarded to the viewer as its data-configuration.

Only the commonly-useful, verified options are modeled — jero blesses a subset rather than mirroring Scalar's whole (evolving) surface; for anything not here, supply a full docs_html page instead. Unset fields are omitted from the wire, so Scalar's own defaults apply untouched.

Source code in jero/openapi.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
class ScalarConfig(Struct, rename="camel", omit_defaults=True):
    """A typed subset of the Scalar docs-UI configuration, passed to
    :meth:`~jero.BaseApp._include_openapi` as ``scalar_config`` and forwarded to the viewer as
    its ``data-configuration``.

    Only the commonly-useful, verified options are modeled — jero blesses a subset rather
    than mirroring Scalar's whole (evolving) surface; for anything not here, supply a full
    ``docs_html`` page instead. Unset fields are omitted from the wire, so Scalar's own
    defaults apply untouched.
    """

    theme: ScalarTheme | None = None
    layout: Literal["modern", "classic"] | None = None
    dark_mode: bool | None = None
    hide_models: bool = False
    hide_search: bool = False
    hide_download_button: bool = False
    hide_test_request_button: bool = False
    hide_dark_mode_toggle: bool = False
    show_sidebar: bool = True

Testing

Synchronous in-process client. Prefer with TestClient(app) as c.

cookie_jar=True opts into automatic cookie persistence across requests (off by default — every request then sends only what you explicitly pass). When on, each response's Set-Cookie values are stored in client.cookie_jar (a plain, directly inspectable and mutable dict[str, str]) and attached to subsequent requests and WebSocket handshakes; an expiring Set-Cookie (Max-Age=0 or a past Expires) removes its entry. Per-request cookies= merges over the jar (explicit wins on name collisions). The jar is name -> value only, with no path/domain scoping — the harness is single-origin and in-process, so RFC 6265 scoping rules would be dead code here.

Source code in jero/testing.py
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
class TestClient:
    """Synchronous in-process client. Prefer ``with TestClient(app) as c``.

    ``cookie_jar=True`` opts into automatic cookie persistence across requests (off by
    default — every request then sends only what you explicitly pass). When on, each
    response's ``Set-Cookie`` values are stored in ``client.cookie_jar`` (a plain,
    directly inspectable and mutable ``dict[str, str]``) and attached to subsequent
    requests and WebSocket handshakes; an expiring ``Set-Cookie`` (``Max-Age=0`` or a
    past ``Expires``) removes its entry. Per-request ``cookies=`` merges *over* the jar
    (explicit wins on name collisions). The jar is name -> value only, with no
    path/domain scoping — the harness is single-origin and in-process, so RFC 6265
    scoping rules would be dead code here."""

    __test__ = False  # stop pytest from collecting this as a test case

    def __init__(self, app: BaseApp[Any], *, cookie_jar: bool = False) -> None:
        self._app = app
        self._jar_enabled = cookie_jar
        self.cookie_jar: dict[str, str] = {}
        self._loop_thread = _LoopThread()
        self._to_app: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
        self._from_app: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
        self._lifespan_task: asyncio.Task[None]
        try:
            self._submit(self._start_lifespan())
        except BaseException:
            self._loop_thread.close()
            raise

    def _submit[T](self, coro: Coroutine[Any, Any, T]) -> T:
        return self._loop_thread.submit(coro)

    @staticmethod
    def _merge_cookies(
        headers: Mapping[str, str] | None, cookies: Mapping[str, str] | None
    ) -> dict[str, str]:
        """``headers`` with ``cookies`` folded in as one ``Cookie`` header. Passing both
        ``cookies=`` and an explicit ``Cookie`` entry in ``headers=`` is ambiguous."""
        merged = dict(headers or {})
        if not cookies:
            return merged
        if any(key.lower() == "cookie" for key in merged):
            raise ValueError("TestClient: pass cookies= or a 'Cookie' header, not both")
        merged["Cookie"] = "; ".join(f"{name}={value}" for name, value in cookies.items())
        return merged

    def _outgoing_cookies(
        self, headers: Mapping[str, str] | None, cookies: Mapping[str, str] | None
    ) -> Mapping[str, str] | None:
        """``cookies`` merged over the jar (explicit wins on a name collision), when the
        jar is enabled. Skipped when ``headers`` already carries an explicit ``Cookie``
        entry: that escape hatch must not silently gain jar cookies, and the ambiguity
        check in :meth:`_merge_cookies` must fire only when the caller actually passed
        both ``cookies=`` and a header — not merely because the jar happens to hold
        something."""
        if not self._jar_enabled or any(key.lower() == "cookie" for key in headers or {}):
            return cookies
        return {**self.cookie_jar, **(cookies or {})}

    def _apply_response_cookies(self, multi_headers: list[tuple[str, str]]) -> None:
        """Fold a response's ``Set-Cookie`` headers into the jar, when enabled: store a
        live cookie, drop one that expires itself (``Max-Age=0`` or a past ``Expires``)."""
        if not self._jar_enabled:
            return
        for key, value in multi_headers:
            if key.lower() != "set-cookie":
                continue
            name, cookie = _parse_set_cookie(value)
            if _cookie_is_expired(cookie):
                self.cookie_jar.pop(name, None)
            else:
                self.cookie_jar[name] = cookie.value

    @staticmethod
    def _part_content(value: str | bytes) -> bytes:
        return value if isinstance(value, bytes) else value.encode()

    @staticmethod
    def _disposition(name: str, filename: str | None = None) -> bytes:
        escaped_name = name.replace("\\", "\\\\").replace('"', '\\"')
        value = f'Content-Disposition: form-data; name="{escaped_name}"'
        if filename is not None:
            escaped_filename = filename.replace("\\", "\\\\").replace('"', '\\"')
            value += f'; filename="{escaped_filename}"'
        return value.encode()

    @staticmethod
    def _iter_data_values(value: _DataValues) -> Sequence[_DataValue]:
        return value if isinstance(value, list) else [value]

    @staticmethod
    def _iter_file_values(value: _FileValues) -> Sequence[_FileValue]:
        return value if isinstance(value, list) else [value]

    def _encode_multipart(
        self,
        data: dict[str, _DataValues] | None,
        files: dict[str, _FileValues] | None,
    ) -> tuple[bytes, str]:
        boundary = "jero-test-boundary"
        chunks: list[bytes] = []
        for name, value in (data or {}).items():
            for item in self._iter_data_values(value):
                chunks += [
                    f"--{boundary}\r\n".encode(),
                    self._disposition(name),
                    b"\r\n\r\n",
                    self._part_content(item),
                    b"\r\n",
                ]
        for name, value in (files or {}).items():
            for item in self._iter_file_values(value):
                filename, content = item[:2]
                content_type = item[2] if len(item) == 3 else None
                chunks += [
                    f"--{boundary}\r\n".encode(),
                    self._disposition(name, filename),
                    b"\r\n",
                ]
                if content_type is not None:
                    chunks += [f"Content-Type: {content_type}\r\n".encode()]
                chunks += [b"\r\n", content, b"\r\n"]
        chunks.append(f"--{boundary}--\r\n".encode())
        return b"".join(chunks), f"multipart/form-data; boundary={boundary}"

    async def _start_lifespan(self) -> None:
        self._lifespan_task = asyncio.create_task(
            self._app({"type": "lifespan"}, self._to_app.get, self._from_app.put)
        )
        await self._to_app.put({"type": "lifespan.startup"})
        message = await self._from_app.get()
        if message["type"] == "lifespan.startup.failed":
            # The app re-raises after reporting; retrieve it so asyncio
            # doesn't warn about an unretrieved task exception.
            with contextlib.suppress(Exception):
                await self._lifespan_task
            raise RuntimeError(f"lifespan startup failed: {message.get('message')}")

    async def _stop_lifespan(self) -> None:
        await self._to_app.put({"type": "lifespan.shutdown"})
        await self._from_app.get()
        await self._lifespan_task

    async def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, str] | None,
        json: Any,
        content: bytes | None,
        data: dict[str, _DataValues] | None,
        files: dict[str, _FileValues] | None,
        headers: dict[str, str] | None,
        cookies: Mapping[str, str] | None,
    ) -> TestResponse:
        body = b""
        outgoing = self._merge_cookies(headers, self._outgoing_cookies(headers, cookies))
        wire_headers = {k.lower(): v for k, v in outgoing.items()}
        if json is not None:
            body = msgspec_encoder.encode(json)
            wire_headers.setdefault("content-type", "application/json")
        elif content is not None:
            body = content
            wire_headers.setdefault("content-type", "application/octet-stream")
        elif data is not None or files is not None:
            body, content_type = self._encode_multipart(data, files)
            wire_headers.setdefault("content-type", content_type)

        scope: dict[str, Any] = {
            "type": "http",
            "method": method,
            "path": path,
            "query_string": urlencode(params or {}).encode("latin-1"),
            "headers": [
                (k.encode("latin-1"), v.encode("latin-1")) for k, v in wire_headers.items()
            ],
        }

        cycle = _RequestCycle(body)
        await self._app(scope, cycle.receive, cycle.send)
        self._apply_response_cookies(cycle.multi_headers)
        return TestResponse(
            status_code=cycle.status,
            headers=cycle.headers,
            content=b"".join(cycle.chunks),
            multi_headers=cycle.multi_headers,
        )

    async def _stream_request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, str] | None,
        json: Any,
        content: bytes | None,
        data: dict[str, _DataValues] | None,
        files: dict[str, _FileValues] | None,
        headers: dict[str, str] | None,
        cookies: Mapping[str, str] | None,
    ) -> _StreamSession:
        body = b""
        outgoing = self._merge_cookies(headers, self._outgoing_cookies(headers, cookies))
        wire_headers = {k.lower(): v for k, v in outgoing.items()}
        if json is not None:
            body = msgspec_encoder.encode(json)
            wire_headers.setdefault("content-type", "application/json")
        elif content is not None:
            body = content
            wire_headers.setdefault("content-type", "application/octet-stream")
        elif data is not None or files is not None:
            body, content_type = self._encode_multipart(data, files)
            wire_headers.setdefault("content-type", content_type)

        scope: dict[str, Any] = {
            "type": "http",
            "method": method,
            "path": path,
            "query_string": urlencode(params or {}).encode("latin-1"),
            "headers": [
                (k.encode("latin-1"), v.encode("latin-1")) for k, v in wire_headers.items()
            ],
        }
        cycle = _StreamCycle(body)
        task = asyncio.create_task(self._app(scope, cycle.receive, cycle.send))
        while True:
            message = await asyncio.to_thread(cycle.chunks.get)
            if message["type"] == "http.response.start":
                headers = {k.decode("latin-1"): v.decode("latin-1") for k, v in message["headers"]}
                return _StreamSession(self._submit, cycle, task, message["status"], headers)
            cycle.chunks.put(message)

    async def _open_websocket(
        self,
        path: str,
        *,
        params: dict[str, str] | None,
        headers: dict[str, str] | None,
        cookies: Mapping[str, str] | None,
        denial_response_extension: bool,
    ) -> tuple[_WebSocketCycle, asyncio.Task[None]]:
        outgoing = self._merge_cookies(headers, self._outgoing_cookies(headers, cookies))
        wire_headers = {key.lower(): value for key, value in outgoing.items()}
        scope: dict[str, Any] = {
            "type": "websocket",
            "path": path,
            "query_string": urlencode(params or {}).encode("latin-1"),
            "headers": [
                (key.encode("latin-1"), value.encode("latin-1"))
                for key, value in wire_headers.items()
            ],
            "subprotocols": [],
            "extensions": ({"websocket.http.response": {}} if denial_response_extension else {}),
        }
        cycle = _WebSocketCycle()
        task = asyncio.create_task(self._app(scope, cycle.receive, cycle.send))
        await cycle.to_app.put({"type": "websocket.connect"})
        first = await asyncio.to_thread(cycle.from_app.get)
        if first["type"] == "websocket.accept":
            return cycle, task
        if first["type"] == "websocket.close":
            await task
            raise WebSocketClosedError(first["code"], first.get("reason", ""))
        if first["type"] != "websocket.http.response.start":
            raise RuntimeError(f"unexpected WebSocket handshake event {first['type']!r}")
        body_message = await asyncio.to_thread(cycle.from_app.get)
        await task
        pairs = [
            (key.decode("latin-1"), value.decode("latin-1")) for key, value in first["headers"]
        ]
        response = TestResponse(
            status_code=first["status"],
            headers=dict(pairs),
            content=body_message.get("body", b""),
            multi_headers=pairs,
        )
        raise WebSocketUpgradeError(response)

    def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, str] | None = None,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue a request and return the buffered response."""
        return self._submit(
            self._request(
                method.upper(),
                path,
                params=params,
                json=json,
                content=content,
                data=data,
                files=files,
                headers=headers,
                cookies=cookies,
            )
        )

    def websocket[Inbound, Outbound](
        self,
        path: str,
        *,
        inbound: TypeForm[Inbound],
        outbound: TypeForm[Outbound],
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
        denial_response_extension: bool = True,
    ) -> TestWebSocket[Inbound, Outbound]:
        """Open a typed in-process WebSocket connection."""
        cycle, task = self._submit(
            self._open_websocket(
                path,
                params=params,
                headers=headers,
                cookies=cookies,
                denial_response_extension=denial_response_extension,
            )
        )
        return TestWebSocket(self._submit, cycle, task, inbound, outbound)

    def stream_request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, str] | None = None,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> _StreamSession:
        """Issue a request and return a streaming session for its chunks."""
        return self._submit(
            self._stream_request(
                method.upper(),
                path,
                params=params,
                json=json,
                content=content,
                data=data,
                files=files,
                headers=headers,
                cookies=cookies,
            )
        )

    def get(
        self,
        path: str,
        *,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue a GET request."""
        return self.request("GET", path, params=params, headers=headers, cookies=cookies)

    def stream_get(
        self,
        path: str,
        *,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> _StreamSession:
        """Open a streaming GET request."""
        return self.stream_request("GET", path, params=params, headers=headers, cookies=cookies)

    def head(
        self,
        path: str,
        *,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue a HEAD request."""
        return self.request("HEAD", path, params=params, headers=headers, cookies=cookies)

    def options(
        self,
        path: str,
        *,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue an OPTIONS request."""
        return self.request("OPTIONS", path, params=params, headers=headers, cookies=cookies)

    def delete(
        self,
        path: str,
        *,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue a DELETE request."""
        return self.request("DELETE", path, params=params, headers=headers, cookies=cookies)

    def stream_delete(
        self,
        path: str,
        *,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> _StreamSession:
        """Open a streaming DELETE request."""
        return self.stream_request("DELETE", path, params=params, headers=headers, cookies=cookies)

    def post(
        self,
        path: str,
        *,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue a POST request (JSON, raw bytes, or multipart form)."""
        return self.request(
            "POST",
            path,
            json=json,
            content=content,
            data=data,
            files=files,
            params=params,
            headers=headers,
            cookies=cookies,
        )

    def stream_post(
        self,
        path: str,
        *,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> _StreamSession:
        """Open a streaming POST request."""
        return self.stream_request(
            "POST",
            path,
            json=json,
            content=content,
            data=data,
            files=files,
            params=params,
            headers=headers,
            cookies=cookies,
        )

    def put(
        self,
        path: str,
        *,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue a PUT request (JSON, raw bytes, or multipart form)."""
        return self.request(
            "PUT",
            path,
            json=json,
            content=content,
            data=data,
            files=files,
            params=params,
            headers=headers,
            cookies=cookies,
        )

    def stream_put(
        self,
        path: str,
        *,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> _StreamSession:
        """Open a streaming PUT request."""
        return self.stream_request(
            "PUT",
            path,
            json=json,
            content=content,
            data=data,
            files=files,
            params=params,
            headers=headers,
            cookies=cookies,
        )

    def patch(
        self,
        path: str,
        *,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> TestResponse:
        """Issue a PATCH request (JSON, raw bytes, or multipart form)."""
        return self.request(
            "PATCH",
            path,
            json=json,
            content=content,
            data=data,
            files=files,
            params=params,
            headers=headers,
            cookies=cookies,
        )

    def stream_patch(
        self,
        path: str,
        *,
        json: Any = None,
        content: bytes | None = None,
        data: dict[str, _DataValues] | None = None,
        files: dict[str, _FileValues] | None = None,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
        cookies: Mapping[str, str] | None = None,
    ) -> _StreamSession:
        """Open a streaming PATCH request."""
        return self.stream_request(
            "PATCH",
            path,
            json=json,
            content=content,
            data=data,
            files=files,
            params=params,
            headers=headers,
            cookies=cookies,
        )

    def close(self) -> None:
        """Run the app's lifespan shutdown and stop the background loop."""
        self._submit(self._stop_lifespan())
        self._loop_thread.close()

    def __enter__(self) -> Self:
        return self

    def __exit__(self, *exc: object) -> None:
        self.close()

_apply_response_cookies(multi_headers)

Fold a response's Set-Cookie headers into the jar, when enabled: store a live cookie, drop one that expires itself (Max-Age=0 or a past Expires).

Source code in jero/testing.py
514
515
516
517
518
519
520
521
522
523
524
525
526
def _apply_response_cookies(self, multi_headers: list[tuple[str, str]]) -> None:
    """Fold a response's ``Set-Cookie`` headers into the jar, when enabled: store a
    live cookie, drop one that expires itself (``Max-Age=0`` or a past ``Expires``)."""
    if not self._jar_enabled:
        return
    for key, value in multi_headers:
        if key.lower() != "set-cookie":
            continue
        name, cookie = _parse_set_cookie(value)
        if _cookie_is_expired(cookie):
            self.cookie_jar.pop(name, None)
        else:
            self.cookie_jar[name] = cookie.value

_merge_cookies(headers, cookies) staticmethod

headers with cookies folded in as one Cookie header. Passing both cookies= and an explicit Cookie entry in headers= is ambiguous.

Source code in jero/testing.py
487
488
489
490
491
492
493
494
495
496
497
498
499
@staticmethod
def _merge_cookies(
    headers: Mapping[str, str] | None, cookies: Mapping[str, str] | None
) -> dict[str, str]:
    """``headers`` with ``cookies`` folded in as one ``Cookie`` header. Passing both
    ``cookies=`` and an explicit ``Cookie`` entry in ``headers=`` is ambiguous."""
    merged = dict(headers or {})
    if not cookies:
        return merged
    if any(key.lower() == "cookie" for key in merged):
        raise ValueError("TestClient: pass cookies= or a 'Cookie' header, not both")
    merged["Cookie"] = "; ".join(f"{name}={value}" for name, value in cookies.items())
    return merged

_outgoing_cookies(headers, cookies)

cookies merged over the jar (explicit wins on a name collision), when the jar is enabled. Skipped when headers already carries an explicit Cookie entry: that escape hatch must not silently gain jar cookies, and the ambiguity check in :meth:_merge_cookies must fire only when the caller actually passed both cookies= and a header — not merely because the jar happens to hold something.

Source code in jero/testing.py
501
502
503
504
505
506
507
508
509
510
511
512
def _outgoing_cookies(
    self, headers: Mapping[str, str] | None, cookies: Mapping[str, str] | None
) -> Mapping[str, str] | None:
    """``cookies`` merged over the jar (explicit wins on a name collision), when the
    jar is enabled. Skipped when ``headers`` already carries an explicit ``Cookie``
    entry: that escape hatch must not silently gain jar cookies, and the ambiguity
    check in :meth:`_merge_cookies` must fire only when the caller actually passed
    both ``cookies=`` and a header — not merely because the jar happens to hold
    something."""
    if not self._jar_enabled or any(key.lower() == "cookie" for key in headers or {}):
        return cookies
    return {**self.cookie_jar, **(cookies or {})}

close()

Run the app's lifespan shutdown and stop the background loop.

Source code in jero/testing.py
1029
1030
1031
1032
def close(self) -> None:
    """Run the app's lifespan shutdown and stop the background loop."""
    self._submit(self._stop_lifespan())
    self._loop_thread.close()

delete(path, *, params=None, headers=None, cookies=None)

Issue a DELETE request.

Source code in jero/testing.py
857
858
859
860
861
862
863
864
865
866
def delete(
    self,
    path: str,
    *,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue a DELETE request."""
    return self.request("DELETE", path, params=params, headers=headers, cookies=cookies)

get(path, *, params=None, headers=None, cookies=None)

Issue a GET request.

Source code in jero/testing.py
813
814
815
816
817
818
819
820
821
822
def get(
    self,
    path: str,
    *,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue a GET request."""
    return self.request("GET", path, params=params, headers=headers, cookies=cookies)

head(path, *, params=None, headers=None, cookies=None)

Issue a HEAD request.

Source code in jero/testing.py
835
836
837
838
839
840
841
842
843
844
def head(
    self,
    path: str,
    *,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue a HEAD request."""
    return self.request("HEAD", path, params=params, headers=headers, cookies=cookies)

options(path, *, params=None, headers=None, cookies=None)

Issue an OPTIONS request.

Source code in jero/testing.py
846
847
848
849
850
851
852
853
854
855
def options(
    self,
    path: str,
    *,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue an OPTIONS request."""
    return self.request("OPTIONS", path, params=params, headers=headers, cookies=cookies)

patch(path, *, json=None, content=None, data=None, files=None, params=None, headers=None, cookies=None)

Issue a PATCH request (JSON, raw bytes, or multipart form).

Source code in jero/testing.py
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
def patch(
    self,
    path: str,
    *,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue a PATCH request (JSON, raw bytes, or multipart form)."""
    return self.request(
        "PATCH",
        path,
        json=json,
        content=content,
        data=data,
        files=files,
        params=params,
        headers=headers,
        cookies=cookies,
    )

post(path, *, json=None, content=None, data=None, files=None, params=None, headers=None, cookies=None)

Issue a POST request (JSON, raw bytes, or multipart form).

Source code in jero/testing.py
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
def post(
    self,
    path: str,
    *,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue a POST request (JSON, raw bytes, or multipart form)."""
    return self.request(
        "POST",
        path,
        json=json,
        content=content,
        data=data,
        files=files,
        params=params,
        headers=headers,
        cookies=cookies,
    )

put(path, *, json=None, content=None, data=None, files=None, params=None, headers=None, cookies=None)

Issue a PUT request (JSON, raw bytes, or multipart form).

Source code in jero/testing.py
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
def put(
    self,
    path: str,
    *,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue a PUT request (JSON, raw bytes, or multipart form)."""
    return self.request(
        "PUT",
        path,
        json=json,
        content=content,
        data=data,
        files=files,
        params=params,
        headers=headers,
        cookies=cookies,
    )

request(method, path, *, params=None, json=None, content=None, data=None, files=None, headers=None, cookies=None)

Issue a request and return the buffered response.

Source code in jero/testing.py
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
def request(
    self,
    method: str,
    path: str,
    *,
    params: dict[str, str] | None = None,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> TestResponse:
    """Issue a request and return the buffered response."""
    return self._submit(
        self._request(
            method.upper(),
            path,
            params=params,
            json=json,
            content=content,
            data=data,
            files=files,
            headers=headers,
            cookies=cookies,
        )
    )

stream_delete(path, *, params=None, headers=None, cookies=None)

Open a streaming DELETE request.

Source code in jero/testing.py
868
869
870
871
872
873
874
875
876
877
def stream_delete(
    self,
    path: str,
    *,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> _StreamSession:
    """Open a streaming DELETE request."""
    return self.stream_request("DELETE", path, params=params, headers=headers, cookies=cookies)

stream_get(path, *, params=None, headers=None, cookies=None)

Open a streaming GET request.

Source code in jero/testing.py
824
825
826
827
828
829
830
831
832
833
def stream_get(
    self,
    path: str,
    *,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> _StreamSession:
    """Open a streaming GET request."""
    return self.stream_request("GET", path, params=params, headers=headers, cookies=cookies)

stream_patch(path, *, json=None, content=None, data=None, files=None, params=None, headers=None, cookies=None)

Open a streaming PATCH request.

Source code in jero/testing.py
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
def stream_patch(
    self,
    path: str,
    *,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> _StreamSession:
    """Open a streaming PATCH request."""
    return self.stream_request(
        "PATCH",
        path,
        json=json,
        content=content,
        data=data,
        files=files,
        params=params,
        headers=headers,
        cookies=cookies,
    )

stream_post(path, *, json=None, content=None, data=None, files=None, params=None, headers=None, cookies=None)

Open a streaming POST request.

Source code in jero/testing.py
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
def stream_post(
    self,
    path: str,
    *,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> _StreamSession:
    """Open a streaming POST request."""
    return self.stream_request(
        "POST",
        path,
        json=json,
        content=content,
        data=data,
        files=files,
        params=params,
        headers=headers,
        cookies=cookies,
    )

stream_put(path, *, json=None, content=None, data=None, files=None, params=None, headers=None, cookies=None)

Open a streaming PUT request.

Source code in jero/testing.py
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
def stream_put(
    self,
    path: str,
    *,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> _StreamSession:
    """Open a streaming PUT request."""
    return self.stream_request(
        "PUT",
        path,
        json=json,
        content=content,
        data=data,
        files=files,
        params=params,
        headers=headers,
        cookies=cookies,
    )

stream_request(method, path, *, params=None, json=None, content=None, data=None, files=None, headers=None, cookies=None)

Issue a request and return a streaming session for its chunks.

Source code in jero/testing.py
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
def stream_request(
    self,
    method: str,
    path: str,
    *,
    params: dict[str, str] | None = None,
    json: Any = None,
    content: bytes | None = None,
    data: dict[str, _DataValues] | None = None,
    files: dict[str, _FileValues] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
) -> _StreamSession:
    """Issue a request and return a streaming session for its chunks."""
    return self._submit(
        self._stream_request(
            method.upper(),
            path,
            params=params,
            json=json,
            content=content,
            data=data,
            files=files,
            headers=headers,
            cookies=cookies,
        )
    )

websocket(path, *, inbound, outbound, params=None, headers=None, cookies=None, denial_response_extension=True)

Open a typed in-process WebSocket connection.

Source code in jero/testing.py
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
def websocket[Inbound, Outbound](
    self,
    path: str,
    *,
    inbound: TypeForm[Inbound],
    outbound: TypeForm[Outbound],
    params: dict[str, str] | None = None,
    headers: dict[str, str] | None = None,
    cookies: Mapping[str, str] | None = None,
    denial_response_extension: bool = True,
) -> TestWebSocket[Inbound, Outbound]:
    """Open a typed in-process WebSocket connection."""
    cycle, task = self._submit(
        self._open_websocket(
            path,
            params=params,
            headers=headers,
            cookies=cookies,
            denial_response_extension=denial_response_extension,
        )
    )
    return TestWebSocket(self._submit, cycle, task, inbound, outbound)

A captured HTTP response: status code, headers, and body bytes.

Source code in jero/testing.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
@dataclass(slots=True)
class TestResponse:
    """A captured HTTP response: status code, headers, and body bytes."""

    __test__ = False  # stop pytest from collecting this as a test case

    status_code: int
    headers: dict[str, str]
    content: bytes
    # Every header pair as sent, repeats included; ``headers`` collapses duplicates.
    multi_headers: list[tuple[str, str]]

    @property
    def text(self) -> str:
        """The response body decoded as UTF-8 text."""
        return self.content.decode()

    def json(self) -> Any:
        """The response body decoded as JSON."""
        return msgspec_decoder.decode(self.content)

    @property
    def cookies(self) -> dict[str, TestCookie]:
        """The response's ``Set-Cookie`` headers, parsed and keyed by cookie name."""
        return dict(
            _parse_set_cookie(value)
            for key, value in self.multi_headers
            if key.lower() == "set-cookie"
        )

cookies property

The response's Set-Cookie headers, parsed and keyed by cookie name.

text property

The response body decoded as UTF-8 text.

json()

The response body decoded as JSON.

Source code in jero/testing.py
137
138
139
def json(self) -> Any:
    """The response body decoded as JSON."""
    return msgspec_decoder.decode(self.content)

One parsed Set-Cookie response header. Attribute names parse case-insensitively; expires is the raw wire value, unparsed.

Source code in jero/testing.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@dataclass(frozen=True, slots=True)
class TestCookie:
    """One parsed ``Set-Cookie`` response header. Attribute names parse
    case-insensitively; ``expires`` is the raw wire value, unparsed."""

    __test__ = False

    value: str
    max_age: int | None = None
    expires: str | None = None
    path: str | None = None
    domain: str | None = None
    secure: bool = False
    http_only: bool = False
    same_site: str | None = None
    partitioned: bool = False

One decoded Server-Sent Event captured from a streaming response.

Source code in jero/testing.py
151
152
153
154
155
156
157
158
159
160
@dataclass(frozen=True, slots=True)
class TestSSEEvent:
    """One decoded Server-Sent Event captured from a streaming response."""

    __test__ = False

    data: Any
    event: str | None = None
    id: str | None = None
    retry: int | None = None

Build a factory in isolation and exercise its create_* methods from sync tests.

The factory-level sibling of :class:TestClient, and a thin sync bridge over :meth:BaseFactory.open — lifecycle has that one code path. The harness enters Factory.open() on a background loop, so services are built — and their resources opened and torn down — exactly as under a live app, but drivable from synchronous test code (matching the sync TestClient). Use it to test the real factory wiring that an app's factory= seam mocks away; in async code (scripts, cron jobs, notebooks) use async with Factory.open() directly.

with FactoryHarness(Factory) as harness:
    service = harness.run(harness.factory.create_widget_service())
    assert isinstance(service, WidgetService)
# everything opened on the stacks is closed here

Synchronous create_* methods can be called directly on harness.factory; run awaits the async ones on the harness's loop.

Source code in jero/testing.py
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
class FactoryHarness[FactoryT: BaseFactory]:
    """Build a factory in isolation and exercise its ``create_*`` methods from sync tests.

    The factory-level sibling of :class:`TestClient`, and a thin sync bridge over
    :meth:`BaseFactory.open` — lifecycle has that one code path. The harness enters
    ``Factory.open()`` on a background loop, so services are built — and their
    resources opened and torn down — exactly as under a live app, but drivable from
    synchronous test code (matching the sync ``TestClient``). Use it to test the
    real factory wiring that an app's ``factory=`` seam mocks away; in async code
    (scripts, cron jobs, notebooks) use ``async with Factory.open()`` directly.

        with FactoryHarness(Factory) as harness:
            service = harness.run(harness.factory.create_widget_service())
            assert isinstance(service, WidgetService)
        # everything opened on the stacks is closed here

    Synchronous ``create_*`` methods can be called directly on ``harness.factory``;
    ``run`` awaits the async ones on the harness's loop.
    """

    def __init__(self, factory_cls: type[FactoryT]) -> None:
        self._loop_thread = _LoopThread()
        self._scope = AsyncExitStack()
        self.factory: FactoryT = self._loop_thread.submit(
            self._scope.enter_async_context(factory_cls.open())
        )

    def run[T](self, coro: Coroutine[Any, Any, T]) -> T:
        """Await an async ``create_*`` coroutine on the harness's loop."""
        return self._loop_thread.submit(coro)

    def close(self) -> None:
        """Exit ``Factory.open()`` — closing everything the factory opened — then stop
        the loop."""
        self._loop_thread.submit(self._scope.aclose())
        self._loop_thread.close()

    def __enter__(self) -> Self:
        return self

    def __exit__(self, *exc: object) -> None:
        self.close()

close()

Exit Factory.open() — closing everything the factory opened — then stop the loop.

Source code in jero/testing.py
1072
1073
1074
1075
1076
def close(self) -> None:
    """Exit ``Factory.open()`` — closing everything the factory opened — then stop
    the loop."""
    self._loop_thread.submit(self._scope.aclose())
    self._loop_thread.close()

run(coro)

Await an async create_* coroutine on the harness's loop.

Source code in jero/testing.py
1068
1069
1070
def run[T](self, coro: Coroutine[Any, Any, T]) -> T:
    """Await an async ``create_*`` coroutine on the harness's loop."""
    return self._loop_thread.submit(coro)