contentgrid-extension-helpers 0.0.1__py3-none-any.whl → 0.0.3__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.
Files changed (30) hide show
  1. contentgrid_extension_helpers/__init__.py +3 -0
  2. contentgrid_extension_helpers/authentication/__init__.py +2 -2
  3. contentgrid_extension_helpers/authentication/user.py +28 -2
  4. contentgrid_extension_helpers/config.py +40 -0
  5. contentgrid_extension_helpers/dependencies/authentication/user.py +73 -0
  6. contentgrid_extension_helpers/dependencies/clients/contentgrid/__init__.py +3 -0
  7. contentgrid_extension_helpers/dependencies/clients/contentgrid/client_factory.py +71 -0
  8. contentgrid_extension_helpers/dependencies/clients/contentgrid/extension_flow_factory.py +85 -0
  9. contentgrid_extension_helpers/dependencies/clients/contentgrid/service_account_factory.py +87 -0
  10. contentgrid_extension_helpers/dependencies/sqlalch/__init__.py +0 -0
  11. contentgrid_extension_helpers/dependencies/sqlalch/db/__init__.py +14 -0
  12. contentgrid_extension_helpers/dependencies/sqlalch/db/base_factory.py +107 -0
  13. contentgrid_extension_helpers/dependencies/sqlalch/db/postgres.py +104 -0
  14. contentgrid_extension_helpers/dependencies/sqlalch/db/sqlite.py +43 -0
  15. contentgrid_extension_helpers/dependencies/sqlalch/repositories/__init__.py +1 -0
  16. contentgrid_extension_helpers/dependencies/sqlalch/repositories/base_repository.py +52 -0
  17. contentgrid_extension_helpers/exceptions.py +48 -0
  18. contentgrid_extension_helpers/logging/__init__.py +1 -1
  19. contentgrid_extension_helpers/logging/json_logging.py +2 -2
  20. contentgrid_extension_helpers/middleware/exception_middleware.py +124 -0
  21. contentgrid_extension_helpers/problem_response.py +45 -0
  22. contentgrid_extension_helpers/responses/__init__.py +0 -0
  23. contentgrid_extension_helpers/responses/hal.py +212 -0
  24. contentgrid_extension_helpers/structured_output/model_deny.py +45 -0
  25. {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.3.dist-info}/METADATA +8 -2
  26. contentgrid_extension_helpers-0.0.3.dist-info/RECORD +30 -0
  27. {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.3.dist-info}/WHEEL +1 -1
  28. contentgrid_extension_helpers-0.0.1.dist-info/RECORD +0 -11
  29. {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.3.dist-info/licenses}/LICENSE +0 -0
  30. {contentgrid_extension_helpers-0.0.1.dist-info → contentgrid_extension_helpers-0.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,43 @@
1
+ from typing import Dict, Any, Optional
2
+
3
+ from pydantic import field_validator
4
+ from .base_factory import DatabaseConfig, DatabaseSessionFactory
5
+
6
+
7
+ class SQLiteConfig(DatabaseConfig):
8
+ sqlite_file_name: str = "database.db"
9
+
10
+ @field_validator("sqlite_file_name")
11
+ def validate_sqlite_file_name(cls, value: str) -> str:
12
+ if not value.endswith('.db'):
13
+ raise ValueError("SQLite file name must end with '.db'")
14
+ return value
15
+
16
+
17
+ class SQLiteSessionFactory(DatabaseSessionFactory):
18
+ """Factory class to create SQLite database connections."""
19
+
20
+ def __init__(self, debug: Optional[bool]= None, sqlite_file_name : Optional[str] = None):
21
+ config_dict = {}
22
+ if debug is not None:
23
+ config_dict['debug'] = debug
24
+ if sqlite_file_name is not None:
25
+ config_dict['sqlite_file_name'] = sqlite_file_name
26
+
27
+ db_config = SQLiteConfig(**config_dict)
28
+ super().__init__(db_config)
29
+
30
+ def create_connection_string(self) -> str:
31
+ """Create the SQLite connection string."""
32
+ config = self.db_config
33
+ if not isinstance(config, SQLiteConfig):
34
+ raise ValueError("SQLiteConfig is required for SQLiteSessionFactory")
35
+ return f"sqlite:///{config.sqlite_file_name}"
36
+
37
+ def create_connect_args(self) -> Dict[str, Any]:
38
+ """Create SQLite connection arguments."""
39
+ return {"check_same_thread": False}
40
+
41
+ def create_engine_kwargs(self) -> Dict[str, Any]:
42
+ """Create SQLite engine keyword arguments."""
43
+ return {} # No pool config for SQLite
@@ -0,0 +1 @@
1
+ from .base_repository import BaseRepository
@@ -0,0 +1,52 @@
1
+ from typing import Dict, Generic, List, Type, TypeVar
2
+ from contentgrid_hal_client import NotFound
3
+ from sqlmodel import SQLModel, Session, select
4
+
5
+
6
+ T = TypeVar('T', bound=SQLModel)
7
+ CreateT = TypeVar('CreateT', bound=SQLModel)
8
+ UpdateT = TypeVar('UpdateT', bound=SQLModel)
9
+
10
+ class BaseRepository(Generic[T, CreateT, UpdateT]):
11
+ """Generic base repository for database operations"""
12
+
13
+ def __init__(self, session: Session, model_class: Type[T]):
14
+ self.session = session
15
+ self.model_class = model_class
16
+
17
+ def create(self, create_model: CreateT) -> T:
18
+ """Create a new entity"""
19
+ db_entity = self.model_class.model_validate(create_model)
20
+ self.session.add(db_entity)
21
+ self.session.commit()
22
+ self.session.refresh(db_entity)
23
+ return db_entity
24
+
25
+ def get_by_id(self, entity_id: int) -> T:
26
+ """Get entity by ID"""
27
+ entity = self.session.get(self.model_class, entity_id)
28
+ if not entity:
29
+ raise NotFound(f"{self.model_class.__name__} with id {entity_id} not found")
30
+ return entity
31
+
32
+ def get_all(self, offset: int = 0, limit: int = 100) -> List[T]:
33
+ # user filter has to be pre query for no error pagination
34
+ """Get all entities with pagination"""
35
+ return self.session.exec(select(self.model_class).offset(offset).limit(limit)).all()
36
+
37
+ def update(self, entity_id: int, update_model: UpdateT) -> T:
38
+ """Update entity by ID"""
39
+ db_entity = self.get_by_id(entity_id)
40
+ update_data = update_model.model_dump(exclude_unset=True)
41
+ db_entity.sqlmodel_update(update_data)
42
+ self.session.add(db_entity)
43
+ self.session.commit()
44
+ self.session.refresh(db_entity)
45
+ return db_entity
46
+
47
+ def delete(self, entity_id: int) -> Dict[str, bool]:
48
+ """Delete entity by ID"""
49
+ db_entity = self.get_by_id(entity_id)
50
+ self.session.delete(db_entity)
51
+ self.session.commit()
52
+ return {"ok": True}
@@ -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 List, Optional, Any, Dict
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, 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://problems.contentgrid.test"):
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
+ )
File without changes
@@ -0,0 +1,212 @@
1
+ from urllib.parse import urlencode
2
+ from fastapi import FastAPI
3
+ from fastapi.routing import APIRoute
4
+ from pydantic import BaseModel, Field, field_serializer
5
+ from fastapi._compat import ModelField
6
+ from typing import Dict, Optional, Tuple, Type, Any, Union, List, Self, TypeVar, Generic, Callable, cast
7
+ import logging
8
+ from contentgrid_hal_client.hal import HALShape, HALLink
9
+ from contentgrid_hal_client.hal_forms import HALFormsTemplate, HALFormsMethod, HALFormsPropertyType, HALFormsProperty
10
+ import uri_template
11
+
12
+ def get_route_from_app(app: FastAPI, endpoint_function: str) -> APIRoute:
13
+ for route in app.routes:
14
+ if isinstance(route, APIRoute) and route.name == endpoint_function:
15
+ return route
16
+ else:
17
+ error_message = f"No route found for endpoint {endpoint_function}"
18
+ raise ValueError(error_message)
19
+
20
+ def _add_params(url : str, params: Optional[Dict[str, str]] = None) -> str:
21
+ if params:
22
+ url_postfix = "?" + urlencode(params)
23
+ url += url_postfix
24
+ return url
25
+
26
+ def get_body_from_route(route : APIRoute) -> Tuple[Optional[BaseModel], dict]:
27
+ required_body = route.body_field
28
+ if not required_body:
29
+ return None, {}
30
+ pydantic_body, default_data = get_pydantic_base_model_from_model_field(required_body)
31
+ return pydantic_body, default_data
32
+
33
+ def get_pydantic_base_model_from_model_field(model_field : ModelField) -> Tuple[BaseModel, dict]:
34
+ default_data = model_field._type_adapter.get_default_value() or {}
35
+ pydantic_body = cast(BaseModel, model_field._type_adapter._type)
36
+ return pydantic_body, default_data
37
+
38
+ def extract_hal_forms_properties_from_pydantic_base_model(
39
+ pydantic_base_model : BaseModel,
40
+ default_data : dict = {}
41
+ ) -> List[HALFormsProperty]:
42
+ properties : List[HALFormsProperty] = []
43
+ for field_name, field_info in pydantic_base_model.model_fields.items():
44
+ # Determine property type based on field type
45
+ property_type = HALFormsPropertyType.text
46
+
47
+ # Convert field type to HALFormsPropertyType
48
+ if field_info.annotation is int or field_info.annotation is float:
49
+ property_type = HALFormsPropertyType.number
50
+ elif field_info.annotation is bool:
51
+ property_type = HALFormsPropertyType.checkbox
52
+
53
+ # Get default value for this field if available
54
+ field_default = default_data.get(field_name) if default_data else None
55
+
56
+ # Create HALFormsProperty
57
+ field_property = HALFormsProperty(
58
+ name=field_name,
59
+ prompt=field_info.description or field_name,
60
+ required=field_info.is_required(),
61
+ type=property_type,
62
+ value=field_default
63
+ )
64
+ properties.append(field_property)
65
+ return properties
66
+
67
+ class LinkForType(BaseModel):
68
+ endpoint_function_name: str
69
+ templated: bool = False
70
+ path_params: Union[dict[str, str], Callable[["FastAPIHALResponse"], dict[str, str]]] = Field(default_factory=dict)
71
+ params: Union[dict[str, Union[str, int, float]], Callable[["FastAPIHALResponse"], dict[str, Union[str, int, float]]]] = Field(default_factory=dict)
72
+ condition: Union[Callable[["FastAPIHALResponse"], bool], bool] = True
73
+
74
+
75
+ class HALLinkFor(LinkForType):
76
+ pass
77
+
78
+ class HALTemplateFor(LinkForType):
79
+ pass
80
+
81
+
82
+ HALLinks = dict[str, Union[HALLink, HALLinkFor]]
83
+ HALTemplates = dict[str, Union[HALFormsTemplate, HALTemplateFor]]
84
+
85
+ # Type variable for generic embedded resources - must be a subclass of FastAPIHALResponse
86
+ T = TypeVar('T', bound='FastAPIHALResponse')
87
+
88
+
89
+ class FastAPIHALResponse(HALShape):
90
+ links: dict[str, Union[HALLink, HALLinkFor]] = Field(alias="_links", exclude=False, default_factory=dict)
91
+ templates: dict[str, Union[HALFormsTemplate, HALTemplateFor]] | None = Field(default=None, alias="_templates")
92
+
93
+ def __expand_link(self, link : HALLinkFor | HALLink) -> HALLink | None:
94
+ """
95
+ Expand the links based on server url and path and params.
96
+ This method should be called after the class is initialized.
97
+ Returns None if the link should be excluded based on condition.
98
+ """
99
+ if not hasattr(self.__class__, '_app'):
100
+ raise ValueError("App not initialized. Call init_app() before using this method.")
101
+
102
+ if not isinstance(link, HALLinkFor):
103
+ return link
104
+
105
+ # Check condition - if False, exclude the link
106
+ if isinstance(link.condition, bool):
107
+ if not link.condition:
108
+ return None
109
+ elif callable(link.condition):
110
+ if not link.condition(self):
111
+ return None
112
+
113
+ if hasattr(self.__class__, '_app') and self.__class__._app:
114
+ route = get_route_from_app(self.__class__._app, link.endpoint_function_name)
115
+
116
+ if hasattr(self.__class__, '_server_url'):
117
+ uri = f"{self.__class__._server_url}{route.path}"
118
+ else:
119
+ uri = route.path
120
+
121
+ expanded_link = HALLink(uri=uri, templated=link.templated)
122
+
123
+ if link.path_params:
124
+ # Handle callable params
125
+ if callable(link.path_params):
126
+ resolved_path_params = link.path_params(self)
127
+ else:
128
+ resolved_path_params = link.path_params
129
+ expanded_link.uri = uri_template.URITemplate(expanded_link.uri).expand(**resolved_path_params)
130
+
131
+ if link.params:
132
+ # Handle callable params
133
+ if callable(link.params):
134
+ resolved_params = link.params(self)
135
+ else:
136
+ resolved_params = link.params
137
+ expanded_link.uri = _add_params(expanded_link.uri, resolved_params)
138
+
139
+ return expanded_link
140
+
141
+
142
+ @field_serializer('links')
143
+ def ser_links(self, value: dict[str, Union[HALLink, HALLinkFor]]) -> dict[str, HALLink]:
144
+ expanded_links = {}
145
+ for key, link_value in value.items():
146
+ expanded_link = self.__expand_link(link_value)
147
+ if expanded_link is not None: # Only include if condition is met
148
+ expanded_links[key] = expanded_link
149
+ return expanded_links
150
+
151
+ @field_serializer('templates')
152
+ def ser_templates(self, value: dict[str, Union[HALFormsTemplate, HALTemplateFor]] | None) -> dict[str, HALFormsTemplate]:
153
+ expanded_templates = {}
154
+ if value is None:
155
+ return expanded_templates
156
+ for key, template_value in value.items():
157
+ if isinstance(template_value, HALTemplateFor) and hasattr(self.__class__, '_app') and self.__class__._app:
158
+ try:
159
+ hallink = self.__expand_link(HALLinkFor(
160
+ **template_value.model_dump()
161
+ ))
162
+
163
+ if hallink is None:
164
+ continue
165
+
166
+ uri = hallink.uri
167
+ route = get_route_from_app(self.__class__._app, template_value.endpoint_function_name)
168
+ body_model, default_data = get_body_from_route(route=route)
169
+ if body_model is None:
170
+ properties = []
171
+ else:
172
+ properties = extract_hal_forms_properties_from_pydantic_base_model(pydantic_base_model=body_model, default_data=default_data)
173
+
174
+ #TODO what should we do when there are multiple methods for the same function/endpoint?
175
+ expanded_templates[key] = HALFormsTemplate(
176
+ title=route.description if hasattr(route, 'description') and route.description else None,
177
+ method=HALFormsMethod(list(route.methods)[0]) if hasattr(route, 'methods') and route.methods else HALFormsMethod.GET,
178
+ target=uri,
179
+ properties=properties
180
+ )
181
+ except ValueError:
182
+ logging.error(f"{self.__class__} hal template expansion failed: Route not found for template endpoint: {template_value.endpoint_function_name}")
183
+ continue
184
+ else:
185
+ expanded_templates[key] = template_value
186
+ return expanded_templates
187
+
188
+
189
+ @classmethod
190
+ def init_app(cls: Type[Self], app: Any) -> None:
191
+ """
192
+ Bind a FastAPI app to other HyperModel base class.
193
+ This allows HyperModel to convert endpoint function names into
194
+ working URLs relative to the application root.
195
+
196
+ Args:
197
+ app (FastAPI): Application to generate URLs from
198
+ """
199
+ cls._app = app
200
+
201
+ @classmethod
202
+ def add_server_url(cls: Type[Self], server_url: str) -> None:
203
+ """
204
+ Set the server URL for generating absolute URLs.
205
+
206
+ Args:
207
+ server_url (str): The base URL of the server.
208
+ """
209
+ cls._server_url = server_url
210
+
211
+ class FastAPIHALCollection(FastAPIHALResponse, Generic[T]):
212
+ embedded: dict[str, List[T]] | None = Field(default=None, alias="_embedded", description="Embedded resources")
@@ -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
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: contentgrid-extension-helpers
3
- Version: 0.0.1
3
+ Version: 0.0.3
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,12 @@ 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: cryptography>45
30
+ Requires-Dist: contentgrid_hal_client>=0.1
31
+ Requires-Dist: contentgrid_application_client>=0.1
32
+ Requires-Dist: contentgrid_management_client>=0.1
33
+ Requires-Dist: pydantic<3,>=2
34
+ Dynamic: license-file
29
35
 
