contentgrid-extension-helpers 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contentgrid_extension_helpers/__init__.py +3 -0
- contentgrid_extension_helpers/authentication/__init__.py +2 -2
- contentgrid_extension_helpers/exceptions.py +48 -0
- contentgrid_extension_helpers/logging/__init__.py +1 -1
- contentgrid_extension_helpers/logging/json_logging.py +2 -2
- contentgrid_extension_helpers/middleware/exception_middleware.py +124 -0
- contentgrid_extension_helpers/problem_response.py +45 -0
- contentgrid_extension_helpers/structured_output/model_deny.py +45 -0
- {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.2.dist-info}/METADATA +2 -1
- contentgrid_extension_helpers-0.0.2.dist-info/RECORD +15 -0
- {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.2.dist-info}/WHEEL +1 -1
- contentgrid_extension_helpers-0.0.1.dist-info/RECORD +0 -11
- {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.2.dist-info}/LICENSE +0 -0
- {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from .oidc import create_current_user_dependency, decode_and_verify_token, create_oauth2_scheme,
|
|
2
|
-
from .user import ContentGridUser
|
|
1
|
+
from .oidc import create_current_user_dependency, decode_and_verify_token, create_oauth2_scheme, get_oauth_jwks_client, get_oidc_jwks_client # noqa: F401
|
|
2
|
+
from .user import ContentGridUser # 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
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
contentgrid_extension_helpers/__init__.py,sha256=Mw64JI29DW5HOzmL1UA8grdyrOqAQeHlllR_zxjagy8,380
|
|
2
|
+
contentgrid_extension_helpers/exceptions.py,sha256=GJOIwDC_51pyhiPaq6wRrkmS3X1aU_PQoEE4sPTgopQ,1812
|
|
3
|
+
contentgrid_extension_helpers/problem_response.py,sha256=v2z_hx92pHWyqMZfnJDi6TaGVIeoGh2AXVr1F9VA00w,1542
|
|
4
|
+
contentgrid_extension_helpers/authentication/__init__.py,sha256=XnupVcladC7H4LudetZ46da9IP69pWKhtRJvikhmIKM,200
|
|
5
|
+
contentgrid_extension_helpers/authentication/oidc.py,sha256=14XEmp_WWDVygb3oBKB9S29UlgJV8wnm_1lw36U4xxc,9820
|
|
6
|
+
contentgrid_extension_helpers/authentication/user.py,sha256=pr3DVZKchLxpJXU6k2uUNLquwr9LjWfr_sArdAjUjZU,185
|
|
7
|
+
contentgrid_extension_helpers/logging/__init__.py,sha256=15tz-g0fLdBjJto8kWcjCYhfXCJg0qrwSXVuXRH9i3Q,77
|
|
8
|
+
contentgrid_extension_helpers/logging/json_logging.py,sha256=NrzoBfEUAwQT7mmCHK0GoFJv5t9rrgjomDTXzZ-vgFI,3112
|
|
9
|
+
contentgrid_extension_helpers/middleware/exception_middleware.py,sha256=RXbdTzdtfufrGBfViTIQZw691jgXE3rmogKF28Vlhzs,4462
|
|
10
|
+
contentgrid_extension_helpers/structured_output/model_deny.py,sha256=n2sEls0kyhvL8hHUKeo3_JQ_ZssyknIbIoDAOUvMVxc,1748
|
|
11
|
+
contentgrid_extension_helpers-0.0.2.dist-info/LICENSE,sha256=tk6n-p8lEmzLJg-O4052CkMgfUtt1q2Zoh1QLAyL7S8,555
|
|
12
|
+
contentgrid_extension_helpers-0.0.2.dist-info/METADATA,sha256=VIMjWfLhujv6cT5eJvnUozPrKy1G4yVwo8XsNPpmRGM,1393
|
|
13
|
+
contentgrid_extension_helpers-0.0.2.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
14
|
+
contentgrid_extension_helpers-0.0.2.dist-info/top_level.txt,sha256=yJGGofrNVsl5psVGO0vLFHO1610ob88GtB9zpvS8iIk,30
|
|
15
|
+
contentgrid_extension_helpers-0.0.2.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
contentgrid_extension_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
contentgrid_extension_helpers/authentication/__init__.py,sha256=b6Bp8jCg2-T7bcTGid7pCk5PfNYwH-52UFUX2Op8ArQ,146
|
|
3
|
-
contentgrid_extension_helpers/authentication/oidc.py,sha256=14XEmp_WWDVygb3oBKB9S29UlgJV8wnm_1lw36U4xxc,9820
|
|
4
|
-
contentgrid_extension_helpers/authentication/user.py,sha256=pr3DVZKchLxpJXU6k2uUNLquwr9LjWfr_sArdAjUjZU,185
|
|
5
|
-
contentgrid_extension_helpers/logging/__init__.py,sha256=u6edUMrsULUinBNN94yJywZyINz4azhIAXO2LMbmtaY,64
|
|
6
|
-
contentgrid_extension_helpers/logging/json_logging.py,sha256=QRoDHZjtMXM-JGPR6JWqJnCljwSQ912wjsZ38XMA7Tw,3119
|
|
7
|
-
contentgrid_extension_helpers-0.0.1.dist-info/LICENSE,sha256=tk6n-p8lEmzLJg-O4052CkMgfUtt1q2Zoh1QLAyL7S8,555
|
|
8
|
-
contentgrid_extension_helpers-0.0.1.dist-info/METADATA,sha256=eP5YG_IsyyRtfonDnTiwnLlKPuQHZ7JWkB2s4aFaO2k,1347
|
|
9
|
-
contentgrid_extension_helpers-0.0.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
10
|
-
contentgrid_extension_helpers-0.0.1.dist-info/top_level.txt,sha256=yJGGofrNVsl5psVGO0vLFHO1610ob88GtB9zpvS8iIk,30
|
|
11
|
-
contentgrid_extension_helpers-0.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|