fastapi-errors-plus 0.6.0__tar.gz → 0.6.1__tar.gz

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.
Files changed (19) hide show
  1. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/PKG-INFO +1 -1
  2. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/__init__.py +1 -1
  3. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/PKG-INFO +1 -1
  4. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/pyproject.toml +1 -1
  5. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/tests/test_app.py +118 -0
  6. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/tests/test_integration.py +95 -0
  7. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/LICENSE +0 -0
  8. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/README.md +0 -0
  9. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/base.py +0 -0
  10. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/errors.py +0 -0
  11. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/protocol.py +0 -0
  12. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus/py.typed +0 -0
  13. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/SOURCES.txt +0 -0
  14. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
  15. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/requires.txt +0 -0
  16. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
  17. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/setup.cfg +0 -0
  18. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/tests/test_base.py +0 -0
  19. {fastapi_errors_plus-0.6.0 → fastapi_errors_plus-0.6.1}/tests/test_errors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Universal library for documenting errors in FastAPI endpoints
5
5
  Author-email: Igor Selivanov <seligoroff@gmail.com>
6
6
  License: MIT
@@ -10,5 +10,5 @@ __all__ = [
10
10
  "BaseErrorDTO", # Base implementation (optional)
11
11
  "StandardErrorDTO", # Extended implementation (optional)
12
12
  ]
13
- __version__ = "0.6.0"
13
+ __version__ = "0.6.1"
14
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Universal library for documenting errors in FastAPI endpoints
5
5
  Author-email: Igor Selivanov <seligoroff@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-errors-plus"
7
- version = "0.6.0"
7
+ version = "0.6.1"
8
8
  description = "Universal library for documenting errors in FastAPI endpoints"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,5 +1,7 @@
1
1
  """Test FastAPI application for integration testing."""
2
2
 
3
+ from typing import Dict, Any
4
+
3
5
  from fastapi import APIRouter, FastAPI, status
4
6
  from fastapi_errors_plus import BaseErrorDTO, Errors, StandardErrorDTO
5
7
  from tests.conftest import SimpleErrorDTO
@@ -11,6 +13,57 @@ app = FastAPI(title="Test API for fastapi-errors-plus")
11
13
  router = APIRouter()
12
14
 
13
15
 
16
+ # Domain Exception pattern for testing (Best Practice example)
17
+ class DomainException(Exception):
18
+ """Base exception implementing ErrorDTO protocol."""
19
+ status_code: int
20
+ message: str
21
+
22
+ def to_example(self) -> Dict[str, Any]:
23
+ """Generate example for OpenAPI."""
24
+ return {
25
+ self.message: {
26
+ "value": {"detail": self.message},
27
+ },
28
+ }
29
+
30
+ @classmethod
31
+ def for_openapi(cls):
32
+ """Returns instance for OpenAPI documentation."""
33
+ return cls()
34
+
35
+
36
+ class TestItemNotFoundError(DomainException):
37
+ """Test item not found error."""
38
+ status_code = status.HTTP_404_NOT_FOUND
39
+ message = "Test item not found"
40
+
41
+ def __init__(self, item_id: str = ""):
42
+ self.item_id = item_id
43
+ super().__init__(self.message)
44
+
45
+ @classmethod
46
+ def for_openapi(cls):
47
+ """Returns instance for OpenAPI documentation."""
48
+ return cls(item_id="test_id")
49
+
50
+
51
+ class TestItemAccessDeniedError(DomainException):
52
+ """Test item access denied error."""
53
+ status_code = status.HTTP_403_FORBIDDEN
54
+ message = "Test item access denied"
55
+
56
+ def __init__(self, item_id: str = "", user_id: str = ""):
57
+ self.item_id = item_id
58
+ self.user_id = user_id
59
+ super().__init__(self.message)
60
+
61
+ @classmethod
62
+ def for_openapi(cls):
63
+ """Returns instance for OpenAPI documentation."""
64
+ return cls(item_id="test_id", user_id="test_user")
65
+
66
+
14
67
  # Example 1: Standard flags only