30
36
  ### ContentGrid-Extension-Helpers
31
37
 
@@ -0,0 +1,30 @@
1
+ contentgrid_extension_helpers/__init__.py,sha256=Mw64JI29DW5HOzmL1UA8grdyrOqAQeHlllR_zxjagy8,380
2
+ contentgrid_extension_helpers/config.py,sha256=V6IUE5bRyg0k5Gzbq8YdB2k_0OeEzHOI-_h4gaQJiPQ,1401
3
+ contentgrid_extension_helpers/exceptions.py,sha256=GJOIwDC_51pyhiPaq6wRrkmS3X1aU_PQoEE4sPTgopQ,1812
4
+ contentgrid_extension_helpers/problem_response.py,sha256=v2z_hx92pHWyqMZfnJDi6TaGVIeoGh2AXVr1F9VA00w,1542
5
+ contentgrid_extension_helpers/authentication/__init__.py,sha256=XnupVcladC7H4LudetZ46da9IP69pWKhtRJvikhmIKM,200
6
+ contentgrid_extension_helpers/authentication/oidc.py,sha256=14XEmp_WWDVygb3oBKB9S29UlgJV8wnm_1lw36U4xxc,9820
7
+ contentgrid_extension_helpers/authentication/user.py,sha256=EwUjIczs3oFiM0TQK-eLixVNgeou4OY0zTwszFURmrc,1540
8
+ contentgrid_extension_helpers/dependencies/authentication/user.py,sha256=c3Xgg3J4O_TqfdCIETg0VtNF1NUrzaB-gxCJaojrbv4,2817
9
+ contentgrid_extension_helpers/dependencies/clients/contentgrid/__init__.py,sha256=J43Bq-VOCr3OZQA8QzofQXwhDbB2jI4qEPbSc6JI_oI,240
10
+ contentgrid_extension_helpers/dependencies/clients/contentgrid/client_factory.py,sha256=7WL39-tsHZFve_35drrmwb2NczUxvLMiQQvXsdBguA4,2541
11
+ contentgrid_extension_helpers/dependencies/clients/contentgrid/extension_flow_factory.py,sha256=AHgxq3WttAsW4BqbMrgo77BmX94OMJz_Yb1TrjyWcOs,4111
12
+ contentgrid_extension_helpers/dependencies/clients/contentgrid/service_account_factory.py,sha256=BdUZxK2gU8TFZNWdkufm_73uKAkboyJd08-000kfwLc,3932
13
+ contentgrid_extension_helpers/dependencies/sqlalch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ contentgrid_extension_helpers/dependencies/sqlalch/db/__init__.py,sha256=7PntPSCwinwSk2il3_LWoSEHnCmx4bQvYLHPC7WJbDw,414
15
+ contentgrid_extension_helpers/dependencies/sqlalch/db/base_factory.py,sha256=vXFQzot1aR__P1ZyMa7VE_JE1sixJjNkKlZBDppKowE,4263
16
+ contentgrid_extension_helpers/dependencies/sqlalch/db/postgres.py,sha256=X2kWg3EBTNcXhQQxuym4pW0GqmZiLxQFi4tvZqiwl7o,4563
17
+ contentgrid_extension_helpers/dependencies/sqlalch/db/sqlite.py,sha256=2FgAcLs8SqMWCUqlOa-rv_JWGLPVhDVt_eTiwWqWdZA,1592
18
+ contentgrid_extension_helpers/dependencies/sqlalch/repositories/__init__.py,sha256=ApJOGKoqnxLfdCqtnkQoOykPCYMnUQhtX29MyD5nOng,43
19
+ contentgrid_extension_helpers/dependencies/sqlalch/repositories/base_repository.py,sha256=PWN-V_YzYwLxRnZWSIfGyVxi8r9lq3wWObOi0cYcFRg,1975
20
+ contentgrid_extension_helpers/logging/__init__.py,sha256=15tz-g0fLdBjJto8kWcjCYhfXCJg0qrwSXVuXRH9i3Q,77
21
+ contentgrid_extension_helpers/logging/json_logging.py,sha256=NrzoBfEUAwQT7mmCHK0GoFJv5t9rrgjomDTXzZ-vgFI,3112
22
+ contentgrid_extension_helpers/middleware/exception_middleware.py,sha256=6mP7IQ7vPZ6-1_QESnAPZNQ5vA0MTTTzpovdszGp964,4467
23
+ contentgrid_extension_helpers/responses/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ contentgrid_extension_helpers/responses/hal.py,sha256=g7_lqBEEmSMYEqp0ZVTqT8xfMQXlT0e4_5_a7WcS1Qs,9234
25
+ contentgrid_extension_helpers/structured_output/model_deny.py,sha256=n2sEls0kyhvL8hHUKeo3_JQ_ZssyknIbIoDAOUvMVxc,1748
26
+ contentgrid_extension_helpers-0.0.3.dist-info/licenses/LICENSE,sha256=tk6n-p8lEmzLJg-O4052CkMgfUtt1q2Zoh1QLAyL7S8,555
27
+ contentgrid_extension_helpers-0.0.3.dist-info/METADATA,sha256=Wfpq2OD1UzL268ZxJEJ_9GotNg-qZU_5WkSvXU8r-oU,1574
28
+ contentgrid_extension_helpers-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ contentgrid_extension_helpers-0.0.3.dist-info/top_level.txt,sha256=yJGGofrNVsl5psVGO0vLFHO1610ob88GtB9zpvS8iIk,30
30
+ contentgrid_extension_helpers-0.0.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5