geek-cafe-saas-sdk 0.7.5__py3-none-any.whl → 0.8.0__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.
Potentially problematic release.
This version of geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +1 -1
- geek_cafe_saas_sdk/core/anonymous_context.py +321 -0
- geek_cafe_saas_sdk/core/request_context.py +184 -0
- geek_cafe_saas_sdk/decorators/__init__.py +1 -1
- geek_cafe_saas_sdk/decorators/auth.py +6 -6
- geek_cafe_saas_sdk/decorators/core.py +44 -44
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +15 -3
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +1 -4
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +0 -2
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +1 -3
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +3 -3
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +1 -2
- geek_cafe_saas_sdk/domains/events/services/event_service.py +6 -4
- geek_cafe_saas_sdk/domains/files/handlers/README.md +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/models/file.py +16 -6
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +34 -9
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +38 -3
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +33 -36
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +35 -2
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +20 -3
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +1 -3
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +1 -2
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +1 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +2 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +0 -2
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +3 -3
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +3 -3
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +10 -3
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +40 -6
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +157 -12
- geek_cafe_saas_sdk/middleware/authorization.py +1 -1
- geek_cafe_saas_sdk/middleware/cors.py +8 -8
- geek_cafe_saas_sdk/services/database_service.py +76 -5
- {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/METADATA +16 -16
- {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/RECORD +131 -129
- {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ from boto3_assist.dynamodb.dynamodb import DynamoDB, DynamoDBIndex
|
|
|
6
6
|
from ..core.service_result import ServiceResult
|
|
7
7
|
from ..core.service_errors import ValidationError, AccessDeniedError, NotFoundError
|
|
8
8
|
from ..core.error_codes import ErrorCode
|
|
9
|
+
from ..core.request_context import RequestContext
|
|
9
10
|
import os
|
|
10
11
|
from aws_lambda_powertools import Logger
|
|
11
12
|
|
|
@@ -17,7 +18,22 @@ logger = Logger()
|
|
|
17
18
|
class DatabaseService(ABC, Generic[T]):
|
|
18
19
|
"""Base service class for database operations."""
|
|
19
20
|
|
|
20
|
-
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
21
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None, request_context: RequestContext):
|
|
22
|
+
"""
|
|
23
|
+
Initialize DatabaseService.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
dynamodb: DynamoDB client instance
|
|
27
|
+
table_name: DynamoDB table name
|
|
28
|
+
request_context: **REQUIRED** Security context with JWT token (no longer optional)
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If table_name is not provided
|
|
32
|
+
TypeError: If request_context is not provided (enforced by Python)
|
|
33
|
+
"""
|
|
34
|
+
if request_context is None:
|
|
35
|
+
raise AccessDeniedError("request_context is required for all database operations. All services must have security context.")
|
|
36
|
+
|
|
21
37
|
self.dynamodb = dynamodb or DynamoDB()
|
|
22
38
|
self.table_name = (
|
|
23
39
|
table_name or os.getenv("DYNAMODB_TABLE_NAME")
|
|
@@ -27,6 +43,19 @@ class DatabaseService(ABC, Generic[T]):
|
|
|
27
43
|
raise ValueError("Table name is required")
|
|
28
44
|
|
|
29
45
|
self.LOG_DYNAMO_DB_QUERY = os.getenv("LOG_DYNAMO_DB_QUERY", False)
|
|
46
|
+
self._request_context = request_context
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def request_context(self) -> RequestContext:
|
|
50
|
+
"""Get the request context (security token)."""
|
|
51
|
+
if self._request_context is None:
|
|
52
|
+
raise AccessDeniedError("No security context set for this service")
|
|
53
|
+
return self._request_context
|
|
54
|
+
|
|
55
|
+
@request_context.setter
|
|
56
|
+
def request_context(self, value: RequestContext):
|
|
57
|
+
"""Set the request context (security token)."""
|
|
58
|
+
self._request_context = value
|
|
30
59
|
|
|
31
60
|
@abstractmethod
|
|
32
61
|
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[T]:
|
|
@@ -114,8 +143,45 @@ class DatabaseService(ABC, Generic[T]):
|
|
|
114
143
|
raise AccessDeniedError("Access denied to resource in different tenant")
|
|
115
144
|
|
|
116
145
|
def _save_model(self, model: T) -> ServiceResult[T]:
|
|
117
|
-
"""Save model to database with
|
|
146
|
+
"""Save model to database with **MANDATORY** security validation and audit trail.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
model: Model to save
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
ServiceResult with saved model or error
|
|
153
|
+
|
|
154
|
+
Security:
|
|
155
|
+
- **ALWAYS** validates tenant access (no longer optional)
|
|
156
|
+
- **ALWAYS** auto-populates audit fields (created_by, updated_by)
|
|
157
|
+
- **ALWAYS** prevents cross-tenant resource creation
|
|
158
|
+
- request_context is required (enforced in __init__)
|
|
159
|
+
"""
|
|
118
160
|
try:
|
|
161
|
+
# MANDATORY security validation - always validate tenant access
|
|
162
|
+
# EXCEPTION: SYSTEM tenant can provision new tenants (signup flow)
|
|
163
|
+
from geek_cafe_saas_sdk.core.anonymous_context import AnonymousContextFactory
|
|
164
|
+
is_system_tenant = (self._request_context.authenticated_tenant_id ==
|
|
165
|
+
AnonymousContextFactory.SYSTEM_TENANT_ID)
|
|
166
|
+
|
|
167
|
+
if hasattr(model, 'tenant_id') and model.tenant_id:
|
|
168
|
+
# SYSTEM tenant can create resources in any tenant (provisioning)
|
|
169
|
+
if not is_system_tenant:
|
|
170
|
+
if not self._request_context.validate_tenant_access(model.tenant_id):
|
|
171
|
+
return ServiceResult.error_result(
|
|
172
|
+
ErrorCode.ACCESS_DENIED,
|
|
173
|
+
"Cannot save resources in other tenants"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# MANDATORY audit trail - always populate audit fields
|
|
177
|
+
# Set created_by_id if this is a new resource
|
|
178
|
+
if hasattr(model, 'created_by_id') and not model.created_by_id:
|
|
179
|
+
model.created_by_id = self._request_context.authenticated_user_id
|
|
180
|
+
|
|
181
|
+
# Always update updated_by_id
|
|
182
|
+
if hasattr(model, 'updated_by_id'):
|
|
183
|
+
model.updated_by_id = self._request_context.authenticated_user_id
|
|
184
|
+
|
|
119
185
|
# The boto3_assist library handles all GSI key population automatically.
|
|
120
186
|
self.dynamodb.save(table_name=self.table_name, item=model)
|
|
121
187
|
return ServiceResult.success_result(model)
|
|
@@ -147,7 +213,7 @@ class DatabaseService(ABC, Generic[T]):
|
|
|
147
213
|
return None
|
|
148
214
|
|
|
149
215
|
def _get_model_by_id_with_tenant_check(
|
|
150
|
-
self, resource_id: str, model_class, tenant_id: str, include_deleted: bool = True
|
|
216
|
+
self, resource_id: str, model_class, tenant_id: Optional[str] = None, include_deleted: bool = True
|
|
151
217
|
) -> Optional[T]:
|
|
152
218
|
"""
|
|
153
219
|
Get model by ID with automatic tenant validation.
|
|
@@ -159,7 +225,7 @@ class DatabaseService(ABC, Generic[T]):
|
|
|
159
225
|
Args:
|
|
160
226
|
resource_id: The resource ID to fetch
|
|
161
227
|
model_class: The model class to instantiate
|
|
162
|
-
tenant_id: The tenant ID
|
|
228
|
+
tenant_id: The tenant ID to validate (uses request_context if not provided)
|
|
163
229
|
include_deleted: If True, returns deleted items. If False, returns None for deleted items.
|
|
164
230
|
Default is True since get-by-id operations typically need to verify deletion,
|
|
165
231
|
perform restores, or show audit history.
|
|
@@ -168,10 +234,15 @@ class DatabaseService(ABC, Generic[T]):
|
|
|
168
234
|
The model if found and belongs to tenant, None otherwise
|
|
169
235
|
|
|
170
236
|
Security:
|
|
237
|
+
- Automatically uses request_context if available
|
|
171
238
|
- Returns None for resources in different tenants (prevents enumeration)
|
|
172
239
|
- Optionally filters deleted resources based on include_deleted parameter
|
|
173
|
-
- Single source of truth: tenant_id from JWT only
|
|
174
240
|
"""
|
|
241
|
+
# Use request_context tenant if tenant_id not explicitly provided
|
|
242
|
+
# request_context is always present (enforced in __init__)
|
|
243
|
+
if tenant_id is None:
|
|
244
|
+
tenant_id = self._request_context.authenticated_tenant_id
|
|
245
|
+
|
|
175
246
|
model = self._get_model_by_id(resource_id, model_class)
|
|
176
247
|
|
|
177
248
|
if not model:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: geek_cafe_saas_sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Base Reusable Services for SaaS
|
|
5
5
|
Project-URL: Homepage, https://github.com/geekcafe/geek-cafe-services
|
|
6
6
|
Project-URL: Documentation, https://github.com/geekcafe/geek-cafe-services/blob/main/README.md
|
|
@@ -87,20 +87,20 @@ Description-Content-Type: text/markdown
|
|
|
87
87
|
<!-- COVERAGE-BADGE:START -->
|
|
88
88
|
## Test Coverage
|
|
89
89
|
|
|
90
|
-

|
|
91
|
+

|
|
92
92
|
|
|
93
|
-
**Overall Coverage:**
|
|
93
|
+
**Overall Coverage:** 82.5% (13150/15936 statements)
|
|
94
94
|
|
|
95
95
|
### Coverage Summary
|
|
96
96
|
|
|
97
97
|
| Metric | Value |
|
|
98
98
|
|--------|-------|
|
|
99
|
-
| Total Statements |
|
|
100
|
-
| Covered Statements |
|
|
101
|
-
| Missing Statements |
|
|
102
|
-
| Coverage Percentage |
|
|
103
|
-
| Total Tests |
|
|
99
|
+
| Total Statements | 15,936 |
|
|
100
|
+
| Covered Statements | 13,150 |
|
|
101
|
+
| Missing Statements | 2,786 |
|
|
102
|
+
| Coverage Percentage | 82.5% |
|
|
103
|
+
| Total Tests | 1296 |
|
|
104
104
|
| Test Status | ✅ All Passing |
|
|
105
105
|
|
|
106
106
|
### Files Needing Attention (< 80% coverage)
|
|
@@ -108,17 +108,17 @@ Description-Content-Type: text/markdown
|
|
|
108
108
|
| Coverage | Missing Lines | File |
|
|
109
109
|
|----------|---------------|------|
|
|
110
110
|
| 17.5% | 47 | `lambda_handlers/_base/authorized_secure_handler.py` |
|
|
111
|
-
|
|
|
111
|
+
| 24.4% | 133 | `domains/communities/services/community_member_service.py` |
|
|
112
|
+
| 46.2% | 85 | `domains/auth/services/resource_permission_service.py` |
|
|
112
113
|
| 46.3% | 58 | `domains/auth/models/role.py` |
|
|
113
114
|
| 46.9% | 34 | `domains/auth/models/permission.py` |
|
|
114
|
-
|
|
|
115
|
+
| 52.2% | 118 | `domains/tenancy/services/subscription_service.py` |
|
|
115
116
|
| 56.2% | 7 | `lambda_handlers/_base/secure_handler.py` |
|
|
116
117
|
| 58.5% | 17 | `domains/messaging/handlers/contact_threads/update/app.py` |
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
| 64.0% | 41 | `domains/files/services/s3_file_service.py` |
|
|
118
|
+
| 60.6% | 85 | `domains/notifications/services/notification_service.py` |
|
|
119
|
+
| 61.8% | 21 | `core/request_context.py` |
|
|
120
120
|
|
|
121
|
-
*... and
|
|
121
|
+
*... and 27 more files with < 80% coverage*
|
|
122
122
|
|
|
123
123
|
### Running Tests
|
|
124
124
|
|
|
@@ -130,7 +130,7 @@ Description-Content-Type: text/markdown
|
|
|
130
130
|
open reports/coverage/index.html
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
*Last updated: 2025-10-
|
|
133
|
+
*Last updated: 2025-10-20 09:57:49*
|
|
134
134
|
|
|
135
135
|
---
|
|
136
136
|
|