fastapi-streams 0.0.1__py3-none-any.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.
@@ -0,0 +1,26 @@
1
+ // Generated by fastapi-streams. Do not edit.
2
+
3
+ export class TransportError extends Error {
4
+ override readonly name: string = "TransportError";
5
+ }
6
+
7
+ export class HttpError<T = unknown> extends TransportError {
8
+ override readonly name: string = "HttpError";
9
+
10
+ constructor(
11
+ readonly status: number,
12
+ readonly details: T,
13
+ readonly response: Response,
14
+ options?: ErrorOptions,
15
+ ) {
16
+ super(`HTTP ${status} ${response.statusText}`.trim(), options);
17
+ }
18
+ }
19
+
20
+ export class StreamParseError extends TransportError {
21
+ override readonly name: string = "StreamParseError";
22
+
23
+ constructor(readonly data: string, cause: unknown) {
24
+ super("Invalid JSON in SSE data", { cause });
25
+ }
26
+ }
@@ -0,0 +1,52 @@
1
+ # Generated by fastapi-streams. Do not edit.
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import AsyncGenerator
5
+
6
+ from pydantic import TypeAdapter
7
+
8
+ from .. import models as Models
9
+ from ..runtime import {{ runtime_imports | join(', ') }}
10
+
11
+
12
+ class {{ class_name }}:
13
+ def __init__(self, transport: Transport) -> None:
14
+ self._transport = transport
15
+ {% for operation in operations %}
16
+
17
+ def {{ operation.operation_id | snake }}(
18
+ self,
19
+ *,
20
+ {% for parameter in operation.parameters %}
21
+ {{ parameter.name | snake }}: {{ parameter.type | py_type }}{{ ' | None = None' if not parameter.required else '' }},
22
+ {% endfor %}
23
+ {% if operation.body %}
24
+ body: {{ operation.body.type | py_type }}{{ ' | None = None' if not operation.body.required else '' }},
25
+ {% endif %}
26
+ options: RequestOptions | None = None,
27
+ ) -> AsyncGenerator[{{ operation.event | py_type }}, None]:
28
+ query: list[tuple[str, str]] = []
29
+ headers: dict[str, str] = {}
30
+ {% for parameter in operation.parameters %}
31
+ {% if parameter.location == 'query' %}
32
+ append_query(query, {{ parameter.name | repr }}, {{ parameter.name | snake }}, explode={{ parameter.explode }}{{ ', required=True' if parameter.required else '' }})
33
+ {% elif parameter.location == 'header' %}
34
+ set_header(headers, {{ parameter.name | repr }}, {{ parameter.name | snake }}{{ ', required=True' if parameter.required else '' }})
35
+ {% endif %}
36
+ {% endfor %}
37
+ {% if operation.body %}
38
+ adapter = TypeAdapter({{ operation.body.type | py_type }})
39
+ content = adapter.dump_json(adapter.validate_python(body), by_alias=True, exclude_unset=True){{ ' if body is not None else None' if not operation.body.required else '' }}
40
+ {% endif %}
41
+ return self._transport.stream(
42
+ {{ operation | py_path }},
43
+ {{ operation.method | repr }},
44
+ TypeAdapter({{ operation.event | py_type }}),
45
+ query=query,
46
+ headers=headers,
47
+ {% if operation.body %}
48
+ body=content,
49
+ {% endif %}
50
+ options=options,
51
+ )
52
+ {% endfor %}
@@ -0,0 +1,40 @@
1
+ # Generated by fastapi-streams. Do not edit.
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from types import TracebackType
6
+
7
+ {{ http_import }}
8
+
9
+ from .runtime import Transport
10
+ {% for group in groups %}
11
+ from .{{ group.directory }}.api import {{ group.class_name }}
12
+ {% endfor %}
13
+
14
+
15
+ class Client:
16
+ def __init__(
17
+ self,
18
+ base_url: str,
19
+ *,
20
+ headers: Mapping[str, str] | None = None,
21
+ http_client: httpx.AsyncClient | None = None,
22
+ ) -> None:
23
+ self._transport = Transport(base_url, headers=headers, http_client=http_client)
24
+ {% for group in groups %}
25
+ self.{{ group.name }} = {{ group.class_name }}(self._transport)
26
+ {% endfor %}
27
+
28
+ async def aclose(self) -> None:
29
+ await self._transport.aclose()
30
+
31
+ async def __aenter__(self) -> Client:
32
+ return self
33
+
34
+ async def __aexit__(
35
+ self,
36
+ exc_type: type[BaseException] | None,
37
+ exc: BaseException | None,
38
+ traceback: TracebackType | None,
39
+ ) -> None:
40
+ await self.aclose()
@@ -0,0 +1,217 @@
1
+ # Generated by fastapi-streams. Do not edit.
2
+ from __future__ import annotations
3
+
4
+ import codecs
5
+ from collections.abc import AsyncGenerator, Mapping, Sequence
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+
9
+ import httpx
10
+ from pydantic import JsonValue, TypeAdapter, ValidationError
11
+
12
+
13
+ class TransportError(Exception):
14
+ pass
15
+
16
+
17
+ class HttpError(TransportError):
18
+ def __init__(self, response: httpx.Response, details: JsonValue) -> None:
19
+ self.status = response.status_code
20
+ self.response = response
21
+ self.details = details
22
+ super().__init__(f"HTTP {self.status} {response.reason_phrase}")
23
+
24
+
25
+ class StreamParseError(TransportError):
26
+ def __init__(self, data: str) -> None:
27
+ self.data = data
28
+ super().__init__("Invalid JSON or event schema in SSE data")
29
+
30
+
31
+ class _Default(Enum):
32
+ TIMEOUT = "inherit"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class RequestOptions:
37
+ headers: Mapping[str, str] | None = None
38
+ timeout: float | None | _Default = _Default.TIMEOUT
39
+
40
+
41
+ type ParameterValue = str | int | float | bool | None
42
+
43
+
44
+ def encode_path(value: str | int | float | bool | None) -> str:
45
+ from urllib.parse import quote
46
+
47
+ text = scalar(value)
48
+ if text in {".", ".."}:
49
+ raise ValueError("Path parameters cannot be dot segments")
50
+ return quote(text, safe="")
51
+
52
+
53
+ def scalar(value: str | int | float | bool | None) -> str:
54
+ if value is None:
55
+ return "null"
56
+ if isinstance(value, bool):
57
+ return "true" if value else "false"
58
+ return str(value)
59
+
60
+
61
+ def append_query(
62
+ query: list[tuple[str, str]],
63
+ name: str,
64
+ value: ParameterValue | Sequence[ParameterValue],
65
+ *,
66
+ explode: bool,
67
+ required: bool = False,
68
+ ) -> None:
69
+ if value is None:
70
+ if required:
71
+ raise TypeError(f"Required query parameter {name} cannot be None")
72
+ return
73
+ if not isinstance(value, str | int | float | bool):
74
+ if explode:
75
+ query.extend((name, scalar(item)) for item in value)
76
+ else:
77
+ query.append((name, ",".join(scalar(item) for item in value)))
78
+ else:
79
+ query.append((name, scalar(value)))
80
+
81
+
82
+ def set_header(
83
+ headers: dict[str, str], name: str, value: ParameterValue, *, required: bool = False
84
+ ) -> None:
85
+ if value is None:
86
+ if required:
87
+ raise TypeError(f"Required header parameter {name} cannot be None")
88
+ return
89
+ headers[name] = scalar(value)
90
+
91
+
92
+ async def iter_sse(response: httpx.Response) -> AsyncGenerator[str, None]:
93
+ decoder = codecs.getincrementaldecoder("utf-8-sig")("strict")
94
+ line = ""
95
+ data: list[str] = []
96
+ skip_lf = False
97
+
98
+ def accept_line() -> str | None:
99
+ nonlocal line, data
100
+ current, line = line, ""
101
+ if not current:
102
+ if not data:
103
+ return None
104
+ payload, data = "\n".join(data), []
105
+ return payload
106
+ field, _, value = current.partition(":")
107
+ if value.startswith(" "):
108
+ value = value[1:]
109
+ if field == "data":
110
+ data.append(value)
111
+ return None
112
+
113
+ async for chunk in response.aiter_bytes():
114
+ for char in decoder.decode(chunk):
115
+ if skip_lf:
116
+ skip_lf = False
117
+ if char == "\n":
118
+ continue
119
+ if char in "\r\n":
120
+ skip_lf = char == "\r"
121
+ payload = accept_line()
122
+ if payload is not None:
123
+ yield payload
124
+ else:
125
+ line += char
126
+ line += decoder.decode(b"", final=True)
127
+ if line:
128
+ accept_line()
129
+ if data:
130
+ yield "\n".join(data)
131
+
132
+
133
+ class Transport:
134
+ def __init__(
135
+ self,
136
+ base_url: str,
137
+ *,
138
+ headers: Mapping[str, str] | None = None,
139
+ http_client: httpx.AsyncClient | None = None,
140
+ ) -> None:
141
+ self.base_url = base_url.rstrip("/")
142
+ self.headers = httpx.Headers(headers)
143
+ self.http_client = http_client or httpx.AsyncClient(timeout=None)
144
+ self.owns_client = http_client is None
145
+
146
+ async def aclose(self) -> None:
147
+ if self.owns_client:
148
+ await self.http_client.aclose()
149
+
150
+ async def stream[T](
151
+ self,
152
+ path: str,
153
+ method: str,
154
+ adapter: TypeAdapter[T],
155
+ *,
156
+ query: list[tuple[str, str]],
157
+ headers: Mapping[str, str],
158
+ body: bytes | None = None,
159
+ options: RequestOptions | None = None,
160
+ ) -> AsyncGenerator[T, None]:
161
+ request_headers = self.headers.copy()
162
+ request_headers.update(headers)
163
+ if options and options.headers:
164
+ request_headers.update(options.headers)
165
+ request_headers["Accept"] = "text/event-stream"
166
+ if body is not None:
167
+ request_headers["Content-Type"] = "application/json"
168
+ try:
169
+ async with self.http_client.stream(
170
+ method,
171
+ self.base_url + path,
172
+ params=tuple(query),
173
+ headers=request_headers,
174
+ content=body,
175
+ timeout=options.timeout
176
+ if options and not isinstance(options.timeout, _Default)
177
+ else httpx.USE_CLIENT_DEFAULT,
178
+ ) as response:
179
+ if not response.is_success:
180
+ raw = bytearray()
181
+ failure: httpx.HTTPError | None = None
182
+ try:
183
+ async for chunk in response.aiter_bytes():
184
+ raw.extend(chunk[: 65536 - len(raw)])
185
+ if len(raw) >= 65536:
186
+ break
187
+ except httpx.HTTPError as exc:
188
+ failure = exc
189
+ text = raw.decode("utf-8", errors="replace")
190
+ try:
191
+ details = (
192
+ TypeAdapter[JsonValue](JsonValue).validate_json(text)
193
+ if text
194
+ else None
195
+ )
196
+ except ValidationError:
197
+ details = text
198
+ raise HttpError(response, details) from failure
199
+ media = (
200
+ response.headers.get("content-type", "")
201
+ .split(";", 1)[0]
202
+ .strip()
203
+ .lower()
204
+ )
205
+ if media != "text/event-stream":
206
+ raise TransportError(
207
+ f"Expected text/event-stream, received {media!r}"
208
+ )
209
+ async for payload in iter_sse(response):
210
+ try:
211
+ yield adapter.validate_json(payload)
212
+ except ValidationError as exc:
213
+ raise StreamParseError(payload) from exc
214
+ except httpx.HTTPError as exc:
215
+ raise TransportError("SSE HTTP transport failed") from exc
216
+ except UnicodeDecodeError as exc:
217
+ raise TransportError("Invalid UTF-8 in SSE stream") from exc
@@ -0,0 +1,264 @@
1
+ // Generated by fastapi-streams. Do not edit.
2
+ import { HttpError, StreamParseError, TransportError } from "./errors.js";
3
+
4
+ export interface ClientOptions {
5
+ baseUrl: string;
6
+ headers?: HeadersInit;
7
+ fetch?: typeof globalThis.fetch;
8
+ credentials?: RequestCredentials;
9
+ }
10
+
11
+ export interface RequestOptions {
12
+ headers?: HeadersInit;
13
+ signal?: AbortSignal;
14
+ }
15
+
16
+ export interface StreamRequest {
17
+ path: string;
18
+ method: string;
19
+ query?: URLSearchParams;
20
+ headers?: HeadersInit;
21
+ body?: string;
22
+ }
23
+
24
+ export function encodePath(value: string | number | boolean | null): string {
25
+ const text = String(value);
26
+ if (text === "." || text === "..") {
27
+ throw new TypeError("Path parameters cannot be dot segments");
28
+ }
29
+ return encodeURIComponent(text);
30
+ }
31
+
32
+ export function appendQuery(
33
+ query: URLSearchParams,
34
+ name: string,
35
+ value: string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined,
36
+ explode: boolean,
37
+ required = false,
38
+ ): void {
39
+ if (value === undefined || value === null) {
40
+ if (required) throw new TypeError(`Required query parameter ${name} cannot be null or undefined`);
41
+ return;
42
+ }
43
+ if (Array.isArray(value)) {
44
+ if (explode) {
45
+ for (const item of value) query.append(name, String(item));
46
+ } else {
47
+ query.append(name, value.map(String).join(","));
48
+ }
49
+ } else {
50
+ query.append(name, String(value));
51
+ }
52
+ }
53
+
54
+ export function setHeader(
55
+ headers: Headers,
56
+ name: string,
57
+ value: string | number | boolean | null | undefined,
58
+ required = false,
59
+ ): void {
60
+ if (value === undefined || value === null) {
61
+ if (required) throw new TypeError(`Required header parameter ${name} cannot be null or undefined`);
62
+ return;
63
+ }
64
+ headers.set(name, String(value));
65
+ }
66
+
67
+ async function readChunk(
68
+ reader: ReadableStreamDefaultReader<Uint8Array>,
69
+ signal?: AbortSignal,
70
+ ): Promise<ReadableStreamReadResult<Uint8Array>> {
71
+ signal?.throwIfAborted();
72
+ if (!signal) return reader.read();
73
+ let onAbort: () => void = () => {};
74
+ const aborted = new Promise<never>((_resolve, reject) => {
75
+ onAbort = () => reject(signal.reason);
76
+ signal.addEventListener("abort", onAbort, { once: true });
77
+ });
78
+ try {
79
+ return await Promise.race([reader.read(), aborted]);
80
+ } finally {
81
+ signal.removeEventListener("abort", onAbort);
82
+ }
83
+ }
84
+
85
+ export async function* parseSSE<T>(
86
+ body: ReadableStream<Uint8Array>,
87
+ signal?: AbortSignal,
88
+ ): AsyncGenerator<T, void, unknown> {
89
+ const reader = body.getReader();
90
+ const decoder = new TextDecoder("utf-8", { fatal: true });
91
+ let line = "";
92
+ let data: string[] = [];
93
+ let skipLF = false;
94
+ let ended = false;
95
+
96
+ function acceptLine(): string | undefined {
97
+ const current = line;
98
+ line = "";
99
+ if (current === "") {
100
+ if (data.length === 0) return undefined;
101
+ const payload = data.join("\n");
102
+ data = [];
103
+ return payload;
104
+ }
105
+ const colon = current.indexOf(":");
106
+ const field = colon < 0 ? current : current.slice(0, colon);
107
+ let value = colon < 0 ? "" : current.slice(colon + 1);
108
+ if (value.startsWith(" ")) value = value.slice(1);
109
+ if (field === "data") data.push(value);
110
+ return undefined;
111
+ }
112
+
113
+ function decode(payload: string): T {
114
+ try {
115
+ return JSON.parse(payload) as T;
116
+ } catch (cause) {
117
+ throw new StreamParseError(payload, cause);
118
+ }
119
+ }
120
+
121
+ try {
122
+ while (true) {
123
+ const chunk = await readChunk(reader, signal);
124
+ const text = chunk.done ? decoder.decode() : decoder.decode(chunk.value, { stream: true });
125
+ for (const char of text) {
126
+ if (skipLF) {
127
+ skipLF = false;
128
+ if (char === "\n") continue;
129
+ }
130
+ if (char === "\r" || char === "\n") {
131
+ skipLF = char === "\r";
132
+ const payload = acceptLine();
133
+ if (payload !== undefined) yield decode(payload);
134
+ } else {
135
+ line += char;
136
+ }
137
+ }
138
+ if (chunk.done) {
139
+ ended = true;
140
+ if (line !== "") acceptLine();
141
+ if (data.length > 0) yield decode(data.join("\n"));
142
+ return;
143
+ }
144
+ }
145
+ } finally {
146
+ try {
147
+ if (!ended) await reader.cancel();
148
+ } finally {
149
+ reader.releaseLock();
150
+ }
151
+ }
152
+ }
153
+
154
+ function mergeHeaders(...sources: (HeadersInit | undefined)[]): Headers {
155
+ const result = new Headers();
156
+ for (const source of sources) {
157
+ new Headers(source).forEach((value, name) => result.set(name, value));
158
+ }
159
+ return result;
160
+ }
161
+
162
+ async function errorDetails(response: Response, signal: AbortSignal): Promise<unknown> {
163
+ if (!response.body) return undefined;
164
+ const reader = response.body.getReader();
165
+ const decoder = new TextDecoder();
166
+ let text = "";
167
+ let ended = false;
168
+ try {
169
+ // Error bodies can be HTML proxy pages; keep diagnostics bounded to 64 KiB.
170
+ while (text.length < 65536) {
171
+ const chunk = await readChunk(reader, signal);
172
+ if (chunk.done) {
173
+ ended = true;
174
+ text += decoder.decode();
175
+ break;
176
+ }
177
+ text += decoder.decode(chunk.value, { stream: true });
178
+ }
179
+ } finally {
180
+ try {
181
+ if (!ended) await reader.cancel();
182
+ } finally {
183
+ reader.releaseLock();
184
+ }
185
+ }
186
+ text = text.slice(0, 65536);
187
+ if (!text) return undefined;
188
+ try {
189
+ return JSON.parse(text) as unknown;
190
+ } catch {
191
+ return text;
192
+ }
193
+ }
194
+
195
+ export async function* stream<T>(
196
+ client: ClientOptions,
197
+ request: StreamRequest,
198
+ options: RequestOptions = {},
199
+ ): AsyncGenerator<T, void, unknown> {
200
+ const controller = new AbortController();
201
+ const onAbort = () => controller.abort(options.signal?.reason);
202
+ options.signal?.addEventListener("abort", onAbort, { once: true });
203
+ if (options.signal?.aborted) onAbort();
204
+ let response: Response | undefined;
205
+ let bodyHandled = false;
206
+ try {
207
+ controller.signal.throwIfAborted();
208
+ const base = client.baseUrl.replace(/\/+$/, "");
209
+ const query = request.query?.toString();
210
+ const url = `${base}${request.path}${query ? `?${query}` : ""}`;
211
+ const headers = mergeHeaders(client.headers, request.headers, options.headers);
212
+ headers.set("Accept", "text/event-stream");
213
+ if (request.body !== undefined) headers.set("Content-Type", "application/json");
214
+ try {
215
+ response = await (client.fetch ?? globalThis.fetch)(url, {
216
+ method: request.method,
217
+ headers,
218
+ signal: controller.signal,
219
+ ...(request.body === undefined ? {} : { body: request.body }),
220
+ ...(client.credentials === undefined ? {} : { credentials: client.credentials }),
221
+ });
222
+ } catch (cause) {
223
+ controller.signal.throwIfAborted();
224
+ throw new TransportError("Failed to fetch SSE stream", { cause });
225
+ }
226
+ if (!response.ok) {
227
+ bodyHandled = true;
228
+ let details: unknown;
229
+ try {
230
+ details = await errorDetails(response, controller.signal);
231
+ } catch (cause) {
232
+ controller.signal.throwIfAborted();
233
+ throw new HttpError(response.status, undefined, response, { cause });
234
+ }
235
+ throw new HttpError(response.status, details, response);
236
+ }
237
+ const media = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
238
+ if (media !== "text/event-stream") {
239
+ throw new TransportError(`Expected text/event-stream, received ${media ?? "no Content-Type"}`);
240
+ }
241
+ if (!response.body) throw new TransportError("SSE response has no body");
242
+ bodyHandled = true;
243
+ try {
244
+ yield* parseSSE<T>(response.body, controller.signal);
245
+ } catch (cause) {
246
+ controller.signal.throwIfAborted();
247
+ if (cause instanceof TransportError) throw cause;
248
+ throw new TransportError("SSE stream read failed", { cause });
249
+ }
250
+ } finally {
251
+ options.signal?.removeEventListener("abort", onAbort);
252
+ try {
253
+ if (!bodyHandled && response?.body && !response.body.locked) {
254
+ try {
255
+ await response.body.cancel();
256
+ } catch (cause) {
257
+ throw new TransportError("Failed to close SSE response", { cause });
258
+ }
259
+ }
260
+ } finally {
261
+ controller.abort();
262
+ }
263
+ }
264
+ }
@@ -0,0 +1,52 @@
1
+ // Generated by fastapi-streams. Do not edit.
2
+ import type * as Types from "../types.js";
3
+ import type { ClientOptions, RequestOptions } from "../transport/sse.js";
4
+ import { {{ functions | join(', ') }} } from "../transport/sse.js";
5
+
6
+ {% for operation in operations %}
7
+ {% if operation.parameters or operation.body %}
8
+ export interface {{ operation | input_name }} {
9
+ {% for param in operation.parameters %}
10
+ {{ param.argument }}{{ '' if param.required else '?' }}: {{ param.type | ts_type('Types.') }};
11
+ {% endfor %}
12
+ {% if operation.body %}
13
+ body{{ '' if operation.body.required else '?' }}: {{ operation.body.type | ts_type('Types.') }};
14
+ {% endif %}
15
+ }
16
+ {% endif %}
17
+ {% if operation.errors %}
18
+ export type {{ operation.operation_id | pascal }}Error = {{ operation | error_type }};
19
+ {% endif %}
20
+
21
+ {% endfor %}
22
+ export function {{ factory }}(client: ClientOptions) {
23
+ return {
24
+ {% for operation in operations %}
25
+ {{ operation.name }}(
26
+ {% if operation.parameters or operation.body %}
27
+ input: {{ operation | input_name }}{{ '' if operation | required else ' = {}' }},
28
+ {% endif %}
29
+ options: RequestOptions = {},
30
+ ): AsyncIterable<{{ operation.event | ts_type('Types.') }}> {
31
+ const query = new URLSearchParams();
32
+ const headers = new Headers();
33
+ {% for param in operation.parameters %}
34
+ {% if param.location == 'query' %}
35
+ appendQuery(query, {{ param.name | json }}, input.{{ param.argument }}, {{ 'true' if param.explode else 'false' }}{{ ', true' if param.required else '' }});
36
+ {% elif param.location == 'header' %}
37
+ setHeader(headers, {{ param.name | json }}, input.{{ param.argument }}{{ ', true' if param.required else '' }});
38
+ {% endif %}
39
+ {% endfor %}
40
+ return stream<{{ operation.event | ts_type('Types.') }}>(client, {
41
+ path: {{ operation | path }},
42
+ method: {{ operation.method | json }},
43
+ query,
44
+ headers,
45
+ {% if operation.body %}
46
+ ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),
47
+ {% endif %}
48
+ }, options);
49
+ },
50
+ {% endfor %}
51
+ };
52
+ }
@@ -0,0 +1,15 @@
1
+ // Generated by fastapi-streams. Do not edit.
2
+ import type { ClientOptions } from "./transport/sse.js";
3
+ {% for group in groups %}
4
+ import { {{ group.factory }} } from "./{{ group.directory }}/api.js";
5
+ {% endfor %}
6
+
7
+ export function createClient(options: ClientOptions) {
8
+ return {
9
+ {% for group in groups %}
10
+ {{ group.name }}: {{ group.factory }}(options),
11
+ {% endfor %}
12
+ };
13
+ }
14
+
15
+ export type Client = ReturnType<typeof createClient>;
@@ -0,0 +1,19 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, cast
3
+
4
+ from jinja2 import Environment, PackageLoader, StrictUndefined
5
+
6
+
7
+ def environment() -> Environment:
8
+ return Environment(
9
+ loader=PackageLoader("fastapi_streams.codegen", "templates"),
10
+ undefined=StrictUndefined,
11
+ autoescape=False,
12
+ keep_trailing_newline=True,
13
+ trim_blocks=True,
14
+ lstrip_blocks=True,
15
+ )
16
+
17
+
18
+ def add_filters(env: Environment, **filters: Callable[..., Any]) -> None:
19
+ cast(dict[str, Callable[..., Any]], env.filters).update(filters)