coakka-http-runtime-python 1.1.2__cp311-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coakka_http/__init__.py +6 -0
- coakka_http/_native.py +192 -0
- coakka_http/_package.json +10 -0
- coakka_http/_projection.py +64 -0
- coakka_http/_service_cpu.py +63 -0
- coakka_http/native/_coakka_http_projection.pyd +0 -0
- coakka_http/native/coakka_http_host.dll +0 -0
- coakka_http/py.typed +0 -0
- coakka_http/runtime.py +555 -0
- coakka_http/service.py +789 -0
- coakka_http/values.py +250 -0
- coakka_http_runtime_python-1.1.2.dist-info/METADATA +63 -0
- coakka_http_runtime_python-1.1.2.dist-info/RECORD +19 -0
- coakka_http_runtime_python-1.1.2.dist-info/WHEEL +4 -0
- coakka_http_runtime_python-1.1.2.dist-info/assets/coakka-logo.png +0 -0
- coakka_http_runtime_python-1.1.2.dist-info/licenses/LICENSE +202 -0
- coakka_http_runtime_python-1.1.2.dist-info/licenses/NATIVE-LICENSE.md +138 -0
- coakka_http_runtime_python-1.1.2.dist-info/licenses/NOTICE +913 -0
- coakka_http_runtime_python-1.1.2.dist-info/licenses/PACKAGE-LICENSE.md +31 -0
coakka_http/runtime.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
'Low-level, lease-safe Python access to the host-inlined HTTP runtime.'
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import ctypes
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass,fields
|
|
8
|
+
from itertools import islice
|
|
9
|
+
from typing import Iterator,Self,TypeAlias,TypeVar,cast
|
|
10
|
+
from.import _native as native
|
|
11
|
+
from.import _projection as managed_projection
|
|
12
|
+
from.values import Activity,ApiAdditionalProperties,ApiExample,ApiMediaType,ApiParameter,ApiParameterLocation,ApiRequestBody,ApiResponse,ApiResponseHeader,ApiSchema,ApiSchemaDefinition,ApiSchemaKind,ApiSchemaProperty,ApiSecurityRequirement,ApiSecurityUse,BodyDelivery,Capability,Compression,CompressionMode,ControlCode,DrainOutcome,DrainState,Event,EventKind,ExchangeId,ExecutionInfo,ExecutionMode,ExecutionReason,ExecutionSelection,Failure,FileResponse,Header,Headers,Health,IoBackend,IoBackendFallbackReason,Limits,MonitorApplyOutcome,MonitorApplyReason,MonitorCategory,MonitorConfig,MonitorEvent,MonitorEventKind,MonitorEventPage,MonitorFailureCount,MonitorLatencyBucket,MonitorPolicy,MonitorSnapshot,NotificationProfile,OutboundTerminal,OutboundReason,OutboundPhase,OutboundRetry,OutboundCertainty,OutboundId,OutboundRequest,PathParameter,QueryParameter,Request,RequestRetirement,RequestRetirementCode,RequestRetirementState,Response,Route,RouteApiMetadata,RouteIdentity,RoutePublication,RoutePublicationOutcome,RouteRebind,RouteRebindOutcome,RouteSnapshot,RouteState,RuntimeConfig,RuntimeInfo,RuntimeInspection,ServicePlan,SseEvent,TlsCredentials,TlsReloadMode,TlsReloadOutcome,TlsReloadReason,TlsReloadRequest,TransportSecurity,WebSocketEvent,WebSocketEventKind,WebSocketId,_MAX_HEADER_COUNT,_EMPTY_HEADERS
|
|
13
|
+
NativeError=native.NativeError
|
|
14
|
+
_MAX_ITEMS=1<<20
|
|
15
|
+
_MAX_WAIT_MS=60000
|
|
16
|
+
_FAILED_INDEX_NONE=(1<<32)-1
|
|
17
|
+
_REQUEST_EVENT_KIND=int(EventKind.REQUEST)
|
|
18
|
+
_ServiceRequestEvent:TypeAlias=tuple[Request,int,int]
|
|
19
|
+
_MAX_METADATA_COLLECTION_ITEMS=16384
|
|
20
|
+
_MAX_METADATA_SCHEMA_DEPTH=128
|
|
21
|
+
_MAX_METADATA_PROJECTION_BYTES=16<<20
|
|
22
|
+
_MAX_METADATA_TEXT_BYTES=64<<10
|
|
23
|
+
_MetadataValue=TypeVar('_MetadataValue')
|
|
24
|
+
_NativeStructure=TypeVar('_NativeStructure',bound=ctypes.Structure)
|
|
25
|
+
_ProjectedValues:TypeAlias=tuple[object,...]
|
|
26
|
+
_ProjectedRequest:TypeAlias=tuple[int,int,int,int,int,int,str,str,str,str,tuple[str,...]|None,bytes,int,int,int,int,tuple[str,...]|None,tuple[object,...]|None,tuple[str,...]|None,bool]
|
|
27
|
+
_ProjectedEvent:TypeAlias=tuple[int,int,int,int,int,int,bytes|None,tuple[str,...]|None]
|
|
28
|
+
_ProjectedSuccess:TypeAlias=_ProjectedRequest|_ProjectedEvent
|
|
29
|
+
def _unsigned(value:int,bits:int,name:str)->int:
|
|
30
|
+
if isinstance(value,bool)or not isinstance(value,int):raise TypeError(f"{name} must be an integer")
|
|
31
|
+
if value<0 or value>=1<<bits:raise ValueError(f"{name} is outside the unsigned {bits}-bit range")
|
|
32
|
+
return value
|
|
33
|
+
def _timeout(value:int)->int:
|
|
34
|
+
value=_unsigned(value,64,'timeout_ms')
|
|
35
|
+
if value!=0 and value!=(1<<64)-1 and value>_MAX_WAIT_MS:raise ValueError('timeout_ms exceeds the 60-second connector bound')
|
|
36
|
+
return value
|
|
37
|
+
def _init(value:ctypes.Structure,function_name:str)->None:getattr(native.LIBRARY.library,function_name)(ctypes.byref(value))
|
|
38
|
+
def _native_exchange(value:ExchangeId)->native.Exchange:
|
|
39
|
+
if not isinstance(value,ExchangeId):raise TypeError('exchange must be an ExchangeId')
|
|
40
|
+
return native.Exchange(_unsigned(value.slot,64,'exchange.slot'),_unsigned(value.generation,64,'exchange.generation'))
|
|
41
|
+
def _native_websocket(value:WebSocketId)->native.WebSocket:
|
|
42
|
+
if not isinstance(value,WebSocketId):raise TypeError('session must be a WebSocketId')
|
|
43
|
+
return native.WebSocket(_unsigned(value.slot,64,'session.slot'),_unsigned(value.generation,64,'session.generation'))
|
|
44
|
+
def _native_outbound(value:OutboundId)->native.OutboundCall:
|
|
45
|
+
if not isinstance(value,OutboundId):raise TypeError('call must be an OutboundId')
|
|
46
|
+
return native.OutboundCall(_unsigned(value.slot,64,'call.slot'),_unsigned(value.generation,64,'call.generation'))
|
|
47
|
+
class _PreparedHeaders:
|
|
48
|
+
'Header-owned immutable descriptors and encoded bytes, with no Core state.';__slots__='arena','pointer','count','cacheable'
|
|
49
|
+
def __init__(self,value:Headers)->None:
|
|
50
|
+
'Bound iteration before encoding; publish only the complete view.';entries=tuple(islice(value,_MAX_HEADER_COUNT+1))
|
|
51
|
+
if len(entries)>_MAX_HEADER_COUNT:raise ValueError('header count exceeds the Python projection bound')
|
|
52
|
+
if any(not isinstance(item,Header)for item in entries):raise TypeError('headers must contain Header values')
|
|
53
|
+
self.arena=native.Arena();projected=[native.Header(self.arena.bytes(item.name,encoding='ascii'),self.arena.bytes(item.value,encoding='latin-1'))for item in entries];array=self.arena.array(native.Header,projected);self.pointer=ctypes.cast(array,ctypes.POINTER(native.Header))if array else None;self.count=len(entries);self.cacheable=type(value)is Headers and all(type(h)is Header and type(h.name)is str and type(h.value)is str for h in entries)
|
|
54
|
+
def _project_headers(value:Headers,arena:native.Arena)->tuple[ctypes._Pointer[native.Header]|None,int,bool]:
|
|
55
|
+
if not isinstance(value,Headers):raise TypeError('headers must be Headers')
|
|
56
|
+
if type(value)is Headers and not len(value):return None,0,True
|
|
57
|
+
cached=getattr(value,'_native_headers',None)
|
|
58
|
+
if type(value)is not Headers or not isinstance(cached,_PreparedHeaders):
|
|
59
|
+
cached=_PreparedHeaders(value)
|
|
60
|
+
if cached.cacheable:object.__setattr__(value,'_native_headers',cached)
|
|
61
|
+
arena.values.append(cached);return cached.pointer,cached.count,cached.cacheable
|
|
62
|
+
def _copy_header(value:native.Header)->Header:return Header._from_native(native.copy_text(value.name,'ascii'),native.copy_text(value.value,'latin-1'))
|
|
63
|
+
class _PreparedResponse:
|
|
64
|
+
'One response-owned, read-only ABI view and its Python buffer owners.\n\n Core copies accepted bytes synchronously. Publication occurs only after\n complete validation/projection. Racing first uses may build independent\n views; each call retains its own owner until return. No shared mutation,\n global cache, native service pointer or exchange identity is retained.\n ';__slots__='value','arena','storage','cacheable'
|
|
65
|
+
def __init__(self,response:Response)->None:
|
|
66
|
+
'Validate and retain every input buffer before publishing this view.';self.arena=native.Arena();self.value=native.Response();status=response.status
|
|
67
|
+
if type(status)is not int or status<0 or status>=1<<32:status=_unsigned(status,32,'response.status')
|
|
68
|
+
headers,count,headers_cacheable=_project_headers(response.headers,self.arena);projection=managed_projection.PROJECTION;body=response.body
|
|
69
|
+
if projection is not None and type(body)is bytes:
|
|
70
|
+
if len(body)>native.MAX_COPY_BYTES:raise ValueError('native byte projection exceeds the Python bound')
|
|
71
|
+
if body:self.arena.values.append(body)
|
|
72
|
+
projection.prepare_response(self.value,status,headers,count,body)
|
|
73
|
+
else:_init(self.value,'coakka_http_host_response_init');self.value.status_code=status;self.value.headers,self.value.header_count=headers,count;self.value.body=self.arena.bytes(body)
|
|
74
|
+
(self.storage):native.Response|memoryview=self.value;self.cacheable=type(response)is Response and type(response.status)is int and type(response.body)is bytes and type(response.headers)is Headers and headers_cacheable
|
|
75
|
+
def _prepared_response(response:Response)->_PreparedResponse:
|
|
76
|
+
'Memoize only exact immutable values; mutable/subclass inputs stay per-call.';cached=getattr(response,'_native_projection',None)
|
|
77
|
+
if type(response)is Response and isinstance(cached,_PreparedResponse):
|
|
78
|
+
if cached.storage is cached.value:cached.storage=memoryview(cached.value).toreadonly()
|
|
79
|
+
return cached
|
|
80
|
+
projected=_PreparedResponse(response)
|
|
81
|
+
if projected.cacheable:object.__setattr__(response,'_native_projection',projected)
|
|
82
|
+
return projected
|
|
83
|
+
class _MetadataBudget:
|
|
84
|
+
'One call-wide byte ceiling shared by every route metadata graph.'
|
|
85
|
+
def __init__(self)->None:'Start an empty bounded projection budget.';self.bytes=0
|
|
86
|
+
def encode(self,value:object,name:str,*,binary:bool=False)->bytes:
|
|
87
|
+
'Encode one typed value once and reserve its complete byte extent.';field_limit=_MAX_METADATA_PROJECTION_BYTES if binary else _MAX_METADATA_TEXT_BYTES
|
|
88
|
+
if isinstance(value,str):
|
|
89
|
+
if str.__len__(value)>field_limit:raise ValueError(f"{name} exceeds the connector byte bound")
|
|
90
|
+
data=str.encode(value,'utf-8')
|
|
91
|
+
elif binary and type(value)is bytes:data=value
|
|
92
|
+
else:expected='bytes or str'if binary else'str';raise TypeError(f"{name} must be {expected}")
|
|
93
|
+
if len(data)>field_limit:raise ValueError(f"{name} exceeds the connector byte bound")
|
|
94
|
+
if len(data)>_MAX_METADATA_PROJECTION_BYTES-self.bytes:raise ValueError('route metadata exceeds the connector byte bound')
|
|
95
|
+
self.bytes+=len(data);return data
|
|
96
|
+
class _MetadataProjection:
|
|
97
|
+
'Call-scoped native metadata graph; Core owns semantic validation.'
|
|
98
|
+
def __init__(self,arena:native.Arena,budget:_MetadataBudget)->None:'Attach every projected owner to one synchronous call arena.';self.arena=arena;self.budget=budget;self.items=0;(self.schemas):dict[int,native.ApiSchema]={}
|
|
99
|
+
def _values(self,value:tuple[_MetadataValue,...],name:str)->tuple[_MetadataValue,...]:
|
|
100
|
+
if not isinstance(value,tuple):raise TypeError(f"{name} must be a tuple")
|
|
101
|
+
self.items+=len(value)
|
|
102
|
+
if self.items>_MAX_METADATA_COLLECTION_ITEMS:raise ValueError('route metadata exceeds the connector marshalling bound')
|
|
103
|
+
return value
|
|
104
|
+
def _pointer(self,value:_NativeStructure)->ctypes._Pointer[_NativeStructure]:self.arena.values.append(value);return ctypes.pointer(value)
|
|
105
|
+
def _array(self,item_type:type[_NativeStructure],values:Sequence[_NativeStructure])->ctypes._Pointer[_NativeStructure]|None:array=self.arena.array(item_type,values);return ctypes.cast(array,ctypes.POINTER(item_type))if array else None
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _bool(value:object,name:str)->int:
|
|
108
|
+
if not isinstance(value,bool):raise TypeError(f"{name} must be a bool")
|
|
109
|
+
return int(value)
|
|
110
|
+
def _text(self,value:object,name:str)->native.Bytes:'Retain one UTF-8 text value under the call-wide byte budget.';return self.arena.bytes(self.budget.encode(value,name))
|
|
111
|
+
def _example_value(self,value:object)->native.Bytes:'Retain one opaque example value under the call-wide byte budget.';return self.arena.bytes(self.budget.encode(value,'example.value',binary=True))
|
|
112
|
+
def examples(self,values:tuple[ApiExample,...])->tuple[ctypes._Pointer[native.ApiExample]|None,int]:
|
|
113
|
+
'Project one bounded example collection into arena-owned records.';projected:list[native.ApiExample]=[]
|
|
114
|
+
for source in self._values(values,'examples'):
|
|
115
|
+
if not isinstance(source,ApiExample):raise TypeError('examples must contain ApiExample values')
|
|
116
|
+
target=native.ApiExample();_init(target,'coakka_http_host_api_example_init');target.name=self._text(source.name,'example.name');target.summary=self._text(source.summary,'example.summary');target.value=self._example_value(source.value);projected.append(target)
|
|
117
|
+
return self._array(native.ApiExample,projected),len(projected)
|
|
118
|
+
def schema(self,source:ApiSchema,depth:int=1)->ctypes._Pointer[native.ApiSchema]:
|
|
119
|
+
'Project a recursive schema while retaining shared-node identity.'
|
|
120
|
+
if not isinstance(source,ApiSchema):raise TypeError('schema must be an ApiSchema')
|
|
121
|
+
if depth>_MAX_METADATA_SCHEMA_DEPTH:raise ValueError('route metadata schema exceeds the marshalling depth')
|
|
122
|
+
cached=self.schemas.get(id(source))
|
|
123
|
+
if cached is not None:return ctypes.pointer(cached)
|
|
124
|
+
target=native.ApiSchema();_init(target,'coakka_http_host_api_schema_init');self.schemas[id(source)]=target;self.arena.values.append(target)
|
|
125
|
+
if not isinstance(source.kind,ApiSchemaKind):raise TypeError('schema.kind must be an ApiSchemaKind')
|
|
126
|
+
if not isinstance(source.additional_properties,ApiAdditionalProperties):raise TypeError('schema.additional_properties must be an ApiAdditionalProperties')
|
|
127
|
+
target.kind=int(source.kind);target.format=self._text(source.format,'schema.format');target.reference=self._text(source.reference,'schema.reference');enum_values=[self._text(item,'schema.enum_values')for item in self._values(source.enum_values,'schema.enum_values')];enum_array=self.arena.array(native.Bytes,enum_values);target.enum_values=ctypes.cast(enum_array,ctypes.POINTER(native.Bytes))if enum_array else None;target.enum_value_count=len(enum_values);target.additional_properties=int(source.additional_properties)
|
|
128
|
+
if source.items is not None:target.items=self.schema(source.items,depth+1)
|
|
129
|
+
properties:list[native.ApiSchemaProperty]=[]
|
|
130
|
+
for property_value in self._values(source.properties,'schema.properties'):
|
|
131
|
+
if not isinstance(property_value,ApiSchemaProperty):raise TypeError('schema.properties must contain ApiSchemaProperty values')
|
|
132
|
+
item=native.ApiSchemaProperty();_init(item,'coakka_http_host_api_schema_property_init');item.name=self._text(property_value.name,'schema.property.name');item.description=self._text(property_value.description,'schema.property.description');item.required=self._bool(property_value.required,'schema property required');item.schema=self.schema(property_value.schema,depth+1);properties.append(item)
|
|
133
|
+
target.properties=self._array(native.ApiSchemaProperty,properties);target.property_count=len(properties)
|
|
134
|
+
if source.nullable is not None:target.has_nullable=1;target.nullable=self._bool(source.nullable,'schema.nullable')
|
|
135
|
+
return ctypes.pointer(target)
|
|
136
|
+
def media_types(self,values:tuple[ApiMediaType,...])->tuple[ctypes._Pointer[native.ApiMediaType]|None,int]:
|
|
137
|
+
'Project one bounded media-type collection and its child graphs.';projected:list[native.ApiMediaType]=[]
|
|
138
|
+
for source in self._values(values,'content'):
|
|
139
|
+
if not isinstance(source,ApiMediaType):raise TypeError('content must contain ApiMediaType values')
|
|
140
|
+
target=native.ApiMediaType();_init(target,'coakka_http_host_api_media_type_init');target.media_type=self._text(source.media_type,'media_type');target.schema=self.schema(source.schema);target.examples,target.example_count=self.examples(source.examples);projected.append(target)
|
|
141
|
+
return self._array(native.ApiMediaType,projected),len(projected)
|
|
142
|
+
def metadata(self,source:RouteApiMetadata)->ctypes._Pointer[native.RouteApiMetadata]:
|
|
143
|
+
'Build one complete borrowed metadata root for synchronous copying.'
|
|
144
|
+
if not isinstance(source,RouteApiMetadata):raise TypeError('route.api_metadata must be a RouteApiMetadata')
|
|
145
|
+
target=native.RouteApiMetadata();_init(target,'coakka_http_host_route_api_metadata_init');target.deprecated=self._bool(source.deprecated,'metadata.deprecated');target.operation_id=self._text(source.operation_id,'metadata.operation_id');target.summary=self._text(source.summary,'metadata.summary');target.description=self._text(source.description,'metadata.description');tags=[self._text(value,'metadata.tags')for value in self._values(source.tags,'tags')];tag_array=self.arena.array(native.Bytes,tags);target.tags=ctypes.cast(tag_array,ctypes.POINTER(native.Bytes))if tag_array else None;target.tag_count=len(tags);parameters:list[native.ApiParameter]=[]
|
|
146
|
+
for parameter in self._values(source.parameters,'parameters'):
|
|
147
|
+
if not isinstance(parameter,ApiParameter):raise TypeError('parameters must contain ApiParameter values')
|
|
148
|
+
if not isinstance(parameter.location,ApiParameterLocation):raise TypeError('parameter.location must be an ApiParameterLocation')
|
|
149
|
+
native_parameter=native.ApiParameter();_init(native_parameter,'coakka_http_host_api_parameter_init');native_parameter.location=int(parameter.location);native_parameter.name=self._text(parameter.name,'parameter.name');native_parameter.description=self._text(parameter.description,'parameter.description');native_parameter.schema=self.schema(parameter.schema);native_parameter.examples,native_parameter.example_count=self.examples(parameter.examples);native_parameter.required=self._bool(parameter.required,'parameter.required');parameters.append(native_parameter)
|
|
150
|
+
target.parameters=self._array(native.ApiParameter,parameters);target.parameter_count=len(parameters)
|
|
151
|
+
if source.request_body is not None:
|
|
152
|
+
if not isinstance(source.request_body,ApiRequestBody):raise TypeError('request_body must be an ApiRequestBody')
|
|
153
|
+
body=native.ApiRequestBody();_init(body,'coakka_http_host_api_request_body_init');body.required=self._bool(source.request_body.required,'request_body.required');body.description=self._text(source.request_body.description,'request_body.description');body.content,body.content_count=self.media_types(source.request_body.content);target.request_body=self._pointer(body)
|
|
154
|
+
responses:list[native.ApiResponse]=[]
|
|
155
|
+
for response in self._values(source.responses,'responses'):
|
|
156
|
+
if not isinstance(response,ApiResponse):raise TypeError('responses must contain ApiResponse values')
|
|
157
|
+
native_response=native.ApiResponse();_init(native_response,'coakka_http_host_api_response_init');native_response.selector=2 if response.status is None else 1;native_response.status_code=0 if response.status is None else _unsigned(response.status,32,'response.status');native_response.description=self._text(response.description,'response.description');headers:list[native.ApiHeader]=[]
|
|
158
|
+
for header in self._values(response.headers,'response.headers'):
|
|
159
|
+
if not isinstance(header,ApiResponseHeader):raise TypeError('response.headers must contain ApiResponseHeader values')
|
|
160
|
+
native_header=native.ApiHeader();_init(native_header,'coakka_http_host_api_header_init');native_header.name=self._text(header.name,'response.header.name');native_header.description=self._text(header.description,'response.header.description');native_header.schema=self.schema(header.schema);native_header.examples,native_header.example_count=self.examples(header.examples);headers.append(native_header)
|
|
161
|
+
native_response.headers=self._array(native.ApiHeader,headers);native_response.header_count=len(headers);native_response.content,native_response.content_count=self.media_types(response.content);responses.append(native_response)
|
|
162
|
+
target.responses=self._array(native.ApiResponse,responses);target.response_count=len(responses);requirements:list[native.ApiSecurityRequirement]=[]
|
|
163
|
+
for security in self._values(source.security_requirements,'security_requirements'):
|
|
164
|
+
if not isinstance(security,ApiSecurityRequirement):raise TypeError('security_requirements must contain ApiSecurityRequirement values')
|
|
165
|
+
requirement=native.ApiSecurityRequirement();_init(requirement,'coakka_http_host_api_security_requirement_init');uses:list[native.ApiSecurityUse]=[]
|
|
166
|
+
for use in self._values(security.uses,'security uses'):
|
|
167
|
+
if not isinstance(use,ApiSecurityUse):raise TypeError('security uses must contain ApiSecurityUse values')
|
|
168
|
+
native_use=native.ApiSecurityUse();_init(native_use,'coakka_http_host_api_security_use_init');native_use.scheme_reference=self._text(use.scheme_reference,'security.scheme_reference');scopes=[self._text(scope,'security.scope')for scope in self._values(use.scopes,'security scopes')];scope_array=self.arena.array(native.Bytes,scopes);native_use.scopes=ctypes.cast(scope_array,ctypes.POINTER(native.Bytes))if scope_array else None;native_use.scope_count=len(scopes);uses.append(native_use)
|
|
169
|
+
requirement.uses=self._array(native.ApiSecurityUse,uses);requirement.use_count=len(uses);requirements.append(requirement)
|
|
170
|
+
target.security_requirements=self._array(native.ApiSecurityRequirement,requirements);target.security_requirement_count=len(requirements);definitions:list[native.ApiSchemaDefinition]=[]
|
|
171
|
+
for definition_value in self._values(source.schema_definitions,'schema_definitions'):
|
|
172
|
+
if not isinstance(definition_value,ApiSchemaDefinition):raise TypeError('schema_definitions must contain ApiSchemaDefinition values')
|
|
173
|
+
definition=native.ApiSchemaDefinition();_init(definition,'coakka_http_host_api_schema_definition_init');definition.name=self._text(definition_value.name,'schema_definition.name');definition.schema=self.schema(definition_value.schema);definitions.append(definition)
|
|
174
|
+
target.schema_definitions=self._array(native.ApiSchemaDefinition,definitions);target.schema_definition_count=len(definitions);return self._pointer(target)
|
|
175
|
+
def _native_route(value:Route,arena:native.Arena,metadata_budget:_MetadataBudget)->native.Route:
|
|
176
|
+
if not isinstance(value,Route):raise TypeError('routes must contain Route values')
|
|
177
|
+
result=native.Route();_init(result,'coakka_http_host_route_init');result.route_id=_unsigned(value.id,64,'route.id');result.handler_binding_id=_unsigned(value.handler_binding_id or value.id,64,'route.handler_binding_id')
|
|
178
|
+
if value.websocket:result.flags|=1
|
|
179
|
+
if value.body_delivery is BodyDelivery.STREAM:result.flags|=2
|
|
180
|
+
result.method=arena.bytes(value.method,encoding='ascii');result.encoded_path_pattern=arena.bytes(value.pattern);policy=value.body_policy;result.body_policy.enabled=int(policy.enabled);result.body_policy.accept_absent=int(policy.accept_absent);result.body_policy.accept_other=int(policy.accept_other);result.body_policy.accept_form_urlencoded=int(policy.accept_form_urlencoded);result.body_policy.accept_multipart_form_data=int(policy.accept_multipart_form_data);result.body_policy.max_body_bytes=_unsigned(policy.max_body_bytes,64,'body_policy.max_body_bytes')
|
|
181
|
+
if value.api_metadata is not None:result.api_metadata=_MetadataProjection(arena,metadata_budget).metadata(value.api_metadata)
|
|
182
|
+
return result
|
|
183
|
+
def _project_configuration(value:RuntimeConfig)->tuple[native.Configuration,ctypes._Pointer[native.Route]|None,int,native.Arena]:
|
|
184
|
+
if not isinstance(value,RuntimeConfig):raise TypeError('configuration must be a RuntimeConfig')
|
|
185
|
+
arena=native.Arena();configuration=native.Configuration();_init(configuration,'coakka_http_host_configuration_init');execution=native.ExecutionOptions(ctypes.sizeof(native.ExecutionOptions),int(ExecutionMode.PREFER_SUPPORTED),1,0);arena.values.append(execution);configuration.execution=ctypes.pointer(execution);listener=value.listener;configuration.port=_unsigned(listener.port,16,'listener.port');configuration.protocol=int(listener.protocol);configuration.security=int(listener.security);configuration.bind_address=arena.bytes(listener.bind_address);configuration.credential_generation=_unsigned(listener.credential_generation,64,'listener.credential_generation');configuration.credential_id=arena.bytes(listener.credential_id);configuration.certificate_chain_file=arena.bytes(listener.certificate_chain_file);configuration.private_key_file=arena.bytes(listener.private_key_file);configuration.trust_roots_file=arena.bytes(listener.trust_roots_file);configuration.use_io_uring=int(value.io_backend is IoBackend.IO_URING)
|
|
186
|
+
for limit_field in fields(value.limits):
|
|
187
|
+
name=limit_field.name;native_name='max_total_segments'if name=='max_total_route_segments'else name;bits=64 if native_name in native._LIMIT_U64 or native_name=='event_loop_cpu_mask'else 32;number=_unsigned(getattr(value.limits,name),bits,name)
|
|
188
|
+
if number:setattr(configuration.limits,native_name,number)
|
|
189
|
+
for name in('reserve_websocket','omit_buffered_success_events'):
|
|
190
|
+
flag=getattr(value,name)
|
|
191
|
+
if not isinstance(flag,bool):raise TypeError(f"{name} must be a bool")
|
|
192
|
+
setattr(configuration,name,int(flag))
|
|
193
|
+
monitor=value.monitor;configuration.monitor.event_capacity=_unsigned(monitor.event_capacity,32,'monitor.event_capacity');configuration.monitor.max_events_per_read=_unsigned(monitor.max_events_per_read,32,'monitor.max_events_per_read');configuration.monitor.categories=int(monitor.categories);configuration.monitor.initial_collection=int(monitor.collection);configuration.monitor.signal_reserved=int(monitor.signal_reserved);configuration.monitor.fixed_latency_buckets=int(monitor.fixed_latency_buckets);compression=value.compression
|
|
194
|
+
if not isinstance(compression,Compression)or not isinstance(compression.mode,CompressionMode):raise TypeError('compression requires a Compression value with a CompressionMode')
|
|
195
|
+
configuration.compression.enabled=int(compression.mode is CompressionMode.GZIP)
|
|
196
|
+
if compression.mode is CompressionMode.GZIP:
|
|
197
|
+
for name in('minimum_body_bytes','maximum_body_bytes','maximum_encoded_bytes','workspace_bytes'):setattr(configuration.compression,name,_unsigned(getattr(compression,name),64,f"compression.{name}"))
|
|
198
|
+
if isinstance(compression.gzip_level,bool)or not isinstance(compression.gzip_level,int):raise TypeError('compression.gzip_level must be an integer')
|
|
199
|
+
if compression.gzip_level<-(1<<31)or compression.gzip_level>=1<<31:raise ValueError('compression.gzip_level is outside the signed 32-bit range')
|
|
200
|
+
configuration.compression.gzip_level=compression.gzip_level
|
|
201
|
+
mounts:list[native.StaticMount]=[]
|
|
202
|
+
for mount_source in value.static_mounts:
|
|
203
|
+
mount_item=native.StaticMount();_init(mount_item,'coakka_http_host_static_mount_init')
|
|
204
|
+
if mount_source.spa_fallback_file:mount_item.flags|=1
|
|
205
|
+
if mount_source.include_dotfiles:mount_item.flags|=2
|
|
206
|
+
mount_item.url_prefix=arena.bytes(mount_source.url_prefix);mount_item.root_path=arena.bytes(mount_source.root_path)
|
|
207
|
+
if mount_source.index_file:mount_item.index_file=arena.bytes(mount_source.index_file)
|
|
208
|
+
if mount_source.spa_fallback_file:mount_item.spa_fallback_file=arena.bytes(mount_source.spa_fallback_file)
|
|
209
|
+
if mount_source.cache_control:mount_item.cache_control=arena.bytes(mount_source.cache_control)
|
|
210
|
+
mounts.append(mount_item)
|
|
211
|
+
mount_array=arena.array(native.StaticMount,mounts);configuration.static_mount_count=len(mounts);configuration.static_mounts=ctypes.cast(mount_array,ctypes.POINTER(native.StaticMount))if mount_array else None
|
|
212
|
+
if value.max_total_static_assets:configuration.max_total_static_assets=_unsigned(value.max_total_static_assets,32,'max_total_static_assets')
|
|
213
|
+
authorities:list[native.FileAuthority]=[]
|
|
214
|
+
for authority_source in value.file_authorities:authority_item=native.FileAuthority();_init(authority_item,'coakka_http_host_file_authority_init');authority_item.authority_id=_unsigned(authority_source.authority_id,64,'authority_id');authority_item.root_path=arena.bytes(authority_source.root_path);authority_item.max_file_bytes=_unsigned(authority_source.max_file_bytes,64,'max_file_bytes');authority_item.max_active_files=_unsigned(authority_source.max_active_files,32,'max_active_files');authorities.append(authority_item)
|
|
215
|
+
authority_array=arena.array(native.FileAuthority,authorities);configuration.file_authority_count=len(authorities);configuration.file_authorities=ctypes.cast(authority_array,ctypes.POINTER(native.FileAuthority))if authority_array else None;endpoints_by_target:list[ctypes.Array[native.OutboundEndpoint]]=[];targets:list[native.OutboundTarget]=[]
|
|
216
|
+
for target_source in value.outbound_targets:
|
|
217
|
+
endpoints:list[native.OutboundEndpoint]=[]
|
|
218
|
+
for endpoint in target_source.endpoints:endpoint_item=native.OutboundEndpoint();_init(endpoint_item,'coakka_http_host_outbound_endpoint_init');endpoint_item.weight=_unsigned(endpoint.weight,32,'endpoint.weight');endpoint_item.node_id=arena.bytes(endpoint.node_id);endpoint_item.connect_host=arena.bytes(endpoint.connect_host);endpoint_item.connect_port=_unsigned(endpoint.connect_port,16,'endpoint.connect_port');endpoint_item.http_authority=arena.bytes(endpoint.http_authority);endpoint_item.security=int(endpoint.security);endpoint_item.available=int(endpoint.available);endpoint_item.tls_peer_identity=arena.bytes(endpoint.tls_peer_identity);endpoint_item.tls_trust_generation=_unsigned(endpoint.tls_trust_generation,64,'endpoint.tls_trust_generation');endpoint_item.tls_client_identity_generation=_unsigned(endpoint.tls_client_identity_generation,64,'endpoint.tls_client_identity_generation');endpoints.append(endpoint_item)
|
|
219
|
+
endpoint_array=arena.array(native.OutboundEndpoint,endpoints)
|
|
220
|
+
if endpoint_array:endpoints_by_target.append(endpoint_array)
|
|
221
|
+
target=native.OutboundTarget();_init(target,'coakka_http_host_outbound_target_init');target.strategy=int(target_source.strategy);target.name=arena.bytes(target_source.name);target.generation=_unsigned(target_source.generation,64,'target.generation');target.endpoints=ctypes.cast(endpoint_array,ctypes.POINTER(native.OutboundEndpoint))if endpoint_array else None;target.endpoint_count=len(endpoints);targets.append(target)
|
|
222
|
+
arena.values.extend(endpoints_by_target);target_array=arena.array(native.OutboundTarget,targets);configuration.outbound_target_count=len(targets);configuration.outbound_targets=ctypes.cast(target_array,ctypes.POINTER(native.OutboundTarget))if target_array else None;trusts:list[native.OutboundTrust]=[]
|
|
223
|
+
for trust_source in value.outbound_trusts:trust_item=native.OutboundTrust();_init(trust_item,'coakka_http_host_outbound_trust_init');trust_item.generation=_unsigned(trust_source.generation,64,'trust.generation');trust_item.ca_pem=arena.bytes(trust_source.ca_pem);trusts.append(trust_item)
|
|
224
|
+
trust_array=arena.array(native.OutboundTrust,trusts);configuration.outbound_trust_count=len(trusts);configuration.outbound_trusts=ctypes.cast(trust_array,ctypes.POINTER(native.OutboundTrust))if trust_array else None;identities:list[native.OutboundIdentity]=[]
|
|
225
|
+
for identity_source in value.outbound_identities:identity_item=native.OutboundIdentity();_init(identity_item,'coakka_http_host_outbound_identity_init');identity_item.generation=_unsigned(identity_source.generation,64,'identity.generation');identity_item.certificate_chain_pem=arena.bytes(identity_source.certificate_chain_pem);identity_item.private_key_pem=arena.bytes(identity_source.private_key_pem);identities.append(identity_item)
|
|
226
|
+
identity_array=arena.array(native.OutboundIdentity,identities);configuration.outbound_identity_count=len(identities);configuration.outbound_identities=ctypes.cast(identity_array,ctypes.POINTER(native.OutboundIdentity))if identity_array else None;metadata_budget=_MetadataBudget();routes=[_native_route(route,arena,metadata_budget)for route in value.routes];route_array=arena.array(native.Route,routes);route_pointer=ctypes.cast(route_array,ctypes.POINTER(native.Route))if route_array else None;return configuration,route_pointer,len(routes),arena
|
|
227
|
+
def features()->Capability:"Read the loaded Core image's capabilities without probing the OS.";return Capability(native.LIBRARY.library.coakka_http_host_features())
|
|
228
|
+
def inspect_runtime()->RuntimeInspection:'Pull global Core facts, including its bounded optional-backend probe.';value=native.RuntimeInspection();_init(value,'coakka_http_host_runtime_info_init');native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_runtime_inspect(ctypes.byref(value)));return RuntimeInspection(abi_version=int(value.abi_version),features=Capability(value.features),io_uring_compiled=bool(value.io_uring_compiled),io_uring_probed=bool(value.io_uring_probed),io_uring_supported=bool(value.io_uring_supported),io_uring_probe_error=int(value.io_uring_probe_error))
|
|
229
|
+
def plan_service(required_capabilities:Capability,adapter_capabilities:Capability=Capability.ALL)->ServicePlan:
|
|
230
|
+
'Validate capability dependencies without probing the operating system.';request=native.ServicePlanRequest();result=native.ServicePlan();_init(request,'coakka_http_host_service_plan_request_init');_init(result,'coakka_http_host_service_plan_init');request.required_capabilities=int(required_capabilities);request.adapter_capabilities=int(adapter_capabilities);status=native.LIBRARY.library.coakka_http_host_plan_service(ctypes.byref(request),ctypes.byref(result))
|
|
231
|
+
if status not in(native.STATUS_OK,9):native.LIBRARY.check(status)
|
|
232
|
+
return ServicePlan(supported=bool(result.supported),required_capabilities=Capability(result.required_capabilities),adapter_capabilities=Capability(result.adapter_capabilities),missing_capabilities=Capability(result.missing_capabilities))
|
|
233
|
+
@dataclass(frozen=True,slots=True)
|
|
234
|
+
class InspectionOptions:'Startup intent for authenticated runtime inspection.';bearer_file:str;runtime_instance_id:int;runtime_generation:int;allow_plaintext_loopback:bool|None=None
|
|
235
|
+
def _project_inspection(value:InspectionOptions,arena:native.Arena)->native.InspectionOptions:
|
|
236
|
+
if not isinstance(value,InspectionOptions):raise TypeError('inspection must be an InspectionOptions')
|
|
237
|
+
result=native.InspectionOptions();_init(result,'coakka_http_host_inspection_options_init');result.runtime_instance_id=_unsigned(value.runtime_instance_id,64,'runtime_instance_id');result.runtime_generation=_unsigned(value.runtime_generation,64,'runtime_generation');result.bearer_file=arena.bytes(value.bearer_file)
|
|
238
|
+
if value.allow_plaintext_loopback is not None:
|
|
239
|
+
if not isinstance(value.allow_plaintext_loopback,bool):raise TypeError('allow_plaintext_loopback must be a bool or None')
|
|
240
|
+
result.allow_plaintext_loopback=int(value.allow_plaintext_loopback)
|
|
241
|
+
return result
|
|
242
|
+
class Runtime:
|
|
243
|
+
'Lifecycle owner for one callback-free native HTTP runtime instance.'
|
|
244
|
+
def __init__(self,configuration:RuntimeConfig=RuntimeConfig(),*,inspection:InspectionOptions|None=None)->None:
|
|
245
|
+
'Validate and copy one complete configuration into native ownership.';projected,routes,route_count,arena=_project_configuration(configuration);service=ctypes.c_void_p();failed_index=ctypes.c_uint32(_FAILED_INDEX_NONE);library=native.LIBRARY.library;arguments=routes,route_count,ctypes.byref(service),ctypes.byref(failed_index)
|
|
246
|
+
if inspection is None:status=library.coakka_http_host_service_create(ctypes.byref(projected),*arguments)
|
|
247
|
+
else:native.LIBRARY.bind_inspection_startup();options=_project_inspection(inspection,arena);status=library.coakka_http_host_service_create_with_inspection(ctypes.byref(projected),ctypes.byref(options),*arguments)
|
|
248
|
+
del arena
|
|
249
|
+
if status!=native.STATUS_OK:
|
|
250
|
+
try:native.LIBRARY.check(status)
|
|
251
|
+
except NativeError as error:
|
|
252
|
+
if failed_index.value!=_FAILED_INDEX_NONE:raise NativeError(error.status,f"route {failed_index.value}: {error}",item_index=int(failed_index.value))from error
|
|
253
|
+
raise
|
|
254
|
+
if not service.value:raise RuntimeError('native runtime returned an empty service')
|
|
255
|
+
self._service=service;self._condition=threading.Condition();self._lifecycle=threading.Lock();self._request_reader=threading.Lock();self._request_event=native.RequestEvent();_init(self._request_event,'coakka_http_host_request_event_init');self._request_event_pointer=ctypes.pointer(self._request_event);self._request_event_address=ctypes.addressof(self._request_event);self._request_view=native.RequestView();_init(self._request_view,'coakka_http_host_request_view_init');self._request_view_pointer=ctypes.pointer(self._request_view);self._request_header=native.Header();self._request_header_pointer=ctypes.pointer(self._request_header);self._websocket_reader=threading.Lock();self._outbound_reader=threading.Lock();self._monitor_waiter=threading.Lock();self._active=0;self._closing=False;projection=managed_projection.PROJECTION;self._service_record_factory=None if projection is None else projection.service_record
|
|
256
|
+
@contextmanager
|
|
257
|
+
def _borrow(self)->Iterator[ctypes.c_void_p]:
|
|
258
|
+
with self._condition:
|
|
259
|
+
if not self._service.value or self._closing:raise RuntimeError('runtime is closing or closed')
|
|
260
|
+
self._active+=1;service=self._service
|
|
261
|
+
try:yield service
|
|
262
|
+
finally:
|
|
263
|
+
with self._condition:
|
|
264
|
+
self._active-=1
|
|
265
|
+
if not self._active:self._condition.notify_all()
|
|
266
|
+
@property
|
|
267
|
+
def capabilities(self)->Capability:"Return the loaded Core image's immutable capability matrix.";return features()
|
|
268
|
+
def info(self)->RuntimeInfo:
|
|
269
|
+
'Copy backend request, probe, fallback, and lifecycle facts.';value=native.ServiceInfo();_init(value,'coakka_http_host_service_info_init')
|
|
270
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_info(service,ctypes.byref(value)))
|
|
271
|
+
return RuntimeInfo(abi_version=int(value.abi_version),io_uring_requested=bool(value.io_uring_requested),io_uring_effective=bool(value.io_uring_effective),io_uring_compiled=bool(value.io_uring_compiled),io_uring_probed=bool(value.io_uring_probed),io_uring_supported=bool(value.io_uring_supported),service_started=bool(value.service_started),fallback_reason=IoBackendFallbackReason(value.io_fallback_reason),io_uring_probe_error=int(value.io_uring_probe_error),request_notification_batch_size=int(value.request_notification_batch_size),event_loop_cpu_mask=int(value.event_loop_cpu_mask),event_loop_cpu_mask_supported=bool(value.event_loop_cpu_mask_supported),event_loop_cpu_mask_applied=bool(value.event_loop_cpu_mask_applied),terminal_notification_batch_size=int(value.terminal_notification_batch_size),execution=ExecutionInfo(selection=ExecutionSelection(requested_mode=ExecutionMode(value.selection.requested_mode),requested_count=int(value.selection.requested_count),selected_count=int(value.selection.selected_count),reason=ExecutionReason(value.selection.reason)),configured_event_loops=int(value.execution.configured_event_loops),active_event_loops=int(value.execution.active_event_loops),draining_event_loops=int(value.execution.draining_event_loops),failed_event_loops=int(value.execution.failed_event_loops),observed=bool(value.execution.observed)))
|
|
272
|
+
runtime_info=info
|
|
273
|
+
def effective_limits(self)->Limits:
|
|
274
|
+
"Copy Core's accepted startup limits; never reconstruct defaults.";value=native.Limits();_init(value,'coakka_http_host_limits_init')
|
|
275
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_limits(service,ctypes.byref(value)))
|
|
276
|
+
projected:dict[str,int]={}
|
|
277
|
+
for limit_field in fields(Limits):
|
|
278
|
+
name=limit_field.name;target='max_total_segments'if name=='max_total_route_segments'else name;number=int(getattr(value,target))
|
|
279
|
+
if name.endswith('_notification_profile'):
|
|
280
|
+
try:number=NotificationProfile(number)
|
|
281
|
+
except ValueError:pass
|
|
282
|
+
projected[name]=number
|
|
283
|
+
return Limits(**projected)
|
|
284
|
+
def wait_drained(self,timeout_ms:int=0)->DrainOutcome:
|
|
285
|
+
"Observe Core's drain barrier while readers/handlers keep progressing.\n\n This never stops Core or frees leases. A finite caller wait may return\n PENDING; EXPIRED is Core's terminal outcome and must not become success.\n ";timeout=_unsigned(timeout_ms,64,'timeout_ms')
|
|
286
|
+
if timeout>_MAX_WAIT_MS:raise ValueError('drain wait exceeds the 60-second bound')
|
|
287
|
+
state=ctypes.c_uint32()
|
|
288
|
+
with self._lifecycle,self._borrow()as service:status=native.LIBRARY.library.coakka_http_host_service_wait_drained(service,timeout,ctypes.byref(state))
|
|
289
|
+
try:outcome:DrainState|int=DrainState(state.value)
|
|
290
|
+
except ValueError:outcome=state.value
|
|
291
|
+
return DrainOutcome(int(status),outcome)
|
|
292
|
+
def start(self)->None:
|
|
293
|
+
'Bind the listener and start native admission.'
|
|
294
|
+
with self._lifecycle,self._borrow()as service:issue=native.Issue();_init(issue,'coakka_http_host_issue_init');native.LIBRARY.check_issue(native.LIBRARY.library.coakka_http_host_service_start(service,ctypes.byref(issue)),issue)
|
|
295
|
+
@property
|
|
296
|
+
def port(self)->int:
|
|
297
|
+
'Return the selected listener port after startup.';value=ctypes.c_uint16()
|
|
298
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_port(service,ctypes.byref(value)))
|
|
299
|
+
return int(value.value)
|
|
300
|
+
def drain(self)->None:
|
|
301
|
+
'Begin Core drain; use wait_drained to observe completion separately.'
|
|
302
|
+
with self._lifecycle,self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_begin_drain(service))
|
|
303
|
+
def stop(self)->None:
|
|
304
|
+
'Perform the bounded idempotent stop transition.'
|
|
305
|
+
with self._lifecycle,self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_stop(service))
|
|
306
|
+
def interrupt(self)->None:
|
|
307
|
+
'Wake the sole inbound reader without creating an event.'
|
|
308
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_interrupt_requests(service))
|
|
309
|
+
def interrupt_outbound(self)->None:
|
|
310
|
+
'Wake the sole outbound reader without creating an event.'
|
|
311
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_interrupt_outbound(service))
|
|
312
|
+
def close(self)->None:
|
|
313
|
+
'Force cleanup after borrowers return; retain the owner on refusal.\n\n This is not a graceful-drain certificate. Service.close owns that policy;\n low-level callers use drain/wait_drained explicitly before this cleanup.\n '
|
|
314
|
+
with self._lifecycle:
|
|
315
|
+
with self._condition:
|
|
316
|
+
if not self._service.value:return
|
|
317
|
+
self._closing=True;service=self._service
|
|
318
|
+
try:
|
|
319
|
+
native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_stop(service))
|
|
320
|
+
with self._condition:
|
|
321
|
+
while self._active:self._condition.wait()
|
|
322
|
+
native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_destroy(ctypes.byref(self._service)))
|
|
323
|
+
finally:
|
|
324
|
+
with self._condition:self._closing=False;self._condition.notify_all()
|
|
325
|
+
def __enter__(self)->Self:'Return this open runtime for scoped synchronous ownership.';return self
|
|
326
|
+
def __exit__(self,_type:object,_value:object,_traceback:object)->None:'Close the runtime when its ownership scope ends.';self.close()
|
|
327
|
+
def take_event(self,timeout_ms:int=0)->Event|None:
|
|
328
|
+
'Wait for and copy one inbound event, releasing its lease exactly once.'
|
|
329
|
+
with self._request_reader,self._borrow()as service:return self._take_borrowed_event(service,_timeout(timeout_ms))
|
|
330
|
+
@contextmanager
|
|
331
|
+
def _service_event_reader(self)->Iterator[ctypes.c_void_p]:
|
|
332
|
+
"Hold the managed reader's native owner for its whole serialized loop."
|
|
333
|
+
with self._request_reader,self._borrow()as service:
|
|
334
|
+
try:yield service
|
|
335
|
+
except BaseException as failure:
|
|
336
|
+
try:self._release_retained_reader_event(service)
|
|
337
|
+
except BaseException as cleanup:raise BaseExceptionGroup('request projection and lease retirement failed',[failure,cleanup])from None
|
|
338
|
+
raise
|
|
339
|
+
else:self._release_retained_reader_event(service)
|
|
340
|
+
def _release_retained_reader_event(self,service:ctypes.c_void_p)->None:
|
|
341
|
+
'Retry only a Core event still retained after a release refusal.'
|
|
342
|
+
if self._request_event.kind!=0:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_release_request(service,self._request_event_pointer))
|
|
343
|
+
def _take_service_event_borrowed(self,service:ctypes.c_void_p)->Event|_ServiceRequestEvent|None:
|
|
344
|
+
'Take one managed event while `_service_event_reader` owns the borrow.';projection=managed_projection.PROJECTION
|
|
345
|
+
if projection is None:raise RuntimeError('managed service requires its native projection image')
|
|
346
|
+
address=service.value
|
|
347
|
+
if address is None:raise RuntimeError('native runtime returned an empty service')
|
|
348
|
+
return self._copy_projected_service_event(projection.take(self,address,self._request_event_address))
|
|
349
|
+
def _copy_projected_headers(self,values:tuple[str,...]|None)->Headers:
|
|
350
|
+
'Build immutable Header values from one native-owned flat tuple.'
|
|
351
|
+
if values is None:return _EMPTY_HEADERS
|
|
352
|
+
projection=managed_projection.PROJECTION
|
|
353
|
+
if projection is None:raise RuntimeError('managed headers require their native projection image')
|
|
354
|
+
return projection.headers(values)
|
|
355
|
+
def _copy_projected_service_event(self,raw:_ProjectedValues)->Event|_ServiceRequestEvent|None:
|
|
356
|
+
'Project one private owning native tuple into the public value graph.';status=cast(int,raw[0])
|
|
357
|
+
if status==native.STATUS_TIMEOUT:return None
|
|
358
|
+
native.LIBRARY.check(status);projected=cast(_ProjectedSuccess,raw);kind=projected[1];sequence=projected[4];handler_binding_id=projected[5]
|
|
359
|
+
if kind==_REQUEST_EVENT_KIND:
|
|
360
|
+
constructor=self._service_record_factory
|
|
361
|
+
if constructor is None:raise RuntimeError('managed service requires its native projection image')
|
|
362
|
+
return constructor(raw)
|
|
363
|
+
exchange=ExchangeId(projected[2],projected[3]);event_values=cast(_ProjectedEvent,projected);return Event(kind=EventKind(kind),exchange=exchange,sequence=sequence,handler_binding_id=handler_binding_id,data=event_values[6]or b'',stream_trailers=self._copy_projected_headers(event_values[7]))
|
|
364
|
+
def _take_borrowed_event(self,service:ctypes.c_void_p,native_timeout:int)->Event|None:
|
|
365
|
+
'Own one take/release law under a caller-proven Runtime borrow.';value=self._request_event;status=native.LIBRARY.library.coakka_http_host_take_request(service,native_timeout,self._request_event_pointer)
|
|
366
|
+
if status==native.STATUS_TIMEOUT:return None
|
|
367
|
+
native.LIBRARY.check(status)
|
|
368
|
+
try:return self._copy_event(service,value)
|
|
369
|
+
finally:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_release_request(service,self._request_event_pointer))
|
|
370
|
+
def _copy_inbound_headers(self,count:int,item_name:str)->Headers:
|
|
371
|
+
'Reuse sole-reader ABI descriptors while returning only owned values.'
|
|
372
|
+
if count>_MAX_HEADER_COUNT:raise RuntimeError('native header count exceeds the Python bound')
|
|
373
|
+
if count==0:return _EMPTY_HEADERS
|
|
374
|
+
project=getattr(native.LIBRARY.library,item_name);values:list[Header]=[]
|
|
375
|
+
for index in range(count):
|
|
376
|
+
if not project(self._request_event_pointer,index,self._request_header_pointer):raise RuntimeError('native header projection rejected a declared index')
|
|
377
|
+
values.append(_copy_header(self._request_header))
|
|
378
|
+
return Headers._from_native(tuple(values))
|
|
379
|
+
def _copy_event(self,service:ctypes.c_void_p,value:native.RequestEvent)->Event:kind=EventKind(value.kind);exchange=ExchangeId(int(value.exchange.slot),int(value.exchange.generation));data=native.copy_bytes(value.data)if kind is EventKind.REQUEST_DATA else b'';request=self._copy_request(service,value,exchange)if kind is EventKind.REQUEST else None;trailers=self._copy_inbound_headers(int(native.LIBRARY.library.coakka_http_host_request_stream_trailer_count(self._request_event_pointer)),'coakka_http_host_request_stream_trailer')if kind is EventKind.REQUEST_TRAILERS else _EMPTY_HEADERS;return Event(kind=kind,exchange=exchange,sequence=int(value.sequence),handler_binding_id=int(value.handler_binding_id),data=data,request=request,stream_trailers=trailers)
|
|
380
|
+
def _copy_request(self,service:ctypes.c_void_p,value:native.RequestEvent,exchange:ExchangeId)->Request:
|
|
381
|
+
library=native.LIBRARY.library;view=self._request_view;native.LIBRARY.check(library.coakka_http_host_request_view(self._request_event_pointer,self._request_view_pointer));path_count=int(view.path_parameter_count);query_count=int(view.query_parameter_count)
|
|
382
|
+
if path_count>_MAX_ITEMS or query_count>_MAX_ITEMS:raise RuntimeError('native request parameter count exceeds the Python bound')
|
|
383
|
+
paths:list[PathParameter]=[]
|
|
384
|
+
for index in range(path_count):
|
|
385
|
+
path_item=native.PathParameter()
|
|
386
|
+
if not library.coakka_http_host_request_path_parameter(self._request_event_pointer,index,ctypes.byref(path_item)):raise RuntimeError('native path parameter projection failed')
|
|
387
|
+
paths.append(PathParameter(native.copy_text(path_item.name,'utf-8'),native.copy_text(path_item.encoded_value,'utf-8')))
|
|
388
|
+
queries:list[QueryParameter]=[]
|
|
389
|
+
for index in range(query_count):
|
|
390
|
+
query_item=native.QueryParameter()
|
|
391
|
+
if not library.coakka_http_host_request_query_parameter(self._request_event_pointer,index,ctypes.byref(query_item)):raise RuntimeError('native query parameter projection failed')
|
|
392
|
+
queries.append(QueryParameter(native.copy_text(query_item.encoded_key,'utf-8'),native.copy_text(query_item.encoded_value,'utf-8'),bool(query_item.has_value)))
|
|
393
|
+
cancelled=ctypes.c_uint8();native.LIBRARY.check(library.coakka_http_host_request_cancelled(service,value.exchange,ctypes.byref(cancelled)));return Request(exchange=exchange,method=native.copy_text(view.method,'ascii'),scheme=native.copy_text(view.scheme,'ascii'),authority=native.copy_text(view.authority,'latin-1'),target=native.copy_text(view.target,'latin-1'),headers=self._copy_inbound_headers(int(view.header_count),'coakka_http_host_request_header'),body=native.copy_bytes(view.body),body_delivery=BodyDelivery(view.body_delivery),route_id=int(view.route_id),route_generation=int(view.route_generation),binding_revision=int(view.binding_revision),handler_binding_id=int(value.handler_binding_id),path_parameters=tuple(paths),query_parameters=tuple(queries),trailers=self._copy_inbound_headers(int(view.trailer_count),'coakka_http_host_request_trailer'),cancelled=bool(cancelled.value))
|
|
394
|
+
def exchange_cancelled(self,exchange:ExchangeId)->bool:
|
|
395
|
+
'Return current cancellation truth for an admitted exchange.';result=ctypes.c_uint8()
|
|
396
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_request_cancelled(service,_native_exchange(exchange),ctypes.byref(result)))
|
|
397
|
+
return bool(result.value)
|
|
398
|
+
def respond(self,exchange:ExchangeId,response:Response)->None:
|
|
399
|
+
'Copy and submit one buffered response.'
|
|
400
|
+
if not isinstance(response,Response):raise TypeError('response must be a Response')
|
|
401
|
+
projected=_prepared_response(response)
|
|
402
|
+
with self._borrow()as service:
|
|
403
|
+
projection=managed_projection.PROJECTION
|
|
404
|
+
if projection is None:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_respond(service,_native_exchange(exchange),ctypes.byref(projected.value)));return
|
|
405
|
+
if not isinstance(exchange,ExchangeId):raise TypeError('exchange must be an ExchangeId')
|
|
406
|
+
address=service.value
|
|
407
|
+
if address is None:raise RuntimeError('native runtime returned an empty service')
|
|
408
|
+
native.LIBRARY.check(projection.respond(projected,address,exchange.slot,exchange.generation,projected.storage))
|
|
409
|
+
def respond_file(self,exchange:ExchangeId,response:FileResponse)->None:
|
|
410
|
+
'Serve one application-selected path under a configured authority.'
|
|
411
|
+
if not isinstance(response,FileResponse):raise TypeError('response must be a FileResponse')
|
|
412
|
+
arena=native.Arena();value=native.FileResponse();_init(value,'coakka_http_host_file_response_init');value.head.status_code=_unsigned(response.status,32,'response.status');value.head.headers,value.head.header_count,_headers_cacheable=_project_headers(response.headers,arena);value.authority_id=_unsigned(response.authority_id,64,'authority_id');value.encoded_path=arena.bytes(response.encoded_path)
|
|
413
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_respond_file(service,_native_exchange(exchange),ctypes.byref(value)))
|
|
414
|
+
def fail(self,exchange:ExchangeId,failure:Failure)->None:
|
|
415
|
+
'Terminalize one exchange with a typed failure.'
|
|
416
|
+
if not isinstance(failure,Failure):raise TypeError('failure must be a Failure')
|
|
417
|
+
arena=native.Arena();value=native.Failure();_init(value,'coakka_http_host_failure_init');value.reason=_unsigned(failure.reason,32,'failure.reason');value.detail=arena.bytes(failure.detail)
|
|
418
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_fail(service,_native_exchange(exchange),ctypes.byref(value)))
|
|
419
|
+
def start_response_stream(self,exchange:ExchangeId,head:Response)->None:
|
|
420
|
+
'Submit a response head for an application-driven stream.';arena=native.Arena();value=native.Response();_init(value,'coakka_http_host_response_init');value.status_code=_unsigned(head.status,32,'response.status');value.headers,value.header_count,_headers_cacheable=_project_headers(head.headers,arena)
|
|
421
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_start_response_stream(service,_native_exchange(exchange),ctypes.byref(value)))
|
|
422
|
+
def write_response_stream(self,exchange:ExchangeId,data:bytes)->None:
|
|
423
|
+
'Copy one response-stream chunk after writable credit.';arena=native.Arena();value=arena.bytes(data)
|
|
424
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_write_response_stream(service,_native_exchange(exchange),value))
|
|
425
|
+
def finish_response_stream(self,exchange:ExchangeId,trailers:Headers=Headers())->None:
|
|
426
|
+
'Finish one response stream with copied optional trailers.';arena=native.Arena();values,count,_headers_cacheable=_project_headers(trailers,arena)
|
|
427
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_finish_response_stream(service,_native_exchange(exchange),values,count))
|
|
428
|
+
def start_sse(self,exchange:ExchangeId,headers:Headers=Headers())->None:
|
|
429
|
+
'Start one native-serialized server-sent event response.';arena=native.Arena();values,count,_headers_cacheable=_project_headers(headers,arena)
|
|
430
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_start_sse(service,_native_exchange(exchange),values,count))
|
|
431
|
+
def write_sse(self,exchange:ExchangeId,event:SseEvent)->None:
|
|
432
|
+
'Copy one structured server-sent event.';arena=native.Arena();value=native.SseEvent();_init(value,'coakka_http_host_sse_event_init');value.data=arena.bytes(event.data)
|
|
433
|
+
if event.event_type is not None:value.event_type=arena.bytes(event.event_type);value.has_event_type=1
|
|
434
|
+
if event.id is not None:value.id=arena.bytes(event.id);value.has_id=1
|
|
435
|
+
if event.retry_ms is not None:value.retry_ms=_unsigned(event.retry_ms,64,'event.retry_ms');value.has_retry=1
|
|
436
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_write_sse(service,_native_exchange(exchange),ctypes.byref(value)))
|
|
437
|
+
def accept_websocket(self,exchange:ExchangeId,subprotocol:str='')->None:
|
|
438
|
+
'Accept a declared WebSocket route with an optional subprotocol.';arena=native.Arena();value=arena.bytes(subprotocol,encoding='ascii')
|
|
439
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_accept_websocket(service,_native_exchange(exchange),value))
|
|
440
|
+
def take_websocket(self,timeout_ms:int=0)->WebSocketEvent|None:
|
|
441
|
+
'Wait for and copy one WebSocket event, then release its lease.';value=native.WebSocketEvent();_init(value,'coakka_http_host_websocket_event_init')
|
|
442
|
+
with self._websocket_reader,self._borrow()as service:
|
|
443
|
+
status=native.LIBRARY.library.coakka_http_host_take_websocket(service,_timeout(timeout_ms),ctypes.byref(value))
|
|
444
|
+
if status==native.STATUS_TIMEOUT:return None
|
|
445
|
+
native.LIBRARY.check(status)
|
|
446
|
+
try:return WebSocketEvent(kind=WebSocketEventKind(value.kind),session=WebSocketId(int(value.session.slot),int(value.session.generation)),origin_exchange=ExchangeId(int(value.origin_exchange.slot),int(value.origin_exchange.generation)),close_code=int(value.close_code),data=native.copy_bytes(value.data))
|
|
447
|
+
finally:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_release_websocket(service,ctypes.byref(value)))
|
|
448
|
+
def send_websocket(self,session:WebSocketId,kind:WebSocketEventKind,data:bytes)->None:
|
|
449
|
+
'Copy one ordered WebSocket text, binary, ping, or pong frame.';arena=native.Arena()
|
|
450
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_send_websocket(service,_native_websocket(session),int(kind),arena.bytes(data)))
|
|
451
|
+
def close_websocket(self,session:WebSocketId,code:int=1000,reason:bytes=b'')->None:
|
|
452
|
+
'Start one ordered WebSocket close handshake.';arena=native.Arena()
|
|
453
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_close_websocket(service,_native_websocket(session),_unsigned(code,32,'close code'),arena.bytes(reason)))
|
|
454
|
+
def outbound_submit(self,request:OutboundRequest)->OutboundId:
|
|
455
|
+
'Submit one buffered call to a startup-declared logical target.'
|
|
456
|
+
if not isinstance(request,OutboundRequest):raise TypeError('request must be an OutboundRequest')
|
|
457
|
+
arena=native.Arena();value=native.OutboundRequest();call=native.OutboundCall();_init(value,'coakka_http_host_outbound_request_init');value.timeout_ms=_unsigned(request.timeout_ms,32,'request.timeout_ms');value.logical_target=arena.bytes(request.logical_target);value.method=arena.bytes(request.method,encoding='ascii');value.target=arena.bytes(request.target);value.headers,value.header_count,_headers_cacheable=_project_headers(request.headers,arena);value.body=arena.bytes(request.body)
|
|
458
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_submit_outbound(service,ctypes.byref(value),ctypes.byref(call)))
|
|
459
|
+
return OutboundId(int(call.slot),int(call.generation))
|
|
460
|
+
def outbound_cancel(self,call:OutboundId)->None:
|
|
461
|
+
'Request cancellation of one admitted outbound call.'
|
|
462
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_cancel_outbound(service,_native_outbound(call)))
|
|
463
|
+
def take_outbound(self,timeout_ms:int=0)->OutboundTerminal|None:
|
|
464
|
+
'Wait for and copy one outbound terminal, then release its lease.';value=native.OutboundEvent();_init(value,'coakka_http_host_outbound_event_init')
|
|
465
|
+
with self._outbound_reader,self._borrow()as service:
|
|
466
|
+
status=native.LIBRARY.library.coakka_http_host_take_outbound(service,_timeout(timeout_ms),ctypes.byref(value))
|
|
467
|
+
if status==native.STATUS_TIMEOUT:return None
|
|
468
|
+
native.LIBRARY.check(status)
|
|
469
|
+
try:
|
|
470
|
+
if value.response_header_count>_MAX_HEADER_COUNT:raise RuntimeError('outbound header count exceeds the Python bound')
|
|
471
|
+
headers:list[Header]=[]
|
|
472
|
+
for index in range(value.response_header_count):
|
|
473
|
+
item=native.Header()
|
|
474
|
+
if not native.LIBRARY.library.coakka_http_host_outbound_header(service,ctypes.byref(value),index,ctypes.byref(item)):raise RuntimeError('native outbound header projection failed')
|
|
475
|
+
headers.append(_copy_header(item))
|
|
476
|
+
return OutboundTerminal(reason=OutboundReason(value.reason),call=OutboundId(int(value.call.slot),int(value.call.generation)),logical_target=native.copy_bytes(value.logical_target).decode('utf-8'),target_generation=int(value.target_generation),selected_node_id=native.copy_bytes(value.selected_node_id).decode('utf-8'),phase=OutboundPhase(value.phase),retry=OutboundRetry(value.retry),certainty=OutboundCertainty(value.certainty),response_status=int(value.response_status),provider_code=int(value.provider_code),response_headers=Headers(headers),response_body=native.copy_bytes(value.response_body),diagnostic=native.copy_bytes(value.diagnostic).decode('utf-8',errors='replace'))
|
|
477
|
+
finally:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_release_outbound(service,ctypes.byref(value)))
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _copy_health(value:native.Health)->Health:return Health(lifecycle=int(value.lifecycle),snapshot_sequence=int(value.snapshot_sequence),observed_monotonic_ns=int(value.observed_monotonic_ns),progress_sequence=int(value.progress_sequence),progress_monotonic_ns=int(value.progress_monotonic_ns),acknowledged_probe_sequence=int(value.acknowledged_probe_sequence),acknowledged_probe_monotonic_ns=int(value.acknowledged_probe_monotonic_ns),route_generation=int(value.route_generation),metadata_generation=int(value.metadata_generation),binding_change_sequence=int(value.binding_change_sequence),ready=bool(value.ready),admission_open=bool(value.admission_open),activity=Activity(active_requests=int(value.activity.active_requests),pending_requests=int(value.activity.pending_requests),active_connections=int(value.activity.active_connections),websocket_sessions=int(value.activity.websocket_sessions),outbound_pending=int(value.activity.outbound_pending)))
|
|
480
|
+
def health(self)->Health:
|
|
481
|
+
'Copy the current pointer-free health snapshot.';value=native.Health();_init(value,'coakka_http_host_health_init')
|
|
482
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_service_health(service,ctypes.byref(value)))
|
|
483
|
+
return self._copy_health(value)
|
|
484
|
+
def probe_liveness(self,timeout_ms:int=1000)->Health:
|
|
485
|
+
'Wait for fresh native progress evidence within a finite bound.';value=native.Health();_init(value,'coakka_http_host_health_init')
|
|
486
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_probe_liveness(service,_timeout(timeout_ms),ctypes.byref(value)))
|
|
487
|
+
return self._copy_health(value)
|
|
488
|
+
def route_snapshot(self)->RouteSnapshot:
|
|
489
|
+
'Copy one coherent structural generation and binding-table cut.';capacity=self.effective_limits().max_routes
|
|
490
|
+
if not 0<=capacity<=_MAX_ITEMS:raise RuntimeError('route snapshot exceeds the Python bound')
|
|
491
|
+
identity=native.RouteIdentity();_init(identity,'coakka_http_host_route_identity_init');values=(native.RouteState*capacity)()
|
|
492
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_route_snapshot(service,values if capacity else None,capacity,ctypes.byref(identity)))
|
|
493
|
+
if identity.route_count>capacity:raise RuntimeError('route snapshot returned an invalid extent')
|
|
494
|
+
return RouteSnapshot(identity=RouteIdentity(route_count=int(identity.route_count),route_generation=int(identity.route_generation),metadata_generation=int(identity.metadata_generation),binding_change_sequence=int(identity.binding_change_sequence)),routes=tuple(RouteState(route_id=int(item.route_id),handler_binding_id=int(item.handler_binding_id),binding_revision=int(item.binding_revision))for item in islice(values,identity.route_count)))
|
|
495
|
+
@staticmethod
|
|
496
|
+
def _copy_retirement(value:native.RequestRetirement)->RequestRetirement:return RequestRetirement(state=RequestRetirementState(value.state),code=RequestRetirementCode(value.code),quiescent_sources=int(value.quiescent_sources),sources=int(value.sources))
|
|
497
|
+
def rebind(self,request:RouteRebind)->RouteRebindOutcome:
|
|
498
|
+
'Apply a generation-checked handler-binding change.';value=native.RebindRequest();outcome=native.RebindOutcome();_init(value,'coakka_http_host_rebind_request_init');_init(outcome,'coakka_http_host_rebind_outcome_init');value.activation_id=_unsigned(request.activation_id,64,'activation_id');value.expected_route_generation=_unsigned(request.expected_route_generation,64,'expected_route_generation');value.route_id=_unsigned(request.route_id,64,'route_id');value.expected_binding_revision=_unsigned(request.expected_binding_revision,64,'expected_binding_revision');value.new_handler_binding_id=_unsigned(request.new_handler_binding_id,64,'new_handler_binding_id')
|
|
499
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_rebind(service,ctypes.byref(value),ctypes.byref(outcome)))
|
|
500
|
+
return RouteRebindOutcome(code=ControlCode(outcome.code),activation_id=int(outcome.activation_id),operation_digest=int(outcome.operation_digest),route_generation=int(outcome.route_generation),binding_change_sequence=int(outcome.binding_change_sequence),route_id=int(outcome.route_id),previous_handler_binding_id=int(outcome.previous_handler_binding_id),previous_binding_revision=int(outcome.previous_binding_revision),effective_handler_binding_id=int(outcome.effective_handler_binding_id),effective_binding_revision=int(outcome.effective_binding_revision),changed=bool(outcome.changed),replayed=bool(outcome.replayed),request_retirement=self._copy_retirement(outcome.request_retirement))
|
|
501
|
+
def publish_routes(self,publication:RoutePublication)->RoutePublicationOutcome:
|
|
502
|
+
'Atomically publish one complete structural route generation.';arena=native.Arena();metadata_budget=_MetadataBudget();routes=[_native_route(item,arena,metadata_budget)for item in publication.routes];route_array=arena.array(native.Route,routes);value=native.PublicationRequest();outcome=native.PublicationOutcome();_init(value,'coakka_http_host_publication_request_init');_init(outcome,'coakka_http_host_publication_outcome_init');value.route_count=len(routes);value.activation_id=_unsigned(publication.activation_id,64,'activation_id');value.expected_route_generation=_unsigned(publication.expected_route_generation,64,'expected_route_generation');value.expected_metadata_generation=_unsigned(publication.expected_metadata_generation,64,'expected_metadata_generation');value.expected_binding_change_sequence=_unsigned(publication.expected_binding_change_sequence,64,'expected_binding_change_sequence');value.routes=ctypes.cast(route_array,ctypes.POINTER(native.Route))if route_array else None
|
|
503
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_publish_routes(service,ctypes.byref(value),ctypes.byref(outcome)))
|
|
504
|
+
return RoutePublicationOutcome(code=ControlCode(outcome.code),activation_id=int(outcome.activation_id),operation_digest=int(outcome.operation_digest),previous_route_generation=int(outcome.previous_route_generation),previous_metadata_generation=int(outcome.previous_metadata_generation),previous_binding_change_sequence=int(outcome.previous_binding_change_sequence),effective_route_generation=int(outcome.effective_route_generation),effective_metadata_generation=int(outcome.effective_metadata_generation),effective_binding_change_sequence=int(outcome.effective_binding_change_sequence),actual=int(outcome.actual),limit=int(outcome.limit),route_count=int(outcome.route_count),rejected_route_index=int(outcome.rejected_route_index),changed=bool(outcome.changed),replayed=bool(outcome.replayed),request_retirement=self._copy_retirement(outcome.request_retirement))
|
|
505
|
+
@staticmethod
|
|
506
|
+
def _copy_monitor_policy(value:native.MonitorPolicy)->MonitorPolicy:return MonitorPolicy(collection=value.collection,notification=value.notification,latency=value.latency,active_event_capacity=int(value.active_event_capacity),aggregate_categories=MonitorCategory(value.aggregate_categories),event_categories=MonitorCategory(value.event_categories))
|
|
507
|
+
@classmethod
|
|
508
|
+
def _copy_monitor_config(cls,value:native.MonitorConfig)->MonitorConfig:return MonitorConfig(generation=int(value.generation),collection_epoch=int(value.collection_epoch),change_sequence=int(value.change_sequence),epoch_started_monotonic_ns=int(value.epoch_started_monotonic_ns),policy_applied_monotonic_ns=int(value.policy_applied_monotonic_ns),supported_categories=MonitorCategory(value.supported_categories),reserved_event_capacity=int(value.reserved_event_capacity),max_events_per_read=int(value.max_events_per_read),signal_reserved=bool(value.signal_reserved),fixed_latency_buckets=bool(value.fixed_latency_buckets),policy=cls._copy_monitor_policy(value.policy))
|
|
509
|
+
@staticmethod
|
|
510
|
+
def _copy_tls_credentials(value:native.TlsCredentials)->TlsCredentials:
|
|
511
|
+
security:TransportSecurity|int=int(value.security)
|
|
512
|
+
try:security=TransportSecurity(security)
|
|
513
|
+
except ValueError:pass
|
|
514
|
+
return TlsCredentials(security=security,reload_mode=TlsReloadMode(value.reload_mode),retired_generations=int(value.retired_generations),maximum_retired_generations=int(value.maximum_retired_generations),closed=bool(value.closed),preparing=bool(value.preparing),generation=int(value.generation),drain_before_generation=int(value.drain_before_generation),credential_id=bytes(value.credential_id).decode('ascii'))
|
|
515
|
+
def tls_credentials(self)->TlsCredentials:
|
|
516
|
+
"Read the secure listener's effective credential generation.";native.LIBRARY.bind_tls_control();value=native.TlsCredentials();_init(value,'coakka_http_host_tls_credentials_init')
|
|
517
|
+
with self._lifecycle,self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_tls_credentials(service,ctypes.byref(value)))
|
|
518
|
+
return self._copy_tls_credentials(value)
|
|
519
|
+
def tls_reload(self,request:TlsReloadRequest)->TlsReloadOutcome:
|
|
520
|
+
"Prepare credentials and return Core's typed publication outcome."
|
|
521
|
+
if not isinstance(request,TlsReloadRequest):raise TypeError('request must be a TlsReloadRequest')
|
|
522
|
+
native.LIBRARY.bind_tls_control();value=native.TlsReloadRequest();outcome=native.TlsReloadOutcome();_init(value,'coakka_http_host_tls_reload_request_init');_init(outcome,'coakka_http_host_tls_reload_outcome_init');arena=native.Arena();value.mode=_unsigned(request.mode,32,'mode');value.security=_unsigned(request.security,32,'security');value.expected_generation=_unsigned(request.expected_generation,64,'expected_generation');value.credential_generation=_unsigned(request.credential_generation,64,'credential_generation');value.credential_id=arena.bytes(request.credential_id);value.certificate_chain_file=arena.bytes(request.certificate_chain_file);value.private_key_file=arena.bytes(request.private_key_file);value.trust_roots_file=arena.bytes(request.trust_roots_file)
|
|
523
|
+
with self._lifecycle,self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_tls_reload(service,ctypes.byref(value),ctypes.byref(outcome)))
|
|
524
|
+
return TlsReloadOutcome(reason=TlsReloadReason(outcome.reason),changed=bool(outcome.changed),effective=self._copy_tls_credentials(outcome.effective))
|
|
525
|
+
def monitor_config(self)->MonitorConfig:
|
|
526
|
+
'Copy the complete effective monitor configuration.';value=native.MonitorConfig();_init(value,'coakka_http_host_monitor_config_init')
|
|
527
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_monitor_config(service,ctypes.byref(value)))
|
|
528
|
+
return self._copy_monitor_config(value)
|
|
529
|
+
def monitor_apply(self,expected_generation:int,policy:MonitorPolicy)->MonitorApplyOutcome:
|
|
530
|
+
'Apply a live policy and return accepted or rejected effective truth.';value=native.MonitorPolicy();outcome=native.MonitorApplyOutcome();_init(value,'coakka_http_host_monitor_policy_init');_init(outcome,'coakka_http_host_monitor_apply_outcome_init');value.collection=int(policy.collection);value.notification=int(policy.notification);value.latency=int(policy.latency);value.active_event_capacity=_unsigned(policy.active_event_capacity,32,'active_event_capacity');value.aggregate_categories=int(policy.aggregate_categories);value.event_categories=int(policy.event_categories)
|
|
531
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_monitor_apply(service,_unsigned(expected_generation,64,'expected_generation'),ctypes.byref(value),ctypes.byref(outcome)))
|
|
532
|
+
return MonitorApplyOutcome(reason=MonitorApplyReason(outcome.reason),actual=int(outcome.actual),limit=int(outcome.limit),changed=bool(outcome.changed),effective=self._copy_monitor_config(outcome.effective))
|
|
533
|
+
def monitor_snapshot(self)->MonitorSnapshot:
|
|
534
|
+
'Copy one bounded aggregate, histogram, and health snapshot.';value=native.MonitorSnapshot();_init(value,'coakka_http_host_monitor_snapshot_init')
|
|
535
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_monitor_snapshot(service,ctypes.byref(value)))
|
|
536
|
+
failure_count=int(value.failure_count);latency_count=int(value.latency_bucket_count)
|
|
537
|
+
if failure_count>native.MAX_MONITOR_FAILURES or latency_count>native.FIXED_LATENCY_BUCKETS:raise RuntimeError('native monitor snapshot exceeds its ABI bound')
|
|
538
|
+
return MonitorSnapshot(collecting=bool(value.collecting),stale=bool(value.stale),partial=bool(value.partial),snapshot_sequence=int(value.snapshot_sequence),observed_monotonic_ns=int(value.observed_monotonic_ns),collected_change_sequence=int(value.collected_change_sequence),admitted_requests=int(value.admitted_requests),request_bytes=int(value.request_bytes),exchange_completed=int(value.exchange_completed),exchange_failed=int(value.exchange_failed),exchange_timed_out=int(value.exchange_timed_out),exchange_disconnected=int(value.exchange_disconnected),exchange_cancelled=int(value.exchange_cancelled),response_status_family=(int(value.response_status_family[0]),int(value.response_status_family[1]),int(value.response_status_family[2]),int(value.response_status_family[3]),int(value.response_status_family[4])),retained_events=int(value.retained_events),overwritten_events=int(value.overwritten_events),dropped_events=int(value.dropped_events),signal_notifications=int(value.signal_notifications),coalesced_signals=int(value.coalesced_signals),signal_failures=int(value.signal_failures),config=self._copy_monitor_config(value.config),health=self._copy_health(value.health),failure_counts=tuple(MonitorFailureCount(int(value.failure_counts[i].reason),int(value.failure_counts[i].count))for i in range(failure_count)),latency_buckets=tuple(MonitorLatencyBucket(int(value.latency_buckets[i].upper_bound_ns),int(value.latency_buckets[i].count))for i in range(latency_count)))
|
|
539
|
+
def monitor_read(self,after_sequence:int,maximum_events:int)->MonitorEventPage:
|
|
540
|
+
'Copy a bounded page of operational events after one sequence.';maximum_events=_unsigned(maximum_events,32,'maximum_events')
|
|
541
|
+
if not 0<maximum_events<=native.FIXED_LATENCY_BUCKETS*4:raise ValueError('maximum_events is outside the native read bound')
|
|
542
|
+
events=(native.MonitorEvent*maximum_events)()
|
|
543
|
+
for item in events:_init(item,'coakka_http_host_monitor_event_init')
|
|
544
|
+
page=native.MonitorPage();_init(page,'coakka_http_host_monitor_page_init')
|
|
545
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_monitor_read(service,_unsigned(after_sequence,64,'after_sequence'),maximum_events,events,maximum_events,ctypes.byref(page)))
|
|
546
|
+
if page.count>maximum_events:raise RuntimeError('native monitor page exceeds its supplied capacity')
|
|
547
|
+
copied=tuple(MonitorEvent(kind=MonitorEventKind(item.kind),sequence=int(item.sequence),change_sequence=int(item.change_sequence),config_generation=int(item.config_generation),collection_epoch=int(item.collection_epoch),observed_monotonic_ns=int(item.observed_monotonic_ns),category=MonitorCategory(item.category),failure_reason=int(item.failure_reason),queue_scope=int(item.queue_scope),queue_depth=int(item.queue_depth),queue_capacity=int(item.queue_capacity),exchange=ExchangeId(int(item.exchange.slot),int(item.exchange.generation)))for item in events[:page.count]);return MonitorEventPage(events=copied,oldest_available_sequence=int(page.oldest_sequence),latest_sequence=int(page.latest_sequence),overwritten_events=int(page.overwritten_events),remaining_events=int(page.remaining_events))
|
|
548
|
+
def monitor_wait(self,timeout_ms:int)->bool:
|
|
549
|
+
'Wait for a coalesced monitor signal; return false on timeout.'
|
|
550
|
+
with self._monitor_waiter,self._borrow()as service:status=native.LIBRARY.library.coakka_http_host_monitor_wait(service,_timeout(timeout_ms))
|
|
551
|
+
if status==native.STATUS_TIMEOUT:return False
|
|
552
|
+
native.LIBRARY.check(status);return True
|
|
553
|
+
def monitor_interrupt(self)->None:
|
|
554
|
+
'Wake the sole monitor waiter without changing HTTP work.'
|
|
555
|
+
with self._borrow()as service:native.LIBRARY.check(native.LIBRARY.library.coakka_http_host_monitor_interrupt(service))
|