swiftpy-framework 0.1.0__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.
- swiftpy/.gitignore +0 -0
- swiftpy/__init__.py +572 -0
- swiftpy/admin/__init__.py +50 -0
- swiftpy/admin/auth.py +522 -0
- swiftpy/admin/registry.py +567 -0
- swiftpy/admin/views.py +1583 -0
- swiftpy/ai/__init__.py +199 -0
- swiftpy/ai/_deterministic.py +40 -0
- swiftpy/ai/_fake.py +604 -0
- swiftpy/ai/agents/__init__.py +30 -0
- swiftpy/ai/agents/base_agent.py +506 -0
- swiftpy/ai/agents/memory.py +864 -0
- swiftpy/ai/client.py +1011 -0
- swiftpy/ai/conversation.py +171 -0
- swiftpy/ai/embeddings.py +321 -0
- swiftpy/ai/exceptions.py +75 -0
- swiftpy/ai/history_strategies.py +157 -0
- swiftpy/ai/middleware.py +918 -0
- swiftpy/ai/observability/__init__.py +28 -0
- swiftpy/ai/observability/drivers.py +763 -0
- swiftpy/ai/observability/observer.py +364 -0
- swiftpy/ai/pool.py +102 -0
- swiftpy/ai/pricing.py +101 -0
- swiftpy/ai/prompts/__init__.py +6 -0
- swiftpy/ai/prompts/loader.py +343 -0
- swiftpy/ai/providers/__init__.py +22 -0
- swiftpy/ai/providers/anthropic_provider.py +353 -0
- swiftpy/ai/providers/base.py +273 -0
- swiftpy/ai/providers/openai_provider.py +275 -0
- swiftpy/ai/rag/__init__.py +53 -0
- swiftpy/ai/rag/context_formatter.py +263 -0
- swiftpy/ai/rag/pipeline_builder.py +544 -0
- swiftpy/ai/rag/reranker.py +484 -0
- swiftpy/ai/rag/result.py +78 -0
- swiftpy/ai/rag/retriever.py +315 -0
- swiftpy/ai/redis_store.py +227 -0
- swiftpy/ai/streaming.py +115 -0
- swiftpy/ai/tools/__init__.py +22 -0
- swiftpy/ai/tools/base_tool.py +489 -0
- swiftpy/ai/tools/first_party.py +384 -0
- swiftpy/ai/tools/tool_loop.py +417 -0
- swiftpy/ai/vectorstore/__init__.py +46 -0
- swiftpy/ai/vectorstore/base.py +542 -0
- swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
- swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
- swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
- swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
- swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
- swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
- swiftpy/auth/__init__.py +50 -0
- swiftpy/auth/api_key_guard.py +544 -0
- swiftpy/auth/auth_manager.py +259 -0
- swiftpy/auth/exceptions.py +71 -0
- swiftpy/auth/jwt_guard.py +230 -0
- swiftpy/auth/jwt_issuer.py +658 -0
- swiftpy/auth/token_blacklist.py +300 -0
- swiftpy/cache/__init__.py +14 -0
- swiftpy/cache/cache.py +251 -0
- swiftpy/cache/driver.py +216 -0
- swiftpy/cache/drivers/memory.py +199 -0
- swiftpy/cache/drivers/redis.py +421 -0
- swiftpy/console/__init__.py +0 -0
- swiftpy/console/cli.py +68 -0
- swiftpy/console/commands/__init__.py +0 -0
- swiftpy/console/commands/cache.py +164 -0
- swiftpy/console/commands/make.py +935 -0
- swiftpy/console/commands/migrate.py +280 -0
- swiftpy/console/commands/new.py +1806 -0
- swiftpy/console/commands/queue.py +306 -0
- swiftpy/console/commands/seed.py +252 -0
- swiftpy/console/commands/start.py +171 -0
- swiftpy/console/commands/storage.py +104 -0
- swiftpy/console/loader.py +21 -0
- swiftpy/console/stubs/__init__.py +26 -0
- swiftpy/console/stubs/controller.stub +14 -0
- swiftpy/console/stubs/event.stub +14 -0
- swiftpy/console/stubs/job.stub +22 -0
- swiftpy/console/stubs/listener.stub +14 -0
- swiftpy/console/stubs/mail.stub +20 -0
- swiftpy/console/stubs/middleware.stub +16 -0
- swiftpy/console/stubs/migration.alter.stub +17 -0
- swiftpy/console/stubs/migration.api_keys.stub +22 -0
- swiftpy/console/stubs/migration.blank.stub +11 -0
- swiftpy/console/stubs/migration.create.stub +15 -0
- swiftpy/console/stubs/migration.stub +11 -0
- swiftpy/console/stubs/migration.update.stub +17 -0
- swiftpy/console/stubs/model.stub +17 -0
- swiftpy/console/stubs/provider.stub +18 -0
- swiftpy/console/stubs/repository.stub +10 -0
- swiftpy/console/stubs/request.stub +16 -0
- swiftpy/console/stubs/resource.stub +21 -0
- swiftpy/console/stubs/schema.stub +12 -0
- swiftpy/console/stubs/seeder.stub +14 -0
- swiftpy/console/stubs/service.stub +10 -0
- swiftpy/core/__init__.py +0 -0
- swiftpy/core/application.py +1306 -0
- swiftpy/core/bootstrap.py +30 -0
- swiftpy/core/config.py +432 -0
- swiftpy/core/container.py +596 -0
- swiftpy/core/container_exceptions.py +18 -0
- swiftpy/core/context.py +148 -0
- swiftpy/core/exceptions.py +352 -0
- swiftpy/core/providers/base.py +35 -0
- swiftpy/database/__init__.py +41 -0
- swiftpy/database/exceptions.py +42 -0
- swiftpy/database/identifiers.py +105 -0
- swiftpy/database/migrations/__init__.py +36 -0
- swiftpy/database/migrations/blueprint.py +431 -0
- swiftpy/database/migrations/lock.py +104 -0
- swiftpy/database/migrations/migration.py +22 -0
- swiftpy/database/migrations/migrator.py +507 -0
- swiftpy/database/migrations/schema.py +435 -0
- swiftpy/database/orm/__init__.py +33 -0
- swiftpy/database/orm/fields.py +92 -0
- swiftpy/database/orm/model.py +2034 -0
- swiftpy/database/orm/registry.py +33 -0
- swiftpy/database/orm/relationships.py +836 -0
- swiftpy/database/pagination/__init__.py +16 -0
- swiftpy/database/pagination/contracts.py +60 -0
- swiftpy/database/pagination/paginator.py +112 -0
- swiftpy/database/pagination/simple_paginator.py +97 -0
- swiftpy/database/pool.py +218 -0
- swiftpy/database/query_builder.py +1803 -0
- swiftpy/database/seeder.py +39 -0
- swiftpy/database/transaction.py +276 -0
- swiftpy/events/__init__.py +10 -0
- swiftpy/events/dispatcher.py +308 -0
- swiftpy/events/redis_bus.py +552 -0
- swiftpy/filesystem/__init__.py +227 -0
- swiftpy/filesystem/disk.py +378 -0
- swiftpy/filesystem/drivers/__init__.py +11 -0
- swiftpy/filesystem/drivers/base.py +156 -0
- swiftpy/filesystem/drivers/local.py +559 -0
- swiftpy/filesystem/drivers/s3.py +655 -0
- swiftpy/filesystem/events.py +37 -0
- swiftpy/filesystem/exceptions.py +127 -0
- swiftpy/filesystem/manager.py +138 -0
- swiftpy/filesystem/metadata.py +22 -0
- swiftpy/filesystem/paths.py +99 -0
- swiftpy/filesystem/service_provider.py +17 -0
- swiftpy/filesystem/uploads/__init__.py +13 -0
- swiftpy/filesystem/uploads/handlers.py +362 -0
- swiftpy/filesystem/uploads/uploaded_file.py +222 -0
- swiftpy/filesystem/uploads/validators.py +186 -0
- swiftpy/filesystem/visibility.py +22 -0
- swiftpy/helpers/__init__.py +42 -0
- swiftpy/helpers/helper.py +180 -0
- swiftpy/http/exceptions.py +41 -0
- swiftpy/http/middleware.py +307 -0
- swiftpy/http/request.py +375 -0
- swiftpy/http/response.py +442 -0
- swiftpy/http/router.py +1050 -0
- swiftpy/mail/__init__.py +58 -0
- swiftpy/mail/attachments.py +255 -0
- swiftpy/mail/config.py +71 -0
- swiftpy/mail/envelope.py +183 -0
- swiftpy/mail/exceptions.py +90 -0
- swiftpy/mail/facade.py +312 -0
- swiftpy/mail/mailable.py +53 -0
- swiftpy/mail/manager.py +240 -0
- swiftpy/mail/message.py +186 -0
- swiftpy/mail/models.py +19 -0
- swiftpy/mail/queue/__init__.py +5 -0
- swiftpy/mail/queue/job.py +279 -0
- swiftpy/mail/service_provider.py +19 -0
- swiftpy/mail/templates/__init__.py +5 -0
- swiftpy/mail/templates/renderer.py +173 -0
- swiftpy/mail/transports/__init__.py +21 -0
- swiftpy/mail/transports/api/__init__.py +13 -0
- swiftpy/mail/transports/api/base.py +194 -0
- swiftpy/mail/transports/api/mailgun.py +158 -0
- swiftpy/mail/transports/api/resend.py +156 -0
- swiftpy/mail/transports/api/sendgrid.py +172 -0
- swiftpy/mail/transports/base.py +24 -0
- swiftpy/mail/transports/console.py +91 -0
- swiftpy/mail/transports/sendmail.py +128 -0
- swiftpy/mail/transports/smtp.py +466 -0
- swiftpy/queue/__init__.py +13 -0
- swiftpy/queue/dispatcher.py +813 -0
- swiftpy/queue/job.py +541 -0
- swiftpy/queue/worker.py +556 -0
- swiftpy/support/__init__.py +12 -0
- swiftpy/support/keys.py +108 -0
- swiftpy/support/redis_config.py +223 -0
- swiftpy/support/strings.py +114 -0
- swiftpy/testing/__init__.py +20 -0
- swiftpy/testing/fakes/__init__.py +11 -0
- swiftpy/testing/fakes/ai_fake.py +21 -0
- swiftpy/testing/fakes/vectorstore_fake.py +194 -0
- swiftpy/testing/fixtures.py +124 -0
- swiftpy/testing/http_client.py +730 -0
- swiftpy/testing/test_case.py +126 -0
- swiftpy/validation/__init__.py +97 -0
- swiftpy/validation/exceptions.py +26 -0
- swiftpy/validation/form_request.py +302 -0
- swiftpy/validation/protocols.py +33 -0
- swiftpy/validation/rules/__init__.py +89 -0
- swiftpy/validation/rules/builtin.py +391 -0
- swiftpy/validation/rules/exists.py +86 -0
- swiftpy/validation/rules/registry.py +290 -0
- swiftpy/validation/rules/required.py +55 -0
- swiftpy/validation/rules/unique.py +138 -0
- swiftpy/validation/validator.py +612 -0
- swiftpy/websockets/__init__.py +19 -0
- swiftpy/websockets/connection.py +317 -0
- swiftpy/websockets/manager.py +271 -0
- swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
- swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
- swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
- swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
- swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
swiftpy/http/response.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import (
|
|
6
|
+
AsyncIterator,
|
|
7
|
+
Iterable,
|
|
8
|
+
Iterator,
|
|
9
|
+
Mapping,
|
|
10
|
+
MutableMapping,
|
|
11
|
+
Sequence,
|
|
12
|
+
)
|
|
13
|
+
from datetime import date, datetime, time
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Protocol, runtime_checkable
|
|
17
|
+
from uuid import UUID
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class _SupportsKeysAndGetItem(Protocol):
|
|
22
|
+
def keys(self) -> Iterable[str]: ...
|
|
23
|
+
def __getitem__(self, key: str, /) -> str: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validate_header_pair(name: str, value: str) -> tuple[str, str]:
|
|
27
|
+
if not isinstance(name, str) or not isinstance(value, str):
|
|
28
|
+
raise TypeError("Header names and values must be strings.")
|
|
29
|
+
if not name or not name.strip():
|
|
30
|
+
raise ValueError("Header name must not be empty.")
|
|
31
|
+
if "\r" in name or "\n" in name or "\x00" in name:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Header names and values must not contain CR or LF characters."
|
|
34
|
+
)
|
|
35
|
+
if "\r" in value or "\n" in value or "\x00" in value:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"Header names and values must not contain CR or LF characters."
|
|
38
|
+
)
|
|
39
|
+
return name, value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ResponseHeaders(MutableMapping[str, str]):
|
|
43
|
+
"""
|
|
44
|
+
Case-insensitive HTTP header container backed by an ordered list of (name, value) pairs.
|
|
45
|
+
Supports dict-style access while preserving duplicate headers such as Set-Cookie via .add() / .getlist().
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
headers: (
|
|
51
|
+
Mapping[str, str]
|
|
52
|
+
| Sequence[tuple[str, str]]
|
|
53
|
+
| ResponseHeaders
|
|
54
|
+
| None
|
|
55
|
+
) = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self._items: list[tuple[str, str]] = []
|
|
58
|
+
if headers is not None:
|
|
59
|
+
if isinstance(headers, ResponseHeaders):
|
|
60
|
+
self._items = list(headers._items)
|
|
61
|
+
elif isinstance(headers, Mapping):
|
|
62
|
+
for k, v in headers.items():
|
|
63
|
+
self[k] = v
|
|
64
|
+
else:
|
|
65
|
+
for k, v in headers:
|
|
66
|
+
self.add(k, v)
|
|
67
|
+
|
|
68
|
+
def add(self, name: str, value: str) -> None:
|
|
69
|
+
"""Append a header pair without removing existing headers of the same name (e.g. Set-Cookie)."""
|
|
70
|
+
safe_name, safe_val = _validate_header_pair(name, value)
|
|
71
|
+
self._items.append((safe_name, safe_val))
|
|
72
|
+
|
|
73
|
+
def append(self, name: str, value: str) -> None:
|
|
74
|
+
"""Alias for add()."""
|
|
75
|
+
self.add(name, value)
|
|
76
|
+
|
|
77
|
+
def getlist(self, name: str) -> list[str]:
|
|
78
|
+
"""Return all values for a header name (case-insensitive)."""
|
|
79
|
+
target = name.lower()
|
|
80
|
+
return [v for k, v in self._items if k.lower() == target]
|
|
81
|
+
|
|
82
|
+
def raw_items(self) -> list[tuple[str, str]]:
|
|
83
|
+
"""Return all (name, value) pairs in order, including repeated headers."""
|
|
84
|
+
return list(self._items)
|
|
85
|
+
|
|
86
|
+
def __getitem__(self, name: str) -> str:
|
|
87
|
+
target = name.lower()
|
|
88
|
+
for k, v in reversed(self._items):
|
|
89
|
+
if k.lower() == target:
|
|
90
|
+
return v
|
|
91
|
+
raise KeyError(name)
|
|
92
|
+
|
|
93
|
+
def __setitem__(self, name: str, value: str) -> None:
|
|
94
|
+
safe_name, safe_val = _validate_header_pair(name, value)
|
|
95
|
+
target = safe_name.lower()
|
|
96
|
+
replaced = False
|
|
97
|
+
new_items: list[tuple[str, str]] = []
|
|
98
|
+
for k, v in self._items:
|
|
99
|
+
if k.lower() == target:
|
|
100
|
+
if not replaced:
|
|
101
|
+
new_items.append((safe_name, safe_val))
|
|
102
|
+
replaced = True
|
|
103
|
+
else:
|
|
104
|
+
new_items.append((k, v))
|
|
105
|
+
if not replaced:
|
|
106
|
+
new_items.append((safe_name, safe_val))
|
|
107
|
+
self._items = new_items
|
|
108
|
+
|
|
109
|
+
def __delitem__(self, name: str) -> None:
|
|
110
|
+
target = name.lower()
|
|
111
|
+
new_items = [(k, v) for k, v in self._items if k.lower() != target]
|
|
112
|
+
if len(new_items) == len(self._items):
|
|
113
|
+
raise KeyError(name)
|
|
114
|
+
self._items = new_items
|
|
115
|
+
|
|
116
|
+
def __contains__(self, name: object) -> bool:
|
|
117
|
+
if not isinstance(name, str):
|
|
118
|
+
return False
|
|
119
|
+
target = name.lower()
|
|
120
|
+
return any(k.lower() == target for k, _ in self._items)
|
|
121
|
+
|
|
122
|
+
def __iter__(self) -> Iterator[str]:
|
|
123
|
+
seen: set[str] = set()
|
|
124
|
+
for k, _ in self._items:
|
|
125
|
+
low = k.lower()
|
|
126
|
+
if low not in seen:
|
|
127
|
+
seen.add(low)
|
|
128
|
+
yield k
|
|
129
|
+
|
|
130
|
+
def __len__(self) -> int:
|
|
131
|
+
return len({k.lower() for k, _ in self._items})
|
|
132
|
+
|
|
133
|
+
def update(
|
|
134
|
+
self,
|
|
135
|
+
other: _SupportsKeysAndGetItem | Iterable[tuple[str, str]] | None = None,
|
|
136
|
+
/,
|
|
137
|
+
**kwargs: str,
|
|
138
|
+
) -> None:
|
|
139
|
+
if other is not None:
|
|
140
|
+
if isinstance(other, ResponseHeaders):
|
|
141
|
+
keys_to_replace = {k.lower() for k, _ in other._items}
|
|
142
|
+
self._items = [
|
|
143
|
+
(k, v)
|
|
144
|
+
for k, v in self._items
|
|
145
|
+
if k.lower() not in keys_to_replace
|
|
146
|
+
]
|
|
147
|
+
for k, v in other._items:
|
|
148
|
+
self.add(k, v)
|
|
149
|
+
elif isinstance(other, Mapping):
|
|
150
|
+
for k, v in other.items():
|
|
151
|
+
self[k] = v
|
|
152
|
+
elif isinstance(other, _SupportsKeysAndGetItem):
|
|
153
|
+
keys = other.keys()
|
|
154
|
+
for k in keys:
|
|
155
|
+
self[k] = other[k]
|
|
156
|
+
else:
|
|
157
|
+
pairs = list(other)
|
|
158
|
+
keys_to_replace = {k.lower() for k, _ in pairs}
|
|
159
|
+
self._items = [
|
|
160
|
+
(k, v)
|
|
161
|
+
for k, v in self._items
|
|
162
|
+
if k.lower() not in keys_to_replace
|
|
163
|
+
]
|
|
164
|
+
for k, v in pairs:
|
|
165
|
+
self.add(k, v)
|
|
166
|
+
for k, v in kwargs.items():
|
|
167
|
+
self[k] = v
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return f"ResponseHeaders({self._items!r})"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _json_default(obj: Any) -> Any:
|
|
174
|
+
"""Default JSON serializer for framework response payloads."""
|
|
175
|
+
if isinstance(obj, (bytes, bytearray, memoryview)):
|
|
176
|
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
|
177
|
+
if isinstance(obj, (datetime, date, time)):
|
|
178
|
+
return obj.isoformat()
|
|
179
|
+
if isinstance(obj, (UUID, Decimal)):
|
|
180
|
+
return str(obj)
|
|
181
|
+
if isinstance(obj, Enum):
|
|
182
|
+
return obj.value
|
|
183
|
+
if hasattr(obj, "to_dict") and callable(obj.to_dict):
|
|
184
|
+
raw_dict = obj.to_dict()
|
|
185
|
+
if isinstance(raw_dict, dict):
|
|
186
|
+
hidden = set(getattr(obj, "__hidden__", ()) or ())
|
|
187
|
+
if hidden:
|
|
188
|
+
return {k: v for k, v in raw_dict.items() if k not in hidden}
|
|
189
|
+
return raw_dict
|
|
190
|
+
return raw_dict
|
|
191
|
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def encode_json(data: Any) -> bytes:
|
|
195
|
+
"""Serialize data to UTF-8 JSON bytes using SwiftPY's strict encoder."""
|
|
196
|
+
return json.dumps(data, default=_json_default, allow_nan=False).encode("utf-8")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Response:
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
content: bytes | str = b"",
|
|
203
|
+
status_code: int = 200,
|
|
204
|
+
headers: (
|
|
205
|
+
Mapping[str, str]
|
|
206
|
+
| Sequence[tuple[str, str]]
|
|
207
|
+
| ResponseHeaders
|
|
208
|
+
| None
|
|
209
|
+
) = None,
|
|
210
|
+
*,
|
|
211
|
+
_set_content_length: bool = True,
|
|
212
|
+
) -> None:
|
|
213
|
+
is_str = False
|
|
214
|
+
if isinstance(content, str):
|
|
215
|
+
is_str = True
|
|
216
|
+
self.content: bytes = content.encode("utf-8")
|
|
217
|
+
elif isinstance(content, (bytes, bytearray, memoryview)):
|
|
218
|
+
self.content = bytes(content)
|
|
219
|
+
else:
|
|
220
|
+
raise TypeError(
|
|
221
|
+
f"Response content must be bytes or str, got {type(content).__name__}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self.status_code = status_code
|
|
225
|
+
self.headers = ResponseHeaders(headers)
|
|
226
|
+
|
|
227
|
+
if is_str and "content-type" not in self.headers:
|
|
228
|
+
self.headers["content-type"] = "text/plain; charset=utf-8"
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
_set_content_length
|
|
232
|
+
and "content-length" not in self.headers
|
|
233
|
+
and self.status_code not in (204, 304)
|
|
234
|
+
and not (100 <= self.status_code < 200)
|
|
235
|
+
):
|
|
236
|
+
self.headers["content-length"] = str(len(self.content))
|
|
237
|
+
|
|
238
|
+
def set_cookie(self, cookie_value: str) -> None:
|
|
239
|
+
"""Append a Set-Cookie header without overwriting previously added cookies."""
|
|
240
|
+
self.headers.add("Set-Cookie", cookie_value)
|
|
241
|
+
|
|
242
|
+
async def send(self, send: Any, *, send_body: bool = True) -> None:
|
|
243
|
+
body_bytes = (
|
|
244
|
+
self.content.encode("utf-8")
|
|
245
|
+
if isinstance(self.content, str)
|
|
246
|
+
else bytes(self.content)
|
|
247
|
+
)
|
|
248
|
+
if (
|
|
249
|
+
"content-length" not in self.headers
|
|
250
|
+
and self.status_code not in (204, 304)
|
|
251
|
+
and not (100 <= self.status_code < 200)
|
|
252
|
+
):
|
|
253
|
+
self.headers["content-length"] = str(len(body_bytes))
|
|
254
|
+
|
|
255
|
+
raw_headers = [
|
|
256
|
+
(
|
|
257
|
+
_validate_header_pair(k, v)[0].encode("latin-1"),
|
|
258
|
+
v.encode("latin-1"),
|
|
259
|
+
)
|
|
260
|
+
for k, v in self.headers.raw_items()
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
await send(
|
|
264
|
+
{
|
|
265
|
+
"type": "http.response.start",
|
|
266
|
+
"status": self.status_code,
|
|
267
|
+
"headers": raw_headers,
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
await send(
|
|
272
|
+
{
|
|
273
|
+
"type": "http.response.body",
|
|
274
|
+
"body": body_bytes if send_body else b"",
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class StreamingResponse(Response):
|
|
280
|
+
def __init__(
|
|
281
|
+
self,
|
|
282
|
+
iterator: AsyncIterator[bytes | str],
|
|
283
|
+
status_code: int = 200,
|
|
284
|
+
headers: (
|
|
285
|
+
Mapping[str, str]
|
|
286
|
+
| Sequence[tuple[str, str]]
|
|
287
|
+
| ResponseHeaders
|
|
288
|
+
| None
|
|
289
|
+
) = None,
|
|
290
|
+
) -> None:
|
|
291
|
+
super().__init__(
|
|
292
|
+
content=b"",
|
|
293
|
+
status_code=status_code,
|
|
294
|
+
headers=headers,
|
|
295
|
+
_set_content_length=False,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
self.iterator = iterator
|
|
299
|
+
|
|
300
|
+
async def send(self, send: Any, *, send_body: bool = True) -> None:
|
|
301
|
+
raw_headers = [
|
|
302
|
+
(
|
|
303
|
+
_validate_header_pair(key, value)[0].encode("latin-1"),
|
|
304
|
+
value.encode("latin-1"),
|
|
305
|
+
)
|
|
306
|
+
for key, value in self.headers.raw_items()
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
await send(
|
|
310
|
+
{
|
|
311
|
+
"type": "http.response.start",
|
|
312
|
+
"status": self.status_code,
|
|
313
|
+
"headers": raw_headers,
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
if send_body:
|
|
319
|
+
async for chunk in self.iterator:
|
|
320
|
+
chunk_bytes = chunk.encode("utf-8") if isinstance(chunk, str) else chunk
|
|
321
|
+
await send(
|
|
322
|
+
{
|
|
323
|
+
"type": "http.response.body",
|
|
324
|
+
"body": chunk_bytes,
|
|
325
|
+
"more_body": True,
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
await send(
|
|
330
|
+
{
|
|
331
|
+
"type": "http.response.body",
|
|
332
|
+
"body": b"",
|
|
333
|
+
"more_body": False,
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
finally:
|
|
337
|
+
aclose = getattr(self.iterator, "aclose", None)
|
|
338
|
+
if callable(aclose):
|
|
339
|
+
close_res = aclose()
|
|
340
|
+
if inspect.isawaitable(close_res):
|
|
341
|
+
await close_res
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def json_response(
|
|
345
|
+
data: Any,
|
|
346
|
+
status_code: int = 200,
|
|
347
|
+
headers: (
|
|
348
|
+
Mapping[str, str]
|
|
349
|
+
| Sequence[tuple[str, str]]
|
|
350
|
+
| ResponseHeaders
|
|
351
|
+
| None
|
|
352
|
+
) = None,
|
|
353
|
+
) -> Response:
|
|
354
|
+
resp_headers = ResponseHeaders({"content-type": "application/json"})
|
|
355
|
+
if headers is not None:
|
|
356
|
+
resp_headers.update(headers)
|
|
357
|
+
return Response(
|
|
358
|
+
content=encode_json(data),
|
|
359
|
+
status_code=status_code,
|
|
360
|
+
headers=resp_headers,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def created(data: Any) -> Response:
|
|
365
|
+
return json_response(
|
|
366
|
+
data,
|
|
367
|
+
status_code=201,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def not_found(message: str = "Not Found") -> Response:
|
|
372
|
+
return json_response(
|
|
373
|
+
{"detail": message},
|
|
374
|
+
status_code=404,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def unprocessable(
|
|
379
|
+
errors: Any,
|
|
380
|
+
) -> Response:
|
|
381
|
+
return json_response(
|
|
382
|
+
{"errors": errors},
|
|
383
|
+
status_code=422,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def unauthorized(
|
|
388
|
+
message: str = "Unauthorized",
|
|
389
|
+
headers: (
|
|
390
|
+
Mapping[str, str]
|
|
391
|
+
| Sequence[tuple[str, str]]
|
|
392
|
+
| ResponseHeaders
|
|
393
|
+
| None
|
|
394
|
+
) = None,
|
|
395
|
+
) -> Response:
|
|
396
|
+
resp_headers = ResponseHeaders(
|
|
397
|
+
{"content-type": "application/json", "WWW-Authenticate": "Bearer"}
|
|
398
|
+
)
|
|
399
|
+
if headers is not None:
|
|
400
|
+
resp_headers.update(headers)
|
|
401
|
+
return Response(
|
|
402
|
+
content=encode_json({"detail": message}),
|
|
403
|
+
status_code=401,
|
|
404
|
+
headers=resp_headers,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def forbidden(message: str = "Forbidden") -> Response:
|
|
409
|
+
return json_response(
|
|
410
|
+
{"detail": message},
|
|
411
|
+
status_code=403,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def stream(
|
|
416
|
+
iterator: AsyncIterator[bytes | str],
|
|
417
|
+
*,
|
|
418
|
+
status_code: int = 200,
|
|
419
|
+
content_type: str = "application/octet-stream",
|
|
420
|
+
) -> StreamingResponse:
|
|
421
|
+
return StreamingResponse(
|
|
422
|
+
iterator=iterator,
|
|
423
|
+
status_code=status_code,
|
|
424
|
+
headers={
|
|
425
|
+
"content-type": content_type,
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
__all__ = [
|
|
431
|
+
"Response",
|
|
432
|
+
"ResponseHeaders",
|
|
433
|
+
"StreamingResponse",
|
|
434
|
+
"created",
|
|
435
|
+
"encode_json",
|
|
436
|
+
"forbidden",
|
|
437
|
+
"json_response",
|
|
438
|
+
"not_found",
|
|
439
|
+
"stream",
|
|
440
|
+
"unauthorized",
|
|
441
|
+
"unprocessable",
|
|
442
|
+
]
|