contentgrid-extension-helpers 0.0.1__tar.gz → 0.0.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.
- {contentgrid_extension_helpers-0.0.1/src/contentgrid_extension_helpers.egg-info → contentgrid_extension_helpers-0.0.2}/PKG-INFO +2 -1
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/pyproject.toml +2 -1
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/requirements.txt +2 -1
- contentgrid_extension_helpers-0.0.2/src/contentgrid_extension_helpers/__init__.py +3 -0
- contentgrid_extension_helpers-0.0.2/src/contentgrid_extension_helpers/authentication/__init__.py +2 -0
- contentgrid_extension_helpers-0.0.2/src/contentgrid_extension_helpers/exceptions.py +48 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers/logging/__init__.py +1 -1
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers/logging/json_logging.py +2 -2
- contentgrid_extension_helpers-0.0.2/src/contentgrid_extension_helpers/middleware/exception_middleware.py +124 -0
- contentgrid_extension_helpers-0.0.2/src/contentgrid_extension_helpers/problem_response.py +45 -0
- contentgrid_extension_helpers-0.0.2/src/contentgrid_extension_helpers/structured_output/model_deny.py +45 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2/src/contentgrid_extension_helpers.egg-info}/PKG-INFO +2 -1
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers.egg-info/SOURCES.txt +5 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers.egg-info/requires.txt +1 -0
- contentgrid_extension_helpers-0.0.2/tests/test_exception_middleware.py +225 -0
- contentgrid_extension_helpers-0.0.1/src/contentgrid_extension_helpers/__init__.py +0 -0
- contentgrid_extension_helpers-0.0.1/src/contentgrid_extension_helpers/authentication/__init__.py +0 -2
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/LICENSE +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/README.md +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/pytest.ini +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/setup.cfg +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/setup.py +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers/authentication/oidc.py +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers/authentication/user.py +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers.egg-info/dependency_links.txt +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/src/contentgrid_extension_helpers.egg-info/top_level.txt +0 -0
- {contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/tests/test_logging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: contentgrid-extension-helpers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Helper functions for contentgrid extensions.
|
|
5
5
|
Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
|
|
6
6
|
License: Copyright 2024 Xenit Solutions
|
|
@@ -26,6 +26,7 @@ Requires-Dist: requests<3,>=2.20.0
|
|
|
26
26
|
Requires-Dist: uri-template<2
|
|
27
27
|
Requires-Dist: fastapi>=0.111
|
|
28
28
|
Requires-Dist: PyJWT>2
|
|
29
|
+
Requires-Dist: contentgrid_hal_client>=0.0.10
|
|
29
30
|
|
|
30
31
|
### ContentGrid-Extension-Helpers
|
|
31
32
|
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
from .exceptions import LLMDenyException, ExtensionHelperException, IllegalActivityError, MissingInputError, InjectionError, MalformedInputError, NotRelatedError, SensitiveInformationError, SecurityError # noqa: F401
|
|
2
|
+
from .middleware.exception_middleware import catch_exceptions_middleware # noqa: F401
|
|
3
|
+
from .structured_output.model_deny import DenyReason, ModelDeny # noqa: F401
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
class ExtensionHelperException(Exception):
|
|
3
|
+
"""Base class for all custom exceptions in the extension helpers library."""
|
|
4
|
+
def __init__(self, *args: object) -> None:
|
|
5
|
+
super().__init__(*args)
|
|
6
|
+
|
|
7
|
+
class LLMDenyException(ExtensionHelperException):
|
|
8
|
+
"""
|
|
9
|
+
Raised when the LLM denies a request based on its rules.
|
|
10
|
+
This is a base class for more specific denial reasons.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, *args: object) -> None:
|
|
13
|
+
super().__init__(*args)
|
|
14
|
+
|
|
15
|
+
class NotRelatedError(LLMDenyException):
|
|
16
|
+
"""Raised when the input is unrelated to the required extraction."""
|
|
17
|
+
def __init__(self, *args: object) -> None:
|
|
18
|
+
super().__init__(*args)
|
|
19
|
+
|
|
20
|
+
class MissingInputError(LLMDenyException):
|
|
21
|
+
"""Raised when required input is missing or empty."""
|
|
22
|
+
def __init__(self, *args: object) -> None:
|
|
23
|
+
super().__init__(*args)
|
|
24
|
+
|
|
25
|
+
class MalformedInputError(LLMDenyException):
|
|
26
|
+
"""Raised when input is present but malformed or contains only strange characters."""
|
|
27
|
+
def __init__(self, *args: object) -> None:
|
|
28
|
+
super().__init__(*args)
|
|
29
|
+
|
|
30
|
+
class IllegalActivityError(LLMDenyException):
|
|
31
|
+
"""Raised when the input describes illegal activities."""
|
|
32
|
+
def __init__(self, *args: object) -> None:
|
|
33
|
+
super().__init__(*args)
|
|
34
|
+
|
|
35
|
+
class SensitiveInformationError(LLMDenyException):
|
|
36
|
+
"""Raised when the input contains sensitive information."""
|
|
37
|
+
def __init__(self, *args: object) -> None:
|
|
38
|
+
super().__init__(*args)
|
|
39
|
+
|
|
40
|
+
class SecurityError(LLMDenyException):
|
|
41
|
+
"""Raised when the input attempts to compromise system security."""
|
|
42
|
+
def __init__(self, *args: object) -> None:
|
|
43
|
+
super().__init__(*args)
|
|
44
|
+
|
|
45
|
+
class InjectionError(SecurityError):
|
|
46
|
+
"""Raised when the input is a prompt injection attack."""
|
|
47
|
+
def __init__(self, *args: object) -> None:
|
|
48
|
+
super().__init__(*args)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
from .json_logging import setup_json_logging, XenitJsonFormatter
|
|
1
|
+
from .json_logging import setup_json_logging, XenitJsonFormatter # noqa: F401
|
|
@@ -2,7 +2,7 @@ import datetime
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Optional, Any, Dict
|
|
6
6
|
|
|
7
7
|
class XenitJsonFormatter(logging.Formatter):
|
|
8
8
|
def __init__(
|
|
@@ -54,7 +54,7 @@ class XenitJsonFormatter(logging.Formatter):
|
|
|
54
54
|
|
|
55
55
|
def setup_json_logging(
|
|
56
56
|
component: str = "cg-extension",
|
|
57
|
-
additional_keys: dict[str,
|
|
57
|
+
additional_keys: dict[str,str] = {},
|
|
58
58
|
log_level: int = logging.DEBUG
|
|
59
59
|
) -> logging.Logger:
|
|
60
60
|
"""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from fastapi import Request, status
|
|
2
|
+
import logging
|
|
3
|
+
from contentgrid_extension_helpers.problem_response import ProblemResponse
|
|
4
|
+
from contentgrid_hal_client.exceptions import (
|
|
5
|
+
NotFound,
|
|
6
|
+
Unauthorized,
|
|
7
|
+
BadRequest,
|
|
8
|
+
IncorrectAttributeType,
|
|
9
|
+
NonExistantAttribute,
|
|
10
|
+
MissingRequiredAttribute,
|
|
11
|
+
MissingHALTemplate,
|
|
12
|
+
)
|
|
13
|
+
from requests.exceptions import HTTPError
|
|
14
|
+
from contentgrid_extension_helpers.exceptions import LLMDenyException
|
|
15
|
+
|
|
16
|
+
async def catch_exceptions_middleware(request: Request, call_next, problem_base_url: str = "https://docs.contentgrid.com"):
|
|
17
|
+
"""
|
|
18
|
+
Catches exceptions and returns ProblemResponse objects.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
request: The incoming request.
|
|
22
|
+
call_next: The next middleware or route handler.
|
|
23
|
+
problem_base_url: The base URL for problem type URIs.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def format_type(type_name: str) -> str:
|
|
27
|
+
"""Formats problem type URIs."""
|
|
28
|
+
return f"{problem_base_url}/{type_name}"
|
|
29
|
+
|
|
30
|
+
cors_headers = {
|
|
31
|
+
"Access-Control-Allow-Origin": "*",
|
|
32
|
+
"Access-Control-Allow-Methods": "*",
|
|
33
|
+
"Access-Control-Allow-Headers": "*, Authorization",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
return await call_next(request)
|
|
38
|
+
except LLMDenyException as e:
|
|
39
|
+
logging.error(f"LLM denied request: {e}")
|
|
40
|
+
return ProblemResponse(
|
|
41
|
+
title="Request Denied",
|
|
42
|
+
problem_type=format_type("request-denied"),
|
|
43
|
+
detail=str(e),
|
|
44
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
45
|
+
headers=cors_headers,
|
|
46
|
+
)
|
|
47
|
+
except NotFound:
|
|
48
|
+
return ProblemResponse(
|
|
49
|
+
title="Not found",
|
|
50
|
+
problem_type=format_type("not-found"),
|
|
51
|
+
detail="Resource not found",
|
|
52
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
53
|
+
headers=cors_headers,
|
|
54
|
+
)
|
|
55
|
+
except Unauthorized:
|
|
56
|
+
return ProblemResponse(
|
|
57
|
+
title="Unauthorized",
|
|
58
|
+
problem_type=format_type("unauthorized"),
|
|
59
|
+
detail="user is not authorized for requested resource",
|
|
60
|
+
status=status.HTTP_401_UNAUTHORIZED,
|
|
61
|
+
headers=cors_headers,
|
|
62
|
+
)
|
|
63
|
+
except BadRequest:
|
|
64
|
+
return ProblemResponse(
|
|
65
|
+
title="Bad Request",
|
|
66
|
+
problem_type=format_type("bad-request"),
|
|
67
|
+
detail="The request was malformed or invalid.",
|
|
68
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
69
|
+
headers=cors_headers,
|
|
70
|
+
)
|
|
71
|
+
except IncorrectAttributeType as e:
|
|
72
|
+
return ProblemResponse(
|
|
73
|
+
title="Incorrect Attribute Type",
|
|
74
|
+
problem_type=format_type("incorrect-attribute-type"),
|
|
75
|
+
detail=str(e),
|
|
76
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
77
|
+
headers=cors_headers,
|
|
78
|
+
)
|
|
79
|
+
except NonExistantAttribute as e:
|
|
80
|
+
return ProblemResponse(
|
|
81
|
+
title="Non-Existent Attribute",
|
|
82
|
+
problem_type=format_type("non-existent-attribute"),
|
|
83
|
+
detail=str(e),
|
|
84
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
85
|
+
headers=cors_headers,
|
|
86
|
+
)
|
|
87
|
+
except MissingRequiredAttribute as e:
|
|
88
|
+
return ProblemResponse(
|
|
89
|
+
title="Missing Required Attribute",
|
|
90
|
+
problem_type=format_type("missing-required-attribute"),
|
|
91
|
+
detail=str(e),
|
|
92
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
93
|
+
headers=cors_headers,
|
|
94
|
+
)
|
|
95
|
+
except MissingHALTemplate as e:
|
|
96
|
+
return ProblemResponse(
|
|
97
|
+
title="Missing HAL Template",
|
|
98
|
+
problem_type=format_type("missing-hal-template"),
|
|
99
|
+
detail=str(e),
|
|
100
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
101
|
+
headers=cors_headers,
|
|
102
|
+
)
|
|
103
|
+
except HTTPError as e:
|
|
104
|
+
logging.exception(f"HTTP Error: {str(e)}", exc_info=True)
|
|
105
|
+
return ProblemResponse(
|
|
106
|
+
title="HTTP Error",
|
|
107
|
+
problem_type=format_type("http-error"),
|
|
108
|
+
detail=f"An HTTP error occurred: {str(e)}",
|
|
109
|
+
status=e.response.status_code if e.response else status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
110
|
+
headers=cors_headers,
|
|
111
|
+
)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logging.exception(
|
|
114
|
+
f"Untyped exception caught in backend request...: {str(e)}",
|
|
115
|
+
exc_info=True,
|
|
116
|
+
stack_info=True,
|
|
117
|
+
)
|
|
118
|
+
return ProblemResponse(
|
|
119
|
+
title="Internal server error",
|
|
120
|
+
problem_type=format_type("unknown"),
|
|
121
|
+
detail=f"An unexpected error occurred: {str(e)}",
|
|
122
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
123
|
+
headers=cors_headers,
|
|
124
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Dict, Mapping
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_problem_dict(title: str | None, problem_type: str | None, status: int | None, detail: str | None, instance: str | None, **kwargs) -> Dict[str, Any]:
|
|
6
|
+
result: Dict[str, Any] = dict()
|
|
7
|
+
if title is not None:
|
|
8
|
+
result["title"] = title
|
|
9
|
+
if problem_type is not None:
|
|
10
|
+
result["type"] = problem_type
|
|
11
|
+
if status is not None:
|
|
12
|
+
result["status"] = status
|
|
13
|
+
if detail is not None:
|
|
14
|
+
result["detail"] = detail
|
|
15
|
+
if instance is not None:
|
|
16
|
+
result["instance"] = instance
|
|
17
|
+
|
|
18
|
+
for key, value in kwargs.items():
|
|
19
|
+
if key == "type":
|
|
20
|
+
raise ValueError(f"{key} is a reserved property name!")
|
|
21
|
+
result[key] = value
|
|
22
|
+
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProblemResponse(JSONResponse):
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
title: str | None = None,
|
|
30
|
+
problem_type: str | None = "about:blank", # not 'type' because it is a built-in function
|
|
31
|
+
status: int = 400,
|
|
32
|
+
detail: str | None = None,
|
|
33
|
+
instance: str | None = None,
|
|
34
|
+
headers: Mapping[str, str] | None = None,
|
|
35
|
+
media_type: str | None = "application/problem+json",
|
|
36
|
+
background: Any | None = None,
|
|
37
|
+
**kwargs
|
|
38
|
+
):
|
|
39
|
+
super().__init__(
|
|
40
|
+
get_problem_dict(title, problem_type, status, detail, instance, **kwargs),
|
|
41
|
+
status_code=status,
|
|
42
|
+
headers=headers,
|
|
43
|
+
media_type=media_type,
|
|
44
|
+
background=background
|
|
45
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
from contentgrid_extension_helpers import exceptions
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DenyReason(Enum):
|
|
8
|
+
NOT_RELATED = "not_related"
|
|
9
|
+
MISSING_INPUT = "missing_input"
|
|
10
|
+
MALFORMED_INPUT = "malformed_input"
|
|
11
|
+
ILLEGAL_ACTIVITY = "illegal_activity"
|
|
12
|
+
SENSITIVE_INFORMATION = "sensitive_information"
|
|
13
|
+
SECURITY_ERROR = "security_error"
|
|
14
|
+
INJECTION_ATTEMPT = "injection_attempt"
|
|
15
|
+
|
|
16
|
+
def to_exception(self) -> type[exceptions.LLMDenyException]:
|
|
17
|
+
"""Maps a DenyReason to its corresponding exception class."""
|
|
18
|
+
match self:
|
|
19
|
+
case DenyReason.NOT_RELATED:
|
|
20
|
+
return exceptions.NotRelatedError
|
|
21
|
+
case DenyReason.MISSING_INPUT:
|
|
22
|
+
return exceptions.MissingInputError
|
|
23
|
+
case DenyReason.MALFORMED_INPUT:
|
|
24
|
+
return exceptions.MalformedInputError
|
|
25
|
+
case DenyReason.ILLEGAL_ACTIVITY:
|
|
26
|
+
return exceptions.IllegalActivityError
|
|
27
|
+
case DenyReason.SENSITIVE_INFORMATION:
|
|
28
|
+
return exceptions.SensitiveInformationError
|
|
29
|
+
case DenyReason.SECURITY_ERROR:
|
|
30
|
+
return exceptions.SecurityError
|
|
31
|
+
case DenyReason.INJECTION_ATTEMPT:
|
|
32
|
+
return exceptions.InjectionError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelDeny(BaseModel):
|
|
36
|
+
deny_type: DenyReason = Field(description="Type of denial")
|
|
37
|
+
reason: str = Field(
|
|
38
|
+
description="Reason for denying the request. Should be a short sentence that can be shown to the user. Not longer than two lines"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def __init__(self, **data):
|
|
42
|
+
super().__init__(**data)
|
|
43
|
+
# Raise the corresponding exception immediately
|
|
44
|
+
exception_class = self.deny_type.to_exception()
|
|
45
|
+
raise exception_class(self.reason)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: contentgrid-extension-helpers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Helper functions for contentgrid extensions.
|
|
5
5
|
Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
|
|
6
6
|
License: Copyright 2024 Xenit Solutions
|
|
@@ -26,6 +26,7 @@ Requires-Dist: requests<3,>=2.20.0
|
|
|
26
26
|
Requires-Dist: uri-template<2
|
|
27
27
|
Requires-Dist: fastapi>=0.111
|
|
28
28
|
Requires-Dist: PyJWT>2
|
|
29
|
+
Requires-Dist: contentgrid_hal_client>=0.0.10
|
|
29
30
|
|
|
30
31
|
### ContentGrid-Extension-Helpers
|
|
31
32
|
|
|
@@ -5,6 +5,8 @@ pytest.ini
|
|
|
5
5
|
requirements.txt
|
|
6
6
|
setup.py
|
|
7
7
|
src/contentgrid_extension_helpers/__init__.py
|
|
8
|
+
src/contentgrid_extension_helpers/exceptions.py
|
|
9
|
+
src/contentgrid_extension_helpers/problem_response.py
|
|
8
10
|
src/contentgrid_extension_helpers.egg-info/PKG-INFO
|
|
9
11
|
src/contentgrid_extension_helpers.egg-info/SOURCES.txt
|
|
10
12
|
src/contentgrid_extension_helpers.egg-info/dependency_links.txt
|
|
@@ -15,4 +17,7 @@ src/contentgrid_extension_helpers/authentication/oidc.py
|
|
|
15
17
|
src/contentgrid_extension_helpers/authentication/user.py
|
|
16
18
|
src/contentgrid_extension_helpers/logging/__init__.py
|
|
17
19
|
src/contentgrid_extension_helpers/logging/json_logging.py
|
|
20
|
+
src/contentgrid_extension_helpers/middleware/exception_middleware.py
|
|
21
|
+
src/contentgrid_extension_helpers/structured_output/model_deny.py
|
|
22
|
+
tests/test_exception_middleware.py
|
|
18
23
|
tests/test_logging.py
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from fastapi import FastAPI, Request, status
|
|
2
|
+
from fastapi.testclient import TestClient
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import MagicMock
|
|
5
|
+
|
|
6
|
+
from contentgrid_extension_helpers.middleware.exception_middleware import (
|
|
7
|
+
catch_exceptions_middleware,
|
|
8
|
+
)
|
|
9
|
+
from contentgrid_hal_client.exceptions import (
|
|
10
|
+
NotFound,
|
|
11
|
+
Unauthorized,
|
|
12
|
+
BadRequest,
|
|
13
|
+
IncorrectAttributeType,
|
|
14
|
+
NonExistantAttribute,
|
|
15
|
+
MissingRequiredAttribute,
|
|
16
|
+
MissingHALTemplate,
|
|
17
|
+
)
|
|
18
|
+
from requests.exceptions import HTTPError
|
|
19
|
+
from contentgrid_extension_helpers.exceptions import LLMDenyException
|
|
20
|
+
from contentgrid_extension_helpers.problem_response import ProblemResponse
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def app():
|
|
25
|
+
"""Creates a FastAPI app with the exception middleware."""
|
|
26
|
+
app = FastAPI()
|
|
27
|
+
|
|
28
|
+
@app.middleware("http")
|
|
29
|
+
async def add_exception_middleware(request: Request, call_next):
|
|
30
|
+
return await catch_exceptions_middleware(request, call_next, "https://test.example.com")
|
|
31
|
+
|
|
32
|
+
return app
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def client(app):
|
|
37
|
+
"""Provides a TestClient for the test app."""
|
|
38
|
+
return TestClient(app)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_no_exception(client: TestClient):
|
|
42
|
+
"""Tests the middleware when no exception is raised."""
|
|
43
|
+
|
|
44
|
+
@client.app.get("/no-error")
|
|
45
|
+
async def no_error():
|
|
46
|
+
return {"message": "OK"}
|
|
47
|
+
|
|
48
|
+
response = client.get("/no-error")
|
|
49
|
+
assert response.status_code == 200
|
|
50
|
+
assert response.json() == {"message": "OK"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_no_exception_with_origin(client: TestClient):
|
|
54
|
+
"""Tests the middleware when no exception is raised and origin is present."""
|
|
55
|
+
|
|
56
|
+
@client.app.get("/no-error")
|
|
57
|
+
async def no_error():
|
|
58
|
+
return {"message": "OK"}
|
|
59
|
+
|
|
60
|
+
response = client.get("/no-error", headers={"Origin": "https://example.com"})
|
|
61
|
+
assert response.status_code == 200
|
|
62
|
+
assert response.json() == {"message": "OK"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_not_found_exception(client: TestClient):
|
|
66
|
+
"""Tests the NotFound exception handling."""
|
|
67
|
+
|
|
68
|
+
@client.app.get("/not-found")
|
|
69
|
+
async def not_found():
|
|
70
|
+
raise NotFound()
|
|
71
|
+
|
|
72
|
+
response = client.get("/not-found")
|
|
73
|
+
assert response.status_code == 404
|
|
74
|
+
problem = response.json()
|
|
75
|
+
assert problem["title"] == "Not found"
|
|
76
|
+
assert problem["type"] == "https://test.example.com/not-found"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_unauthorized_exception(client: TestClient):
|
|
80
|
+
"""Tests the Unauthorized exception handling."""
|
|
81
|
+
|
|
82
|
+
@client.app.get("/unauthorized")
|
|
83
|
+
async def unauthorized():
|
|
84
|
+
raise Unauthorized()
|
|
85
|
+
|
|
86
|
+
response = client.get("/unauthorized", headers={"Origin": "https://example.com"})
|
|
87
|
+
assert response.status_code == 401
|
|
88
|
+
problem = response.json()
|
|
89
|
+
assert problem["title"] == "Unauthorized"
|
|
90
|
+
assert problem["type"] == "https://test.example.com/unauthorized"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_bad_request_exception(client: TestClient):
|
|
94
|
+
"""Tests the BadRequest exception handling."""
|
|
95
|
+
|
|
96
|
+
@client.app.get("/bad-request")
|
|
97
|
+
async def bad_request():
|
|
98
|
+
raise BadRequest()
|
|
99
|
+
|
|
100
|
+
response = client.get("/bad-request")
|
|
101
|
+
assert response.status_code == 400
|
|
102
|
+
problem = response.json()
|
|
103
|
+
assert problem["title"] == "Bad Request"
|
|
104
|
+
assert problem["type"] == "https://test.example.com/bad-request"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_incorrect_attribute_type_exception(client: TestClient):
|
|
108
|
+
"""Tests the IncorrectAttributeType exception handling."""
|
|
109
|
+
|
|
110
|
+
@client.app.get("/incorrect-attribute")
|
|
111
|
+
async def incorrect_attribute():
|
|
112
|
+
raise IncorrectAttributeType("Incorrect type")
|
|
113
|
+
|
|
114
|
+
response = client.get("/incorrect-attribute", headers={"Origin": "https://different.com"})
|
|
115
|
+
assert response.status_code == 400
|
|
116
|
+
problem = response.json()
|
|
117
|
+
assert problem["title"] == "Incorrect Attribute Type"
|
|
118
|
+
assert problem["type"] == "https://test.example.com/incorrect-attribute-type"
|
|
119
|
+
assert problem["detail"] == "Incorrect type"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_non_existent_attribute_exception(client: TestClient):
|
|
123
|
+
"""Tests the NonExistantAttribute exception handling."""
|
|
124
|
+
|
|
125
|
+
@client.app.get("/non-existent-attribute")
|
|
126
|
+
async def non_existent_attribute():
|
|
127
|
+
raise NonExistantAttribute("Attribute does not exist")
|
|
128
|
+
|
|
129
|
+
response = client.get("/non-existent-attribute")
|
|
130
|
+
assert response.status_code == 404
|
|
131
|
+
problem = response.json()
|
|
132
|
+
assert problem["title"] == "Non-Existent Attribute"
|
|
133
|
+
assert problem["type"] == "https://test.example.com/non-existent-attribute"
|
|
134
|
+
assert problem["detail"] == "Attribute does not exist"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_missing_required_attribute_exception(client: TestClient):
|
|
138
|
+
"""Tests the MissingRequiredAttribute exception handling."""
|
|
139
|
+
|
|
140
|
+
@client.app.get("/missing-required-attribute")
|
|
141
|
+
async def missing_required_attribute():
|
|
142
|
+
raise MissingRequiredAttribute("Missing attribute")
|
|
143
|
+
|
|
144
|
+
response = client.get("/missing-required-attribute")
|
|
145
|
+
assert response.status_code == 400
|
|
146
|
+
problem = response.json()
|
|
147
|
+
assert problem["title"] == "Missing Required Attribute"
|
|
148
|
+
assert problem["type"] == "https://test.example.com/missing-required-attribute"
|
|
149
|
+
assert problem["detail"] == "Missing attribute"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_missing_hal_template_exception(client: TestClient):
|
|
153
|
+
"""Tests the MissingHALTemplate exception handling."""
|
|
154
|
+
|
|
155
|
+
@client.app.get("/missing-hal-template")
|
|
156
|
+
async def missing_hal_template():
|
|
157
|
+
raise MissingHALTemplate("HAL template missing")
|
|
158
|
+
|
|
159
|
+
response = client.get("/missing-hal-template")
|
|
160
|
+
assert response.status_code == 404
|
|
161
|
+
problem = response.json()
|
|
162
|
+
assert problem["title"] == "Missing HAL Template"
|
|
163
|
+
assert problem["type"] == "https://test.example.com/missing-hal-template"
|
|
164
|
+
assert problem["detail"] == "HAL template missing"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_http_error_exception(client: TestClient):
|
|
168
|
+
"""Tests the HTTPError exception handling."""
|
|
169
|
+
|
|
170
|
+
@client.app.get("/http-error")
|
|
171
|
+
async def http_error():
|
|
172
|
+
response = MagicMock()
|
|
173
|
+
response.status_code = 418 # I'm a teapot!
|
|
174
|
+
raise HTTPError("Teapot error", response=response)
|
|
175
|
+
|
|
176
|
+
response = client.get("/http-error")
|
|
177
|
+
assert response.status_code == 418
|
|
178
|
+
problem = response.json()
|
|
179
|
+
assert problem["title"] == "HTTP Error"
|
|
180
|
+
assert problem["type"] == "https://test.example.com/http-error"
|
|
181
|
+
assert problem["detail"] == "An HTTP error occurred: Teapot error"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_llm_deny_exception(client: TestClient):
|
|
185
|
+
"""Tests the LLMDenyException exception handling."""
|
|
186
|
+
|
|
187
|
+
@client.app.get("/llm-deny")
|
|
188
|
+
async def llm_deny():
|
|
189
|
+
raise LLMDenyException("LLM denied")
|
|
190
|
+
|
|
191
|
+
response = client.get("/llm-deny")
|
|
192
|
+
assert response.status_code == 400
|
|
193
|
+
problem = response.json()
|
|
194
|
+
assert problem["title"] == "Request Denied"
|
|
195
|
+
assert problem["type"] == "https://test.example.com/request-denied"
|
|
196
|
+
assert problem["detail"] == "LLM denied"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_generic_exception(client: TestClient):
|
|
200
|
+
"""Tests the generic Exception handling."""
|
|
201
|
+
|
|
202
|
+
@client.app.get("/generic-exception")
|
|
203
|
+
async def generic_exception():
|
|
204
|
+
raise Exception("Something went wrong")
|
|
205
|
+
|
|
206
|
+
response = client.get("/generic-exception")
|
|
207
|
+
assert response.status_code == 500
|
|
208
|
+
problem = response.json()
|
|
209
|
+
assert problem["title"] == "Internal server error"
|
|
210
|
+
assert problem["type"] == "https://test.example.com/unknown"
|
|
211
|
+
assert problem["detail"] == "An unexpected error occurred: Something went wrong"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_http_error_no_response(client: TestClient):
|
|
215
|
+
"""Tests HTTPError when e.response is None"""
|
|
216
|
+
@client.app.get("/http-error-no-response")
|
|
217
|
+
async def http_error_no_response():
|
|
218
|
+
raise HTTPError("Generic HTTP Error")
|
|
219
|
+
|
|
220
|
+
response = client.get("/http-error-no-response")
|
|
221
|
+
assert response.status_code == 500
|
|
222
|
+
problem = response.json()
|
|
223
|
+
assert problem["title"] == "HTTP Error"
|
|
224
|
+
assert problem["type"] == "https://test.example.com/http-error"
|
|
225
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentgrid_extension_helpers-0.0.1 → contentgrid_extension_helpers-0.0.2}/tests/test_logging.py
RENAMED
|
File without changes
|