fastapi-errors-plus 0.6.1__tar.gz → 0.6.2__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.1 → fastapi_errors_plus-0.6.2}/LICENSE +1 -0
  2. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/PKG-INFO +97 -1
  3. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/README.md +96 -0
  4. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/__init__.py +1 -1
  5. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/base.py +1 -0
  6. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/errors.py +3 -3
  7. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/protocol.py +1 -0
  8. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/PKG-INFO +97 -1
  9. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/pyproject.toml +15 -1
  10. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_app.py +4 -55
  11. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_base.py +5 -0
  12. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_errors.py +114 -15
  13. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_integration.py +21 -7
  14. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/py.typed +0 -0
  15. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/SOURCES.txt +0 -0
  16. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
  17. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/requires.txt +0 -0
  18. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
  19. {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Universal library for documenting errors in FastAPI endpoints
5
5
  Author-email: Igor Selivanov <seligoroff@gmail.com>
6
6
  License: MIT
@@ -335,6 +335,102 @@ Any class implementing this protocol (through structural typing) can be used wit
335
335
 
336
336
  Both approaches work together — you can mix them in the same `Errors()` call!
337
337
 
338
+ ## Using Pydantic with ErrorDTO
339
+
340
+ **Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
341
+
342
+ Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
343
+
344
+ ### Simple Pydantic Model as ErrorDTO
345
+
346
+ ```python
347
+ from pydantic import BaseModel, Field
348
+ from fastapi_errors_plus import Errors
349
+ from typing import Dict, Any
350
+
351
+ class PydanticErrorDTO(BaseModel):
352
+ """Pydantic model implementing ErrorDTO Protocol."""
353
+ status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
354
+ message: str = Field(..., min_length=1, description="Error message")
355
+
356
+ def to_example(self) -> Dict[str, Any]:
357
+ """Generate example for OpenAPI."""
358
+ return {
359
+ self.message: {
360
+ "value": {"detail": self.message},
361
+ },
362
+ }
363
+
364
+ # Usage
365
+ notification_error = PydanticErrorDTO(
366
+ status_code=404,
367
+ message="Notification not found",
368
+ )
369
+
370
+ @router.delete(
371
+ "/{id}",
372
+ responses=Errors(notification_error),
373
+ )
374
+ def delete_item(id: int):
375
+ pass
376
+ ```
377
+
378
+ **Benefits:**
379
+ - Runtime validation through Pydantic
380
+ - Type safety
381
+ - Automatic field documentation
382
+ - Works with ErrorDTO Protocol through structural typing
383
+
384
+ ### Complex ErrorDTO with Pydantic
385
+
386
+ For errors with additional fields:
387
+
388
+ ```python
389
+ from pydantic import BaseModel, Field
390
+ from fastapi_errors_plus import Errors
391
+ from typing import Dict, Any, Optional
392
+
393
+ class DetailedErrorDTO(BaseModel):
394
+ """Pydantic model for errors with additional fields."""
395
+ status_code: int = Field(..., ge=400, le=599)
396
+ message: str = Field(..., min_length=1)
397
+ error_code: Optional[str] = Field(None, description="Internal error code")
398
+ timestamp: Optional[str] = Field(None, description="Error timestamp")
399
+
400
+ def to_example(self) -> Dict[str, Any]:
401
+ """Generate example for OpenAPI."""
402
+ example = {"detail": self.message}
403
+ if self.error_code:
404
+ example["error_code"] = self.error_code
405
+ if self.timestamp:
406
+ example["timestamp"] = self.timestamp
407
+
408
+ return {
409
+ self.message: {
410
+ "value": example,
411
+ },
412
+ }
413
+
414
+ # Usage
415
+ validation_error = DetailedErrorDTO(
416
+ status_code=422,
417
+ message="Validation failed",
418
+ error_code="VALIDATION_ERROR",
419
+ timestamp="2025-01-15T10:30:00Z",
420
+ )
421
+ ```
422
+
423
+ **When to use Pydantic with ErrorDTO:**
424
+ - Your project already uses Pydantic extensively
425
+ - You need runtime validation for error objects
426
+ - You want automatic field documentation
427
+ - You have complex error structures with multiple fields
428
+
429
+ **When not to use Pydantic:**
430
+ - Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
431
+ - You need simple error objects (dataclasses are sufficient)
432
+ - You want to keep dependencies minimal
433
+
338
434
  ## Best Practice: Connecting Exceptions and ErrorDTO
339
435
 
340
436
  ### Problem
@@ -309,6 +309,102 @@ Any class implementing this protocol (through structural typing) can be used wit
309
309
 
310
310
  Both approaches work together — you can mix them in the same `Errors()` call!
311
311
 
312
+ ## Using Pydantic with ErrorDTO
313
+
314
+ **Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
315
+
316
+ Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
317
+
318
+ ### Simple Pydantic Model as ErrorDTO
319
+
320
+ ```python
321
+ from pydantic import BaseModel, Field
322
+ from fastapi_errors_plus import Errors
323
+ from typing import Dict, Any
324
+
325
+ class PydanticErrorDTO(BaseModel):
326
+ """Pydantic model implementing ErrorDTO Protocol."""
327
+ status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
328
+ message: str = Field(..., min_length=1, description="Error message")
329
+
330
+ def to_example(self) -> Dict[str, Any]:
331
+ """Generate example for OpenAPI."""
332
+ return {
333
+ self.message: {
334
+ "value": {"detail": self.message},
335
+ },
336
+ }
337
+
338
+ # Usage
339
+ notification_error = PydanticErrorDTO(
340
+ status_code=404,
341
+ message="Notification not found",
342
+ )
343
+
344
+ @router.delete(
345
+ "/{id}",
346
+ responses=Errors(notification_error),
347
+ )
348
+ def delete_item(id: int):
349
+ pass
350
+ ```
351
+
352
+ **Benefits:**
353
+ - Runtime validation through Pydantic
354
+ - Type safety
355
+ - Automatic field documentation
356
+ - Works with ErrorDTO Protocol through structural typing
357
+
358
+ ### Complex ErrorDTO with Pydantic
359
+
360
+ For errors with additional fields:
361
+
362
+ ```python
363
+ from pydantic import BaseModel, Field
364
+ from fastapi_errors_plus import Errors
365
+ from typing import Dict, Any, Optional
366
+
367
+ class DetailedErrorDTO(BaseModel):
368
+ """Pydantic model for errors with additional fields."""
369
+ status_code: int = Field(..., ge=400, le=599)
370
+ message: str = Field(..., min_length=1)
371
+ error_code: Optional[str] = Field(None, description="Internal error code")
372
+ timestamp: Optional[str] = Field(None, description="Error timestamp")
373
+
374
+ def to_example(self) -> Dict[str, Any]:
375
+ """Generate example for OpenAPI."""
376
+ example = {"detail": self.message}
377
+ if self.error_code:
378
+ example["error_code"] = self.error_code
379
+ if self.timestamp:
380
+ example["timestamp"] = self.timestamp
381
+
382
+ return {
383
+ self.message: {
384
+ "value": example,
385
+ },
386
+ }
387
+
388
+ # Usage
389
+ validation_error = DetailedErrorDTO(
390
+ status_code=422,
391
+ message="Validation failed",
392
+ error_code="VALIDATION_ERROR",
393
+ timestamp="2025-01-15T10:30:00Z",
394
+ )
395
+ ```
396
+
397
+ **When to use Pydantic with ErrorDTO:**
398
+ - Your project already uses Pydantic extensively
399
+ - You need runtime validation for error objects
400
+ - You want automatic field documentation
401
+ - You have complex error structures with multiple fields
402
+
403
+ **When not to use Pydantic:**
404
+ - Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
405
+ - You need simple error objects (dataclasses are sufficient)
406
+ - You want to keep dependencies minimal
407
+
312
408
  ## Best Practice: Connecting Exceptions and ErrorDTO
313
409
 
314
410
  ### Problem
@@ -10,5 +10,5 @@ __all__ = [
10
10
  "BaseErrorDTO", # Base implementation (optional)
11
11
  "StandardErrorDTO", # Extended implementation (optional)
12
12
  ]
13
- __version__ = "0.6.1"
13
+ __version__ = "0.6.2"
14
14
 
@@ -122,3 +122,4 @@ class StandardErrorDTO(BaseErrorDTO):
122
122
 
123
123
 
124
124
 
125
+
@@ -140,7 +140,7 @@ class Errors(Mapping):
140
140
 
141
141
  if add_422:
142
142
  self._add_standard_error(
143
- status.HTTP_422_UNPROCESSABLE_ENTITY,
143
+ status.HTTP_422_UNPROCESSABLE_CONTENT,
144
144
  "Validation Error",
145
145
  {"detail": "Validation error"},
146
146
  )
@@ -239,7 +239,7 @@ class Errors(Mapping):
239
239
  STANDARD_DESCRIPTIONS = {
240
240
  status.HTTP_401_UNAUTHORIZED: "Unauthorized",
241
241
  status.HTTP_403_FORBIDDEN: "Forbidden",
242
- status.HTTP_422_UNPROCESSABLE_ENTITY: "Validation Error",
242
+ status.HTTP_422_UNPROCESSABLE_CONTENT: "Validation Error",
243
243
  status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error",
244
244
  }
245
245
 
@@ -263,7 +263,7 @@ class Errors(Mapping):
263
263
  standard_keys = {
264
264
  status.HTTP_401_UNAUTHORIZED: "StandardUnauthorized",
265
265
  status.HTTP_403_FORBIDDEN: "StandardForbidden",
266
- status.HTTP_422_UNPROCESSABLE_ENTITY: "StandardValidationError",
266
+ status.HTTP_422_UNPROCESSABLE_CONTENT: "StandardValidationError",
267
267
  status.HTTP_500_INTERNAL_SERVER_ERROR: "StandardInternalServerError",
268
268
  }
269
269
  example_key = standard_keys.get(status_code, f"Standard{status_code}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-errors-plus
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Universal library for documenting errors in FastAPI endpoints
5
5
  Author-email: Igor Selivanov <seligoroff@gmail.com>
6
6
  License: MIT
@@ -335,6 +335,102 @@ Any class implementing this protocol (through structural typing) can be used wit
335
335
 
336
336
  Both approaches work together — you can mix them in the same `Errors()` call!
337
337
 
338
+ ## Using Pydantic with ErrorDTO
339
+
340
+ **Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
341
+
342
+ Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
343
+
344
+ ### Simple Pydantic Model as ErrorDTO
345
+
346
+ ```python
347
+ from pydantic import BaseModel, Field
348
+ from fastapi_errors_plus import Errors
349
+ from typing import Dict, Any
350
+
351
+ class PydanticErrorDTO(BaseModel):
352
+ """Pydantic model implementing ErrorDTO Protocol."""
353
+ status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
354
+ message: str = Field(..., min_length=1, description="Error message")
355
+
356
+ def to_example(self) -> Dict[str, Any]:
357
+ """Generate example for OpenAPI."""
358
+ return {
359
+ self.message: {
360
+ "value": {"detail": self.message},
361
+ },
362
+ }
363
+
364
+ # Usage
365
+ notification_error = PydanticErrorDTO(
366
+ status_code=404,
367
+ message="Notification not found",
368
+ )
369
+
370
+ @router.delete(
371
+ "/{id}",
372
+ responses=Errors(notification_error),
373
+ )
374
+ def delete_item(id: int):
375
+ pass
376
+ ```
377
+
378
+ **Benefits:**
379
+ - Runtime validation through Pydantic
380
+ - Type safety
381
+ - Automatic field documentation
382
+ - Works with ErrorDTO Protocol through structural typing
383
+
384
+ ### Complex ErrorDTO with Pydantic
385
+
386
+ For errors with additional fields:
387
+
388
+ ```python
389
+ from pydantic import BaseModel, Field
390
+ from fastapi_errors_plus import Errors
391
+ from typing import Dict, Any, Optional
392
+
393
+ class DetailedErrorDTO(BaseModel):
394
+ """Pydantic model for errors with additional fields."""
395
+ status_code: int = Field(..., ge=400, le=599)
396
+ message: str = Field(..., min_length=1)
397
+ error_code: Optional[str] = Field(None, description="Internal error code")
398
+ timestamp: Optional[str] = Field(None, description="Error timestamp")
399
+
400
+ def to_example(self) -> Dict[str, Any]:
401
+ """Generate example for OpenAPI."""
402
+ example = {"detail": self.message}
403
+ if self.error_code:
404
+ example["error_code"] = self.error_code
405
+ if self.timestamp:
406
+ example["timestamp"] = self.timestamp
407
+
408
+ return {
409
+ self.message: {
410
+ "value": example,
411
+ },
412
+ }
413
+
414
+ # Usage
415
+ validation_error = DetailedErrorDTO(
416
+ status_code=422,
417
+ message="Validation failed",
418
+ error_code="VALIDATION_ERROR",
419
+ timestamp="2025-01-15T10:30:00Z",
420
+ )
421
+ ```
422
+
423
+ **When to use Pydantic with ErrorDTO:**
424
+ - Your project already uses Pydantic extensively
425
+ - You need runtime validation for error objects
426
+ - You want automatic field documentation
427
+ - You have complex error structures with multiple fields
428
+
429
+ **When not to use Pydantic:**
430
+ - Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
431
+ - You need simple error objects (dataclasses are sufficient)
432
+ - You want to keep dependencies minimal
433
+
338
434
  ## Best Practice: Connecting Exceptions and ErrorDTO
339
435
 
340
436
  ### Problem
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-errors-plus"
7
- version = "0.6.1"
7
+ version = "0.6.2"
8
8
  description = "Universal library for documenting errors in FastAPI endpoints"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -45,9 +45,23 @@ testpaths = ["tests"]
45
45
  python_files = "test_*.py"
46
46
  python_classes = "Test*"
47
47
  python_functions = "test_*"
48
+ markers = [
49
+ "unit: Unit tests for individual components",
50
+ "integration: Integration tests with FastAPI application",
51
+ ]
52
+ addopts = [
53
+ "--cov=fastapi_errors_plus",
54
+ "--cov-report=term-missing",
55
+ "--cov-report=html:tests/htmlcov",
56
+ "--cov-report=xml:tests/coverage.xml",
57
+ "--cov-branch",
58
+ "--cov-fail-under=80",
59
+ "-v",
60
+ ]
48
61
 
49
62
  [tool.coverage.run]
50
63
  source = ["fastapi_errors_plus"]
64
+ data_file = "tests/.coverage"
51
65
  omit = [
52
66
  "*/tests/*",
53
67
  "*/__pycache__/*",
@@ -33,7 +33,7 @@ class DomainException(Exception):
33
33
  return cls()
34
34
 
35
35
 
36
- class TestItemNotFoundError(DomainException):
36
+ class MockItemNotFoundError(DomainException):
37
37
  """Test item not found error."""
38
38
  status_code = status.HTTP_404_NOT_FOUND
39
39
  message = "Test item not found"
@@ -48,7 +48,7 @@ class TestItemNotFoundError(DomainException):
48
48
  return cls(item_id="test_id")
49
49
 
50
50
 
51
- class TestItemAccessDeniedError(DomainException):
51
+ class MockItemAccessDeniedError(DomainException):
52
52
  """Test item access denied error."""
53
53
  status_code = status.HTTP_403_FORBIDDEN
54
54
  message = "Test item access denied"
@@ -273,63 +273,12 @@ def create_item_mixed_base_dto(item_id: int):
273
273
  return {"message": f"Item {item_id} created"}
274
274
 
275
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
276
  # Example 11: Domain Exception as ErrorDTO (Best Practice pattern)
328
277
  @router.delete(
329
278
  "/domain-exception/{item_id}",
330
279
  responses=Errors(
331
- TestItemNotFoundError.for_openapi(), # Using for_openapi() pattern
332
- TestItemAccessDeniedError.for_openapi(), # Using for_openapi() pattern
280
+ MockItemNotFoundError.for_openapi(), # Using for_openapi() pattern
281
+ MockItemAccessDeniedError.for_openapi(), # Using for_openapi() pattern
333
282
  unauthorized_401=True, # Standard flag
334
283
  ),
335
284
  )
@@ -6,6 +6,7 @@ from fastapi import status
6
6
  from fastapi_errors_plus import BaseErrorDTO, Errors, StandardErrorDTO
7
7
 
8
8
 
9
+ @pytest.mark.unit
9
10
  class TestBaseErrorDTO:
10
11
  """Tests for BaseErrorDTO class."""
11
12
 
@@ -69,6 +70,7 @@ class TestBaseErrorDTO:
69
70
  assert examples["Item not found"]["value"] == {"detail": "Item not found"}
70
71
 
71
72
 
73
+ @pytest.mark.unit
72
74
  class TestStandardErrorDTO:
73
75
  """Tests for StandardErrorDTO class."""
74
76
 
@@ -197,6 +199,7 @@ class TestStandardErrorDTO:
197
199
  assert "InvalidToken" in examples # From StandardErrorDTO
198
200
 
199
201
 
202
+ @pytest.mark.unit
200
203
  class TestBaseErrorDTOInheritance:
201
204
  """Tests for inheritance from BaseErrorDTO."""
202
205
 
@@ -254,6 +257,7 @@ class TestBaseErrorDTOInheritance:
254
257
  assert "Custom" in examples
255
258
 
256
259
 
260
+ @pytest.mark.unit
257
261
  class TestBaseErrorDTOCompatibility:
258
262
  """Tests for compatibility with structural typing (Protocol)."""
259
263
 
@@ -304,3 +308,4 @@ class TestBaseErrorDTOCompatibility:
304
308
 
305
309
 
306
310
 
311
+
@@ -9,6 +9,7 @@ from fastapi_errors_plus import Errors
9
9
  from tests.conftest import SimpleErrorDTO
10
10
 
11
11
 
12
+ @pytest.mark.unit
12
13
  class TestErrorsStandardFlags:
13
14
  """Tests for standard HTTP status flags."""
14
15
 
@@ -36,8 +37,8 @@ class TestErrorsStandardFlags:
36
37
  errors = Errors(validation_error=True)
37
38
  responses = errors
38
39
 
39
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
40
- assert responses[status.HTTP_422_UNPROCESSABLE_ENTITY]["description"] == "Validation Error"
40
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
41
+ assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation Error"
41
42
 
42
43
  def test_internal_server_error_flag(self):
43
44
  """Test generation of 500 Internal Server Error from flag."""
@@ -68,8 +69,8 @@ class TestErrorsStandardFlags:
68
69
  errors = Errors(validation_error_422=True)
69
70
  responses = errors
70
71
 
71
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
72
- assert responses[status.HTTP_422_UNPROCESSABLE_ENTITY]["description"] == "Validation Error"
72
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
73
+ assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation Error"
73
74
 
74
75
  def test_internal_server_error_500_flag(self):
75
76
  """Test generation of 500 Internal Server Error from explicit flag."""
@@ -108,7 +109,7 @@ class TestErrorsStandardFlags:
108
109
 
109
110
  assert status.HTTP_401_UNAUTHORIZED in responses
110
111
  assert status.HTTP_403_FORBIDDEN in responses
111
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
112
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
112
113
  assert status.HTTP_500_INTERNAL_SERVER_ERROR in responses
113
114
  assert len(responses) == 4
114
115
 
@@ -133,11 +134,12 @@ class TestErrorsStandardFlags:
133
134
 
134
135
  assert status.HTTP_401_UNAUTHORIZED in responses
135
136
  assert status.HTTP_403_FORBIDDEN in responses
136
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
137
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
137
138
  assert status.HTTP_500_INTERNAL_SERVER_ERROR in responses
138
139
  assert len(responses) == 4
139
140
 
140
141
 
142
+ @pytest.mark.unit
141
143
  class TestErrorsDict:
142
144
  """Tests for dict-based errors."""
143
145
 
@@ -214,6 +216,7 @@ class TestErrorsDict:
214
216
  assert "SessionNotFound" in examples
215
217
 
216
218
 
219
+ @pytest.mark.unit
217
220
  class TestErrorsErrorDTO:
218
221
  """Tests for ErrorDTO-based errors."""
219
222
 
@@ -240,6 +243,7 @@ class TestErrorsErrorDTO:
240
243
  assert len(responses) == 2
241
244
 
242
245
 
246
+ @pytest.mark.unit
243
247
  class TestErrorsMergeExamples:
244
248
  """Tests for merging examples for the same status code."""
245
249
 
@@ -296,6 +300,7 @@ class TestErrorsMergeExamples:
296
300
  assert "examples" in content or "example" in content
297
301
 
298
302
 
303
+ @pytest.mark.unit
299
304
  class TestErrorsMixed:
300
305
  """Tests for mixed usage (flags + dict + ErrorDTO)."""
301
306
 
@@ -327,6 +332,7 @@ class TestErrorsMixed:
327
332
  assert len(responses) == 4
328
333
 
329
334
 
335
+ @pytest.mark.unit
330
336
  class TestErrorsEdgeCases:
331
337
  """Tests for edge cases."""
332
338
 
@@ -337,7 +343,7 @@ class TestErrorsEdgeCases:
337
343
 
338
344
  assert isinstance(responses, Mapping)
339
345
  # validation_error=True by default, so 422 should be present
340
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
346
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
341
347
  assert len(responses) == 1
342
348
 
343
349
  def test_validation_error_default_true(self):
@@ -345,15 +351,15 @@ class TestErrorsEdgeCases:
345
351
  errors = Errors()
346
352
  responses = errors
347
353
 
348
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
349
- assert responses[status.HTTP_422_UNPROCESSABLE_ENTITY]["description"] == "Validation Error"
354
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
355
+ assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation Error"
350
356
 
351
357
  def test_validation_error_can_be_disabled(self):
352
358
  """Test that validation_error can be explicitly set to False."""
353
359
  errors = Errors(validation_error=False)
354
360
  responses = errors
355
361
 
356
- assert status.HTTP_422_UNPROCESSABLE_ENTITY not in responses
362
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT not in responses
357
363
  assert len(responses) == 0
358
364
 
359
365
  def test_validation_error_422_default_true(self):
@@ -361,28 +367,28 @@ class TestErrorsEdgeCases:
361
367
  errors = Errors()
362
368
  responses = errors
363
369
 
364
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
370
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
365
371
 
366
372
  def test_validation_error_422_can_be_disabled(self):
367
373
  """Test that validation_error_422 can be explicitly set to False."""
368
374
  errors = Errors(validation_error_422=False)
369
375
  responses = errors
370
376
 
371
- assert status.HTTP_422_UNPROCESSABLE_ENTITY not in responses
377
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT not in responses
372
378
 
373
379
  def test_validation_error_explicit_true_still_works(self):
374
380
  """Test that explicit validation_error=True still works (backward compatibility)."""
375
381
  errors = Errors(validation_error=True)
376
382
  responses = errors
377
383
 
378
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses
384
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
379
385
 
380
386
  def test_validation_error_old_and_new_flags(self):
381
387
  """Test that both old and new validation_error flags work together."""
382
388
  errors = Errors(validation_error=False, validation_error_422=False)
383
389
  responses = errors
384
390
 
385
- assert status.HTTP_422_UNPROCESSABLE_ENTITY not in responses
391
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT not in responses
386
392
 
387
393
  def test_validation_error_with_other_errors(self):
388
394
  """Test validation_error=True by default with other errors."""
@@ -391,7 +397,7 @@ class TestErrorsEdgeCases:
391
397
 
392
398
  assert status.HTTP_401_UNAUTHORIZED in responses
393
399
  assert status.HTTP_403_FORBIDDEN in responses
394
- assert status.HTTP_422_UNPROCESSABLE_ENTITY in responses # Default True
400
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses # Default True
395
401
  assert len(responses) == 3
396
402
 
397
403
  def test_no_flags_no_errors(self):
@@ -446,6 +452,7 @@ class TestErrorsEdgeCases:
446
452
  assert len(errors) == 1
447
453
 
448
454
 
455
+ @pytest.mark.unit
449
456
  class TestUniqueKeys:
450
457
  """Tests for unique key generation to prevent collisions."""
451
458
 
@@ -522,6 +529,7 @@ class TestUniqueKeys:
522
529
  assert unique_key == "CustomExample_4"
523
530
 
524
531
 
532
+ @pytest.mark.unit
525
533
  class TestErrorsValidation:
526
534
  """Tests for ErrorDTO validation."""
527
535
 
@@ -608,6 +616,7 @@ class TestErrorsValidation:
608
616
  assert "BadObject" in error_msg
609
617
 
610
618
 
619
+ @pytest.mark.unit
611
620
  class TestDTODescriptionPriority:
612
621
  """Tests for DTO description priority over standard flags."""
613
622
 
@@ -654,3 +663,93 @@ class TestDTODescriptionPriority:
654
663
  # Dict description should be preserved (dict > DTO)
655
664
  assert errors[401]["description"] == "My Custom Description"
656
665
 
666
+
667
+ @pytest.mark.unit
668
+ class TestPydanticIntegration:
669
+ """Tests for Pydantic model integration with ErrorDTO Protocol.
670
+
671
+ These tests are optional - they will be skipped if Pydantic is not installed.
672
+ This demonstrates that the library works with Pydantic models through structural typing,
673
+ but Pydantic is not a required dependency.
674
+ """
675
+
676
+ def test_pydantic_model_as_error_dto(self):
677
+ """Test that Pydantic models work with ErrorDTO Protocol (with proper skip)."""
678
+ try:
679
+ from pydantic import BaseModel, Field
680
+ from typing import Dict, Any
681
+ except ImportError:
682
+ pytest.skip("Pydantic is not installed - this is expected and OK")
683
+
684
+ class PydanticErrorDTO(BaseModel):
685
+ """Pydantic model implementing ErrorDTO Protocol."""
686
+ status_code: int = Field(..., ge=400, le=599)
687
+ message: str = Field(..., min_length=1)
688
+
689
+ def to_example(self) -> Dict[str, Any]:
690
+ """Generate example for OpenAPI."""
691
+ return {
692
+ self.message: {
693
+ "value": {"detail": self.message},
694
+ },
695
+ }
696
+
697
+ # Create instance
698
+ error = PydanticErrorDTO(
699
+ status_code=404,
700
+ message="Not found",
701
+ )
702
+
703
+ # Use with Errors class
704
+ errors = Errors(error, validation_error=False)
705
+ responses = errors
706
+
707
+ assert status.HTTP_404_NOT_FOUND in responses
708
+ assert responses[status.HTTP_404_NOT_FOUND]["description"] == "Not found"
709
+
710
+ def test_pydantic_model_with_additional_fields(self):
711
+ """Test Pydantic model with additional fields."""
712
+ try:
713
+ from pydantic import BaseModel, Field
714
+ from typing import Dict, Any, Optional
715
+ except ImportError:
716
+ pytest.skip("Pydantic is not installed - this is expected and OK")
717
+
718
+ class DetailedErrorDTO(BaseModel):
719
+ """Pydantic model for errors with additional fields."""
720
+ status_code: int = Field(..., ge=400, le=599)
721
+ message: str = Field(..., min_length=1)
722
+ error_code: Optional[str] = Field(None, description="Internal error code")
723
+
724
+ def to_example(self) -> Dict[str, Any]:
725
+ """Generate example for OpenAPI."""
726
+ example = {"detail": self.message}
727
+ if self.error_code:
728
+ example["error_code"] = self.error_code
729
+
730
+ return {
731
+ self.message: {
732
+ "value": example,
733
+ },
734
+ }
735
+
736
+ # Create instance
737
+ error = DetailedErrorDTO(
738
+ status_code=422,
739
+ message="Validation failed",
740
+ error_code="VALIDATION_ERROR",
741
+ )
742
+
743
+ # Use with Errors class
744
+ errors = Errors(error, validation_error=False)
745
+ responses = errors
746
+
747
+ assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
748
+ assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation failed"
749
+
750
+ # Check that error_code is in the example
751
+ examples = responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["content"]["application/json"]["examples"]
752
+ example_value = examples["Validation failed"]["value"]
753
+ assert example_value["detail"] == "Validation failed"
754
+ assert example_value["error_code"] == "VALIDATION_ERROR"
755
+
@@ -4,8 +4,10 @@ from fastapi import status
4
4
  from fastapi.testclient import TestClient
5
5
 
6
6
  from tests.test_app import app
7
+ import pytest
7
8
 
8
9
 
10
+ @pytest.mark.integration
9
11
  class TestOpenAPIGeneration:
10
12
  """Tests for OpenAPI specification generation."""
11
13
 
@@ -39,6 +41,7 @@ class TestOpenAPIGeneration:
39
41
  assert "/api/v1/mixed-base-dto/{item_id}" in paths
40
42
 
41
43
 
44
+ @pytest.mark.integration
42
45
  class TestStandardFlagsEndpoint:
43
46
  """Tests for standard flags endpoint."""
44
47
 
@@ -71,6 +74,7 @@ class TestStandardFlagsEndpoint:
71
74
  assert "example" in error_401["content"]["application/json"]
72
75
 
73
76
 
77
+ @pytest.mark.integration
74
78
  class TestDictErrorsEndpoint:
75
79
  """Tests for dict errors endpoint."""
76
80
 
@@ -100,6 +104,7 @@ class TestDictErrorsEndpoint:
100
104
  assert "application/json" in error_404["content"]
101
105
 
102
106
 
107
+ @pytest.mark.integration
103
108
  class TestErrorDTOEndpoint:
104
109
  """Tests for ErrorDTO endpoint."""
105
110
 
@@ -129,6 +134,7 @@ class TestErrorDTOEndpoint:
129
134
  assert "examples" in error_404["content"]["application/json"]
130
135
 
131
136
 
137
+ @pytest.mark.integration
132
138
  class TestMixedEndpoint:
133
139
  """Tests for mixed endpoint (flags + dict + ErrorDTO)."""
134
140
 
@@ -166,6 +172,7 @@ class TestMixedEndpoint:
166
172
  assert responses["409"]["description"] == "Conflict"
167
173
 
168
174
 
175
+ @pytest.mark.integration
169
176
  class TestMergeExamplesEndpoint:
170
177
  """Tests for merge examples endpoint."""
171
178
 
@@ -187,6 +194,7 @@ class TestMergeExamplesEndpoint:
187
194
  assert "Error 2" in examples
188
195
 
189
196
 
197
+ @pytest.mark.integration
190
198
  class TestEmptyErrorsEndpoint:
191
199
  """Tests for empty errors endpoint."""
192
200
 
@@ -206,6 +214,7 @@ class TestEmptyErrorsEndpoint:
206
214
  assert len(error_codes) == 0
207
215
 
208
216
 
217
+ @pytest.mark.integration
209
218
  class TestMergeFlagDictEndpoint:
210
219
  """Tests for merge flag and dict endpoint."""
211
220
 
@@ -226,6 +235,7 @@ class TestMergeFlagDictEndpoint:
226
235
  assert "SessionNotFound" in examples
227
236
 
228
237
 
238
+ @pytest.mark.integration
229
239
  class TestBaseErrorDTOEndpoint:
230
240
  """Tests for BaseErrorDTO endpoint."""
231
241
 
@@ -257,6 +267,7 @@ class TestBaseErrorDTOEndpoint:
257
267
  assert "Item not found" in examples
258
268
 
259
269
 
270
+ @pytest.mark.integration
260
271
  class TestStandardErrorDTOEndpoint:
261
272
  """Tests for StandardErrorDTO endpoint."""
262
273
 
@@ -301,6 +312,7 @@ class TestStandardErrorDTOEndpoint:
301
312
  assert "RoleHasNoAccess" in examples
302
313
 
303
314
 
315
+ @pytest.mark.integration
304
316
  class TestMixedBaseDTOEndpoint:
305
317
  """Tests for mixed BaseErrorDTO + StandardErrorDTO + flags endpoint."""
306
318
 
@@ -319,6 +331,7 @@ class TestMixedBaseDTOEndpoint:
319
331
  assert "500" in responses # From flag
320
332
 
321
333
 
334
+ @pytest.mark.integration
322
335
  class TestDomainExceptionEndpoint:
323
336
  """Tests for Domain Exception as ErrorDTO endpoint (Best Practice pattern)."""
324
337
 
@@ -332,8 +345,8 @@ class TestDomainExceptionEndpoint:
332
345
  responses = endpoint["responses"]
333
346
 
334
347
  assert "401" in responses # From flag
335
- assert "403" in responses # From TestItemAccessDeniedError.for_openapi()
336
- assert "404" in responses # From TestItemNotFoundError.for_openapi()
348
+ assert "403" in responses # From MockItemAccessDeniedError.for_openapi()
349
+ assert "404" in responses # From MockItemNotFoundError.for_openapi()
337
350
 
338
351
  def test_404_response_structure_from_domain_exception(self):
339
352
  """Test 404 response structure from Domain Exception."""
@@ -369,9 +382,9 @@ class TestDomainExceptionEndpoint:
369
382
 
370
383
  def test_for_openapi_method_returns_error_dto(self):
371
384
  """Test that for_openapi() method returns instance implementing ErrorDTO."""
372
- from tests.test_app import TestItemNotFoundError
385
+ from tests.test_app import MockItemNotFoundError
373
386
 
374
- error_instance = TestItemNotFoundError.for_openapi()
387
+ error_instance = MockItemNotFoundError.for_openapi()
375
388
 
376
389
  # Check that it implements ErrorDTO protocol
377
390
  assert hasattr(error_instance, "status_code")
@@ -392,11 +405,11 @@ class TestDomainExceptionEndpoint:
392
405
  def test_domain_exception_works_with_errors_class(self):
393
406
  """Test that Domain Exception instances work with Errors class."""
394
407
  from fastapi_errors_plus import Errors
395
- from tests.test_app import TestItemNotFoundError, TestItemAccessDeniedError
408
+ from tests.test_app import MockItemNotFoundError, MockItemAccessDeniedError
396
409
 
397
410
  errors = Errors(
398
- TestItemNotFoundError.for_openapi(),
399
- TestItemAccessDeniedError.for_openapi(),
411
+ MockItemNotFoundError.for_openapi(),
412
+ MockItemAccessDeniedError.for_openapi(),
400
413
  )
401
414
  responses = errors
402
415
 
@@ -406,6 +419,7 @@ class TestDomainExceptionEndpoint:
406
419
  assert responses[403]["description"] == "Test item access denied"
407
420
 
408
421
 
422
+ @pytest.mark.integration
409
423
  class TestRealEndpoints:
410
424
  """Tests for actual endpoint responses."""
411
425