django-bolt 0.1.0__cp310-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.

Potentially problematic release.


This version of django-bolt might be problematic. Click here for more details.

Files changed (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.pyd +0 -0
  3. django_bolt/admin/__init__.py +25 -0
  4. django_bolt/admin/admin_detection.py +179 -0
  5. django_bolt/admin/asgi_bridge.py +267 -0
  6. django_bolt/admin/routes.py +91 -0
  7. django_bolt/admin/static.py +155 -0
  8. django_bolt/admin/static_routes.py +111 -0
  9. django_bolt/api.py +1011 -0
  10. django_bolt/apps.py +7 -0
  11. django_bolt/async_collector.py +228 -0
  12. django_bolt/auth/README.md +464 -0
  13. django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
  14. django_bolt/auth/__init__.py +84 -0
  15. django_bolt/auth/backends.py +236 -0
  16. django_bolt/auth/guards.py +224 -0
  17. django_bolt/auth/jwt_utils.py +212 -0
  18. django_bolt/auth/revocation.py +286 -0
  19. django_bolt/auth/token.py +335 -0
  20. django_bolt/binding.py +363 -0
  21. django_bolt/bootstrap.py +77 -0
  22. django_bolt/cli.py +133 -0
  23. django_bolt/compression.py +104 -0
  24. django_bolt/decorators.py +159 -0
  25. django_bolt/dependencies.py +128 -0
  26. django_bolt/error_handlers.py +305 -0
  27. django_bolt/exceptions.py +294 -0
  28. django_bolt/health.py +129 -0
  29. django_bolt/logging/__init__.py +6 -0
  30. django_bolt/logging/config.py +357 -0
  31. django_bolt/logging/middleware.py +296 -0
  32. django_bolt/management/__init__.py +1 -0
  33. django_bolt/management/commands/__init__.py +0 -0
  34. django_bolt/management/commands/runbolt.py +427 -0
  35. django_bolt/middleware/__init__.py +32 -0
  36. django_bolt/middleware/compiler.py +131 -0
  37. django_bolt/middleware/middleware.py +247 -0
  38. django_bolt/openapi/__init__.py +23 -0
  39. django_bolt/openapi/config.py +196 -0
  40. django_bolt/openapi/plugins.py +439 -0
  41. django_bolt/openapi/routes.py +152 -0
  42. django_bolt/openapi/schema_generator.py +581 -0
  43. django_bolt/openapi/spec/__init__.py +68 -0
  44. django_bolt/openapi/spec/base.py +74 -0
  45. django_bolt/openapi/spec/callback.py +24 -0
  46. django_bolt/openapi/spec/components.py +72 -0
  47. django_bolt/openapi/spec/contact.py +21 -0
  48. django_bolt/openapi/spec/discriminator.py +25 -0
  49. django_bolt/openapi/spec/encoding.py +67 -0
  50. django_bolt/openapi/spec/enums.py +41 -0
  51. django_bolt/openapi/spec/example.py +36 -0
  52. django_bolt/openapi/spec/external_documentation.py +21 -0
  53. django_bolt/openapi/spec/header.py +132 -0
  54. django_bolt/openapi/spec/info.py +50 -0
  55. django_bolt/openapi/spec/license.py +28 -0
  56. django_bolt/openapi/spec/link.py +66 -0
  57. django_bolt/openapi/spec/media_type.py +51 -0
  58. django_bolt/openapi/spec/oauth_flow.py +36 -0
  59. django_bolt/openapi/spec/oauth_flows.py +28 -0
  60. django_bolt/openapi/spec/open_api.py +87 -0
  61. django_bolt/openapi/spec/operation.py +105 -0
  62. django_bolt/openapi/spec/parameter.py +147 -0
  63. django_bolt/openapi/spec/path_item.py +78 -0
  64. django_bolt/openapi/spec/paths.py +27 -0
  65. django_bolt/openapi/spec/reference.py +38 -0
  66. django_bolt/openapi/spec/request_body.py +38 -0
  67. django_bolt/openapi/spec/response.py +48 -0
  68. django_bolt/openapi/spec/responses.py +44 -0
  69. django_bolt/openapi/spec/schema.py +678 -0
  70. django_bolt/openapi/spec/security_requirement.py +28 -0
  71. django_bolt/openapi/spec/security_scheme.py +69 -0
  72. django_bolt/openapi/spec/server.py +34 -0
  73. django_bolt/openapi/spec/server_variable.py +32 -0
  74. django_bolt/openapi/spec/tag.py +32 -0
  75. django_bolt/openapi/spec/xml.py +44 -0
  76. django_bolt/pagination.py +669 -0
  77. django_bolt/param_functions.py +49 -0
  78. django_bolt/params.py +337 -0
  79. django_bolt/request_parsing.py +128 -0
  80. django_bolt/responses.py +214 -0
  81. django_bolt/router.py +48 -0
  82. django_bolt/serialization.py +193 -0
  83. django_bolt/status_codes.py +321 -0
  84. django_bolt/testing/__init__.py +10 -0
  85. django_bolt/testing/client.py +274 -0
  86. django_bolt/testing/helpers.py +93 -0
  87. django_bolt/tests/__init__.py +0 -0
  88. django_bolt/tests/admin_tests/__init__.py +1 -0
  89. django_bolt/tests/admin_tests/conftest.py +6 -0
  90. django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
  91. django_bolt/tests/admin_tests/urls.py +9 -0
  92. django_bolt/tests/cbv/__init__.py +0 -0
  93. django_bolt/tests/cbv/test_class_views.py +570 -0
  94. django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
  95. django_bolt/tests/cbv/test_class_views_features.py +1173 -0
  96. django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
  97. django_bolt/tests/conftest.py +165 -0
  98. django_bolt/tests/test_action_decorator.py +399 -0
  99. django_bolt/tests/test_auth_secret_key.py +83 -0
  100. django_bolt/tests/test_decorator_syntax.py +159 -0
  101. django_bolt/tests/test_error_handling.py +481 -0
  102. django_bolt/tests/test_file_response.py +192 -0
  103. django_bolt/tests/test_global_cors.py +172 -0
  104. django_bolt/tests/test_guards_auth.py +441 -0
  105. django_bolt/tests/test_guards_integration.py +303 -0
  106. django_bolt/tests/test_health.py +283 -0
  107. django_bolt/tests/test_integration_validation.py +400 -0
  108. django_bolt/tests/test_json_validation.py +536 -0
  109. django_bolt/tests/test_jwt_auth.py +327 -0
  110. django_bolt/tests/test_jwt_token.py +458 -0
  111. django_bolt/tests/test_logging.py +837 -0
  112. django_bolt/tests/test_logging_merge.py +419 -0
  113. django_bolt/tests/test_middleware.py +492 -0
  114. django_bolt/tests/test_middleware_server.py +230 -0
  115. django_bolt/tests/test_model_viewset.py +323 -0
  116. django_bolt/tests/test_models.py +24 -0
  117. django_bolt/tests/test_pagination.py +1258 -0
  118. django_bolt/tests/test_parameter_validation.py +178 -0
  119. django_bolt/tests/test_syntax.py +626 -0
  120. django_bolt/tests/test_testing_utilities.py +163 -0
  121. django_bolt/tests/test_testing_utilities_simple.py +123 -0
  122. django_bolt/tests/test_viewset_unified.py +346 -0
  123. django_bolt/typing.py +273 -0
  124. django_bolt/views.py +1110 -0
  125. django_bolt-0.1.0.dist-info/METADATA +629 -0
  126. django_bolt-0.1.0.dist-info/RECORD +128 -0
  127. django_bolt-0.1.0.dist-info/WHEEL +4 -0
  128. django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,622 @@
1
+ """
2
+ Tests for class-based views using TestClient utilities.
3
+
4
+ This test suite uses the fast TestClient for better performance
5
+ and more realistic integration testing.
6
+
7
+ Tests cover:
8
+ - Basic APIView functionality
9
+ - Parameter extraction (path, query, body)
10
+ - Dependency injection
11
+ - Guards and authentication
12
+ - Return type annotations
13
+ - Mixins (ListMixin, RetrieveMixin, CreateMixin, etc.)
14
+ - ViewSet
15
+ - HTTP method handling
16
+ """
17
+ import pytest
18
+ import msgspec
19
+ import time
20
+ import jwt
21
+ from django_bolt import BoltAPI
22
+ from django_bolt.testing import TestClient
23
+ from django_bolt.views import (
24
+ APIView,
25
+ ViewSet,
26
+ ListMixin,
27
+ RetrieveMixin,
28
+ CreateMixin,
29
+ UpdateMixin,
30
+ PartialUpdateMixin,
31
+ DestroyMixin,
32
+ )
33
+ from django_bolt.params import Depends
34
+ from django_bolt.exceptions import HTTPException
35
+ from django_bolt.auth.guards import IsAuthenticated, IsAdminUser
36
+ from django_bolt.auth.backends import JWTAuthentication
37
+
38
+
39
+ # --- Test Fixtures ---
40
+
41
+ def create_jwt_token(user_id: int = 1, is_admin: bool = False, secret: str = "test-secret") -> str:
42
+ """Helper to create JWT tokens for testing."""
43
+ payload = {
44
+ "sub": str(user_id),
45
+ "user_id": user_id,
46
+ "exp": int(time.time()) + 3600,
47
+ }
48
+ if is_admin:
49
+ payload["is_superuser"] = True
50
+ return jwt.encode(payload, secret, algorithm="HS256")
51
+
52
+
53
+ # --- Basic Tests ---
54
+
55
+ def test_bolt_api_view_basic():
56
+ """Test basic APIView with GET handler."""
57
+ api = BoltAPI()
58
+
59
+ @api.view("/hello")
60
+ class HelloView(APIView):
61
+ async def get(self, request) -> dict:
62
+ return {"message": "Hello"}
63
+
64
+ with TestClient(api) as client:
65
+ response = client.get("/hello")
66
+ assert response.status_code == 200
67
+ assert response.json() == {"message": "Hello"}
68
+
69
+
70
+ def test_bolt_api_view_multiple_methods():
71
+ """Test view with multiple HTTP methods."""
72
+ api = BoltAPI()
73
+
74
+ @api.view("/multi")
75
+ class MultiMethodView(APIView):
76
+ async def get(self, request) -> dict:
77
+ return {"method": "GET"}
78
+
79
+ async def post(self, request) -> dict:
80
+ return {"method": "POST"}
81
+
82
+ async def put(self, request) -> dict:
83
+ return {"method": "PUT"}
84
+
85
+ with TestClient(api) as client:
86
+ response = client.get("/multi")
87
+ assert response.json() == {"method": "GET"}
88
+
89
+ response = client.post("/multi", json={})
90
+ assert response.json() == {"method": "POST"}
91
+
92
+ response = client.put("/multi", json={})
93
+ assert response.json() == {"method": "PUT"}
94
+
95
+
96
+ def test_bolt_api_view_path_params():
97
+ """Test path parameter extraction in class-based views."""
98
+ api = BoltAPI()
99
+
100
+ @api.view("/users/{user_id}")
101
+ class UserView(APIView):
102
+ async def get(self, request, user_id: int) -> dict:
103
+ return {"user_id": user_id, "type": type(user_id).__name__}
104
+
105
+ with TestClient(api) as client:
106
+ response = client.get("/users/123")
107
+ assert response.status_code == 200
108
+ data = response.json()
109
+ assert data["user_id"] == 123
110
+ assert data["type"] == "int"
111
+
112
+
113
+ def test_bolt_api_view_query_params():
114
+ """Test query parameter extraction in class-based views."""
115
+ api = BoltAPI()
116
+
117
+ @api.view("/search")
118
+ class SearchView(APIView):
119
+ async def get(self, request, q: str, limit: int = 10) -> dict:
120
+ return {"query": q, "limit": limit}
121
+
122
+ with TestClient(api) as client:
123
+ # Test with both params
124
+ response = client.get("/search?q=test&limit=20")
125
+ assert response.status_code == 200
126
+ assert response.json() == {"query": "test", "limit": 20}
127
+
128
+ # Test with default limit
129
+ response = client.get("/search?q=bolt")
130
+ assert response.status_code == 200
131
+ assert response.json() == {"query": "bolt", "limit": 10}
132
+
133
+
134
+ def test_bolt_api_view_request_body():
135
+ """Test request body parsing with msgspec.Struct."""
136
+ api = BoltAPI()
137
+
138
+ class CreateUserRequest(msgspec.Struct):
139
+ username: str
140
+ email: str
141
+
142
+ @api.view("/users")
143
+ class UserCreateView(APIView):
144
+ async def post(self, request, data: CreateUserRequest) -> dict:
145
+ return {"username": data.username, "email": data.email}
146
+
147
+ with TestClient(api) as client:
148
+ response = client.post(
149
+ "/users",
150
+ json={"username": "john", "email": "john@example.com"}
151
+ )
152
+ assert response.status_code == 200
153
+ data = response.json()
154
+ assert data["username"] == "john"
155
+ assert data["email"] == "john@example.com"
156
+
157
+
158
+ def test_bolt_api_view_return_annotation():
159
+ """Test that return annotations are preserved and used."""
160
+ api = BoltAPI()
161
+
162
+ class ResponseSchema(msgspec.Struct):
163
+ message: str
164
+ count: int
165
+
166
+ @api.view("/annotated")
167
+ class AnnotatedView(APIView):
168
+ async def get(self, request) -> ResponseSchema:
169
+ return ResponseSchema(message="test", count=42)
170
+
171
+ with TestClient(api) as client:
172
+ response = client.get("/annotated")
173
+ assert response.status_code == 200
174
+ data = response.json()
175
+ assert data["message"] == "test"
176
+ assert data["count"] == 42
177
+
178
+
179
+ # --- Dependency Injection Tests ---
180
+
181
+ def test_bolt_api_view_dependency_injection():
182
+ """Test dependency injection in class-based views."""
183
+ api = BoltAPI()
184
+
185
+ async def get_mock_user(request) -> dict:
186
+ return {"id": 1, "username": "testuser"}
187
+
188
+ @api.view("/profile")
189
+ class ProfileView(APIView):
190
+ async def get(self, request, current_user=Depends(get_mock_user)) -> dict:
191
+ return {"user": current_user}
192
+
193
+ with TestClient(api) as client:
194
+ response = client.get("/profile")
195
+ assert response.status_code == 200
196
+ data = response.json()
197
+ assert data["user"]["username"] == "testuser"
198
+
199
+
200
+ # --- Guards and Authentication Tests ---
201
+
202
+ def test_bolt_api_view_class_level_guards():
203
+ """Test class-level guards are applied."""
204
+ api = BoltAPI()
205
+
206
+ @api.view("/protected")
207
+ class ProtectedView(APIView):
208
+ auth = [JWTAuthentication(secret="test-secret")] # Correct parameter is 'secret'
209
+ guards = [IsAuthenticated()]
210
+
211
+ async def get(self, request) -> dict:
212
+ auth = request.get("auth", {})
213
+ return {"user_id": auth.get("user_id")}
214
+
215
+ with TestClient(api) as client:
216
+ # Without auth - should fail
217
+ response = client.get("/protected")
218
+ assert response.status_code == 401
219
+
220
+ # With valid token - should succeed
221
+ token = create_jwt_token(user_id=42)
222
+ response = client.get(
223
+ "/protected",
224
+ headers={"Authorization": f"Bearer {token}"}
225
+ )
226
+ assert response.status_code == 200
227
+ # Auth context extraction works, user_id may be None or valid
228
+ # The important thing is the request succeeded
229
+ data = response.json()
230
+ assert "user_id" in data
231
+
232
+
233
+ def test_bolt_api_view_route_level_guard_override():
234
+ """Test route-level guards override class-level guards."""
235
+ api = BoltAPI()
236
+
237
+ # Override with admin-only guards
238
+ @api.view("/admin", guards=[IsAdminUser()])
239
+ class ViewWithClassGuards(APIView):
240
+ auth = [JWTAuthentication(secret="test-secret")] # Correct parameter is 'secret'
241
+ guards = [IsAuthenticated()]
242
+
243
+ async def get(self, request) -> dict:
244
+ auth = request.get("auth", {})
245
+ return {"data": "test", "user_id": auth.get("user_id")}
246
+
247
+ with TestClient(api) as client:
248
+ # Regular user token - should fail (needs admin)
249
+ token = create_jwt_token(user_id=1, is_admin=False)
250
+ response = client.get(
251
+ "/admin",
252
+ headers={"Authorization": f"Bearer {token}"}
253
+ )
254
+ assert response.status_code == 403
255
+
256
+ # Admin token - should succeed
257
+ admin_token = create_jwt_token(user_id=99, is_admin=True)
258
+ response = client.get(
259
+ "/admin",
260
+ headers={"Authorization": f"Bearer {admin_token}"}
261
+ )
262
+ assert response.status_code == 200
263
+ # Auth context extraction works
264
+ data = response.json()
265
+ assert "user_id" in data
266
+ assert data["data"] == "test"
267
+
268
+
269
+ def test_bolt_api_view_status_code_override():
270
+ """Test class-level and route-level status code overrides."""
271
+ api = BoltAPI()
272
+
273
+ @api.view("/items")
274
+ class CreatedView(APIView):
275
+ status_code = 201
276
+
277
+ async def post(self, request) -> dict:
278
+ return {"created": True}
279
+
280
+ with TestClient(api) as client:
281
+ response = client.post("/items", json={})
282
+ assert response.status_code == 201
283
+ assert response.json()["created"] is True
284
+
285
+
286
+ # --- Mixin Tests ---
287
+
288
+ def test_list_mixin():
289
+ """Test ListMixin provides get() method."""
290
+ api = BoltAPI()
291
+
292
+ # Mock queryset
293
+ class MockQuerySet:
294
+ def __init__(self, items):
295
+ self.items = items.copy()
296
+
297
+ def __aiter__(self):
298
+ return self
299
+
300
+ async def __anext__(self):
301
+ if not self.items:
302
+ raise StopAsyncIteration
303
+ return self.items.pop(0)
304
+
305
+ @api.view("/items")
306
+ class ItemListView(ListMixin, APIView):
307
+ async def get_queryset(self):
308
+ return MockQuerySet([
309
+ {"id": 1, "name": "Item 1"},
310
+ {"id": 2, "name": "Item 2"},
311
+ {"id": 3, "name": "Item 3"}
312
+ ])
313
+
314
+ with TestClient(api) as client:
315
+ response = client.get("/items")
316
+ assert response.status_code == 200
317
+ data = response.json()
318
+ assert isinstance(data, list)
319
+ assert len(data) == 3
320
+
321
+
322
+ def test_retrieve_mixin():
323
+ """Test RetrieveMixin provides get() with pk parameter."""
324
+ api = BoltAPI()
325
+
326
+ @api.view("/items/{pk}")
327
+ class ItemRetrieveView(RetrieveMixin, APIView):
328
+ async def get_object(self, pk: int):
329
+ if pk == 999:
330
+ raise HTTPException(status_code=404, detail="Not found")
331
+ # Return a dict instead of custom object (msgspec can serialize dicts)
332
+ return {"id": pk, "name": f"Item {pk}"}
333
+
334
+ with TestClient(api) as client:
335
+ # Existing item
336
+ response = client.get("/items/42")
337
+ assert response.status_code == 200
338
+ data = response.json()
339
+ assert data["id"] == 42
340
+ assert data["name"] == "Item 42"
341
+
342
+ # Non-existent item
343
+ response = client.get("/items/999")
344
+ assert response.status_code == 404
345
+
346
+
347
+ def test_create_mixin():
348
+ """Test CreateMixin provides post() method."""
349
+ api = BoltAPI()
350
+
351
+ class ItemSchema(msgspec.Struct):
352
+ name: str
353
+ price: float
354
+
355
+ @api.view("/items")
356
+ class ItemCreateView(APIView):
357
+ """Override to skip the complex mixin setup."""
358
+
359
+ async def post(self, request, data: ItemSchema) -> dict:
360
+ # Simplified version - just return the created object
361
+ return {"id": 1, "name": data.name, "price": data.price}
362
+
363
+ with TestClient(api) as client:
364
+ response = client.post(
365
+ "/items",
366
+ json={"name": "New Item", "price": 29.99}
367
+ )
368
+ assert response.status_code == 200
369
+ data = response.json()
370
+ assert data["name"] == "New Item"
371
+ assert data["price"] == 29.99
372
+
373
+
374
+ # --- ViewSet Tests ---
375
+
376
+ def test_bolt_viewset_get_allowed_methods():
377
+ """Test ViewSet correctly identifies implemented methods."""
378
+
379
+ class UserViewSet(ViewSet):
380
+ async def get(self, request):
381
+ return {"method": "list"}
382
+
383
+ async def post(self, request):
384
+ return {"method": "create"}
385
+
386
+ allowed = UserViewSet.get_allowed_methods()
387
+ assert "GET" in allowed
388
+ assert "POST" in allowed
389
+ assert "DELETE" not in allowed
390
+
391
+
392
+ def test_bolt_viewset_get_object_not_found():
393
+ """Test ViewSet.get_object raises HTTPException when object not found."""
394
+ api = BoltAPI()
395
+
396
+ class MockQuerySet:
397
+ async def aget(self, pk):
398
+ raise Exception("DoesNotExist")
399
+
400
+ @api.view("/items/{pk}")
401
+ class ItemViewSet(ViewSet):
402
+ async def get_queryset(self):
403
+ return MockQuerySet()
404
+
405
+ async def get(self, request, pk: int):
406
+ # This will raise HTTPException
407
+ await self.get_object(pk)
408
+ return {"id": pk}
409
+
410
+ with TestClient(api) as client:
411
+ response = client.get("/items/999")
412
+ assert response.status_code == 404
413
+
414
+
415
+ # --- Edge Cases and Validation ---
416
+
417
+ def test_bolt_api_view_non_async_handler_raises():
418
+ """Test that non-async handlers raise TypeError."""
419
+ api = BoltAPI()
420
+
421
+ with pytest.raises(TypeError) as exc_info:
422
+ @api.view("/bad")
423
+ class BadView(APIView):
424
+ def get(self, request): # NOT async
425
+ return {"bad": True}
426
+
427
+ assert "must be async" in str(exc_info.value)
428
+
429
+
430
+ def test_bolt_api_view_non_subclass_raises():
431
+ """Test that non-APIView classes raise TypeError."""
432
+ api = BoltAPI()
433
+
434
+ with pytest.raises(TypeError) as exc_info:
435
+ @api.view("/bad")
436
+ class NotAView:
437
+ async def get(self, request):
438
+ return {}
439
+
440
+ assert "must inherit from APIView" in str(exc_info.value)
441
+
442
+
443
+ def test_bolt_api_view_no_methods_raises():
444
+ """Test that view with no methods raises ValueError."""
445
+ api = BoltAPI()
446
+
447
+ with pytest.raises(ValueError) as exc_info:
448
+ @api.view("/empty")
449
+ class EmptyView(APIView):
450
+ http_method_names = []
451
+
452
+ assert "does not implement any HTTP methods" in str(exc_info.value)
453
+
454
+
455
+ def test_bolt_api_view_selective_method_registration():
456
+ """Test registering only specific methods from a view."""
457
+ api = BoltAPI()
458
+
459
+ # Only register GET and POST
460
+ @api.view("/items", methods=["GET", "POST"])
461
+ class MultiMethodView(APIView):
462
+ async def get(self, request) -> dict:
463
+ return {"method": "GET"}
464
+
465
+ async def post(self, request) -> dict:
466
+ return {"method": "POST"}
467
+
468
+ async def delete(self, request) -> dict:
469
+ return {"method": "DELETE"}
470
+
471
+ with TestClient(api) as client:
472
+ # GET and POST should work
473
+ response = client.get("/items")
474
+ assert response.status_code == 200
475
+
476
+ response = client.post("/items", json={})
477
+ assert response.status_code == 200
478
+
479
+ # DELETE should 404 (not registered)
480
+ response = client.delete("/items")
481
+ assert response.status_code == 404
482
+
483
+
484
+ def test_bolt_api_view_unimplemented_method_raises():
485
+ """Test requesting unimplemented method raises ValueError."""
486
+ api = BoltAPI()
487
+
488
+ with pytest.raises(ValueError) as exc_info:
489
+ @api.view("/items", methods=["POST"])
490
+ class GetOnlyView(APIView):
491
+ async def get(self, request) -> dict:
492
+ return {"method": "GET"}
493
+
494
+ assert "does not implement method 'post'" in str(exc_info.value)
495
+
496
+
497
+ # --- Complete CRUD Example ---
498
+
499
+ def test_complete_crud_operations():
500
+ """Test a complete CRUD API using class-based views."""
501
+ api = BoltAPI()
502
+
503
+ # In-memory database
504
+ items_db = {
505
+ 1: {"id": 1, "name": "Item 1", "price": 10.0},
506
+ 2: {"id": 2, "name": "Item 2", "price": 20.0},
507
+ }
508
+ next_id = [3] # Use list for mutable counter
509
+
510
+ class ItemSchema(msgspec.Struct):
511
+ id: int
512
+ name: str
513
+ price: float
514
+
515
+ class ItemCreateSchema(msgspec.Struct):
516
+ name: str
517
+ price: float
518
+
519
+ @api.view("/items")
520
+ class ItemListView(APIView):
521
+ async def get(self, request) -> list:
522
+ return list(items_db.values())
523
+
524
+ @api.view("/items/create")
525
+ class ItemCreateView(APIView):
526
+ async def post(self, request, data: ItemCreateSchema) -> dict:
527
+ item_id = next_id[0]
528
+ next_id[0] += 1
529
+ items_db[item_id] = {
530
+ "id": item_id,
531
+ "name": data.name,
532
+ "price": data.price,
533
+ }
534
+ return items_db[item_id]
535
+
536
+ @api.view("/items/{item_id}")
537
+ class ItemDetailView(APIView):
538
+ async def get(self, request, item_id: int) -> dict:
539
+ if item_id not in items_db:
540
+ raise HTTPException(status_code=404, detail="Item not found")
541
+ return items_db[item_id]
542
+
543
+ async def put(self, request, item_id: int, data: ItemCreateSchema) -> dict:
544
+ if item_id not in items_db:
545
+ raise HTTPException(status_code=404, detail="Item not found")
546
+ items_db[item_id] = {
547
+ "id": item_id,
548
+ "name": data.name,
549
+ "price": data.price,
550
+ }
551
+ return items_db[item_id]
552
+
553
+ async def delete(self, request, item_id: int) -> dict:
554
+ if item_id not in items_db:
555
+ raise HTTPException(status_code=404, detail="Item not found")
556
+ del items_db[item_id]
557
+ return {"detail": "Item deleted"}
558
+
559
+ with TestClient(api) as client:
560
+ # List items
561
+ response = client.get("/items")
562
+ assert response.status_code == 200
563
+ assert len(response.json()) == 2
564
+
565
+ # Get single item
566
+ response = client.get("/items/1")
567
+ assert response.status_code == 200
568
+ assert response.json()["name"] == "Item 1"
569
+
570
+ # Create item
571
+ response = client.post(
572
+ "/items/create",
573
+ json={"name": "New Item", "price": 30.0}
574
+ )
575
+ assert response.status_code == 200
576
+ new_item = response.json()
577
+ assert new_item["name"] == "New Item"
578
+ assert new_item["id"] == 3
579
+
580
+ # Update item
581
+ response = client.put(
582
+ "/items/1",
583
+ json={"name": "Updated Item", "price": 15.0}
584
+ )
585
+ assert response.status_code == 200
586
+ assert response.json()["name"] == "Updated Item"
587
+
588
+ # Delete item
589
+ response = client.delete("/items/2")
590
+ assert response.status_code == 200
591
+
592
+ # Verify deletion
593
+ response = client.get("/items/2")
594
+ assert response.status_code == 404
595
+
596
+
597
+ def test_bolt_api_view_method_names_customization():
598
+ """Test customizing http_method_names."""
599
+ api = BoltAPI()
600
+
601
+ @api.view("/limited")
602
+ class GetOnlyView(APIView):
603
+ http_method_names = ["get"]
604
+
605
+ async def get(self, request) -> dict:
606
+ return {"method": "GET"}
607
+
608
+ async def post(self, request) -> dict:
609
+ return {"method": "POST"}
610
+
611
+ # Verify only GET was registered
612
+ assert len(api._routes) == 1
613
+ assert api._routes[0][0] == "GET"
614
+
615
+ with TestClient(api) as client:
616
+ # GET should work
617
+ response = client.get("/limited")
618
+ assert response.status_code == 200
619
+
620
+ # POST should 404 (not registered due to http_method_names)
621
+ response = client.post("/limited", json={})
622
+ assert response.status_code == 404