15
68
  @router.get(
16
69
  "/standard-flags",
@@ -220,6 +273,71 @@ def create_item_mixed_base_dto(item_id: int):
220
273
  return {"message": f"Item {item_id} created"}
221
274
 
222
275
 
276
+ # Domain Exception pattern for testing (Best Practice example)
277
+ class DomainException(Exception):
278
+ """Base exception implementing ErrorDTO protocol."""
279
+ status_code: int
280
+ message: str
281
+
282
+ def to_example(self) -> Dict[str, Any]:
283
+ """Generate example for OpenAPI."""
284
+ return {
285
+ self.message: {
286
+ "value": {"detail": self.message},
287
+ },
288
+ }
289
+
290
+ @classmethod
291
+ def for_openapi(cls):
292
+ """Returns instance for OpenAPI documentation."""
293
+ return cls()
294
+
295
+
296
+ class TestItemNotFoundError(DomainException):
297
+ """Test item not found error."""
298
+ status_code = status.HTTP_404_NOT_FOUND
299
+ message = "Test item not found"
300
+
301
+ def __init__(self, item_id: str = ""):
302
+ self.item_id = item_id
303
+ super().__init__(self.message)
304
+
305
+ @classmethod
306
+ def for_openapi(cls):
307
+ """Returns instance for OpenAPI documentation."""
308
+ return cls(item_id="test_id")
309
+
310
+
311
+ class TestItemAccessDeniedError(DomainException):
312
+ """Test item access denied error."""
313
+ status_code = status.HTTP_403_FORBIDDEN
314
+ message = "Test item access denied"
315
+
316
+ def __init__(self, item_id: str = "", user_id: str = ""):
317
+ self.item_id = item_id
318
+ self.user_id = user_id
319
+ super().__init__(self.message)
320
+
321
+ @classmethod
322
+ def for_openapi(cls):
323
+ """Returns instance for OpenAPI documentation."""
324
+ return cls(item_id="test_id", user_id="test_user")
325
+
326
+
327
+ # Example 11: Domain Exception as ErrorDTO (Best Practice pattern)
328
+ @router.delete(
329
+ "/domain-exception/{item_id}",
330
+ responses=Errors(
331
+ TestItemNotFoundError.for_openapi(), # Using for_openapi() pattern
332
+ TestItemAccessDeniedError.for_openapi(), # Using for_openapi() pattern
333
+ unauthorized_401=True, # Standard flag
334
+ ),
335
+ )
336
+ def delete_item_domain_exception(item_id: str):
337
+ """Endpoint demonstrating Domain Exception as ErrorDTO pattern."""
338
+ return {"message": f"Item {item_id} deleted"}
339
+
340
+
223
341
  # Register router
224
342
  app.include_router(router, prefix="/api/v1", tags=["test"])
225
343
 
@@ -319,6 +319,93 @@ class TestMixedBaseDTOEndpoint:
319
319
  assert "500" in responses # From flag
320
320
 
321
321
 
322
+ class TestDomainExceptionEndpoint:
323
+ """Tests for Domain Exception as ErrorDTO endpoint (Best Practice pattern)."""
324
+
325
+ def test_responses_in_openapi(self):
326
+ """Test that Domain Exception responses are in OpenAPI."""
327
+ client = TestClient(app)
328
+ response = client.get("/openapi.json")
329
+ schema = response.json()
330
+
331
+ endpoint = schema["paths"]["/api/v1/domain-exception/{item_id}"]["delete"]
332
+ responses = endpoint["responses"]
333
+
334
+ assert "401" in responses # From flag
335
+ assert "403" in responses # From TestItemAccessDeniedError.for_openapi()
336
+ assert "404" in responses # From TestItemNotFoundError.for_openapi()
337
+
338
+ def test_404_response_structure_from_domain_exception(self):
339
+ """Test 404 response structure from Domain Exception."""
340
+ client = TestClient(app)
341
+ response = client.get("/openapi.json")
342
+ schema = response.json()
343
+
344
+ endpoint = schema["paths"]["/api/v1/domain-exception/{item_id}"]["delete"]
345
+ error_404 = endpoint["responses"]["404"]
346
+
347
+ assert error_404["description"] == "Test item not found"
348
+ assert "content" in error_404
349
+ assert "application/json" in error_404["content"]
350
+ assert "examples" in error_404["content"]["application/json"]
351
+ examples = error_404["content"]["application/json"]["examples"]
352
+ assert "Test item not found" in examples
353
+
354
+ def test_403_response_structure_from_domain_exception(self):
355
+ """Test 403 response structure from Domain Exception."""
356
+ client = TestClient(app)
357
+ response = client.get("/openapi.json")
358
+ schema = response.json()
359
+
360
+ endpoint = schema["paths"]["/api/v1/domain-exception/{item_id}"]["delete"]
361
+ error_403 = endpoint["responses"]["403"]
362
+
363
+ assert error_403["description"] == "Test item access denied"
364
+ assert "content" in error_403
365
+ assert "application/json" in error_403["content"]
366
+ assert "examples" in error_403["content"]["application/json"]
367
+ examples = error_403["content"]["application/json"]["examples"]
368
+ assert "Test item access denied" in examples
369
+
370
+ def test_for_openapi_method_returns_error_dto(self):
371
+ """Test that for_openapi() method returns instance implementing ErrorDTO."""
372
+ from tests.test_app import TestItemNotFoundError
373
+
374
+ error_instance = TestItemNotFoundError.for_openapi()
375
+
376
+ # Check that it implements ErrorDTO protocol
377
+ assert hasattr(error_instance, "status_code")
378
+ assert hasattr(error_instance, "message")
379
+ assert hasattr(error_instance, "to_example")
380
+ assert callable(error_instance.to_example)
381
+
382
+ # Check values
383
+ assert error_instance.status_code == 404
384
+ assert error_instance.message == "Test item not found"
385
+ assert error_instance.item_id == "test_id"
386
+
387
+ # Check to_example() works
388
+ example = error_instance.to_example()
389
+ assert isinstance(example, dict)
390
+ assert "Test item not found" in example
391
+
392
+ def test_domain_exception_works_with_errors_class(self):
393
+ """Test that Domain Exception instances work with Errors class."""
394
+ from fastapi_errors_plus import Errors
395
+ from tests.test_app import TestItemNotFoundError, TestItemAccessDeniedError
396
+
397
+ errors = Errors(
398
+ TestItemNotFoundError.for_openapi(),
399
+ TestItemAccessDeniedError.for_openapi(),
400
+ )
401
+ responses = errors
402
+
403
+ assert 404 in responses
404
+ assert 403 in responses
405
+ assert responses[404]["description"] == "Test item not found"
406
+ assert responses[403]["description"] == "Test item access denied"
407
+
408
+
322
409
  class TestRealEndpoints:
323
410
  """Tests for actual endpoint responses."""
324
411
 
@@ -361,5 +448,13 @@ class TestRealEndpoints:
361
448
 
362
449
  assert response.status_code == 200
363
450
  assert response.json() == {"message": "Item 1 deleted"}
451
+
452
+ def test_domain_exception_endpoint_works(self):
453
+ """Test that Domain Exception endpoint returns 200."""
454
+ client = TestClient(app)
455
+ response = client.delete("/api/v1/domain-exception/1")
456
+
457
+ assert response.status_code == 200
458
+ assert response.json() == {"message": "Item 1 deleted"}
364
459
 
365
460