coding-client 1.21.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.
- coding_client/__init__.py +1 -0
- coding_client/api/__init__.py +19 -0
- coding_client/api/exception_handlers.py +158 -0
- coding_client/api/logging_setup.py +44 -0
- coding_client/api/metrics.py +40 -0
- coding_client/api/openapi_config.py +124 -0
- coding_client/api/request_logging.py +60 -0
- coding_client/api/validation.py +150 -0
- coding_client/auth/__init__.py +78 -0
- coding_client/auth/_default_tenant.py +132 -0
- coding_client/auth/_header_trust.py +98 -0
- coding_client/auth/_onbehalf.py +315 -0
- coding_client/auth/_trusted_networks.py +94 -0
- coding_client/auth/dependencies.py +61 -0
- coding_client/auth/middleware.py +445 -0
- coding_client/auth/models.py +83 -0
- coding_client/auth/outbound.py +137 -0
- coding_client/auth/rbac.py +21 -0
- coding_client/auth/rbac_coding.py +5 -0
- coding_client/auth/tenant.py +650 -0
- coding_client/auth/websocket.py +148 -0
- coding_client/clients/__init__.py +24 -0
- coding_client/clients/base.py +131 -0
- coding_client/clients/coding_ai.py +33 -0
- coding_client/clients/coding_analysis.py +36 -0
- coding_client/clients/coding_containers.py +45 -0
- coding_client/clients/coding_github.py +127 -0
- coding_client/clients/coding_realtime.py +127 -0
- coding_client/clients/coding_security.py +51 -0
- coding_client/clients/coding_testing.py +31 -0
- coding_client/clients/coding_workspaces.py +265 -0
- coding_client/config/__init__.py +12 -0
- coding_client/config/base_settings.py +61 -0
- coding_client/config/yaml_config_loader.py +172 -0
- coding_client/db/__init__.py +42 -0
- coding_client/db/db_types.py +122 -0
- coding_client/db/exceptions.py +59 -0
- coding_client/db/postgres_connection.py +572 -0
- coding_client/db/tenant_query_helper.py +417 -0
- coding_client/db_init.py +49 -0
- coding_client/exceptions/__init__.py +67 -0
- coding_client/exceptions/api_exceptions.py +395 -0
- coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
- coding_client/interfaces/__init__.py +22 -0
- coding_client/interfaces/cache_interface.py +179 -0
- coding_client/interfaces/container_interface.py +49 -0
- coding_client/interfaces/docker_interface.py +283 -0
- coding_client/interfaces/graph_interface.py +28 -0
- coding_client/interfaces/security_interface.py +72 -0
- coding_client/interfaces/webhook_interface.py +67 -0
- coding_client/llm/__init__.py +54 -0
- coding_client/llm/base_agent.py +195 -0
- coding_client/llm/context.py +121 -0
- coding_client/llm/responses.py +102 -0
- coding_client/llm/routing.py +82 -0
- coding_client/llm/settings.py +102 -0
- coding_client/models/__init__.py +62 -0
- coding_client/models/agent.py +38 -0
- coding_client/models/ai_test_failure.py +745 -0
- coding_client/models/analysis.py +330 -0
- coding_client/models/architecture.py +685 -0
- coding_client/models/bug.py +128 -0
- coding_client/models/code_graph.py +317 -0
- coding_client/models/container.py +241 -0
- coding_client/models/core.py +878 -0
- coding_client/models/docker_container.py +104 -0
- coding_client/models/github.py +293 -0
- coding_client/models/logging.py +29 -0
- coding_client/models/package_manager.py +257 -0
- coding_client/models/pr_validation.py +440 -0
- coding_client/models/pydantic_ai.py +395 -0
- coding_client/models/repo_scanner.py +249 -0
- coding_client/models/responses.py +431 -0
- coding_client/models/security.py +185 -0
- coding_client/models/static_analysis.py +508 -0
- coding_client/models/system.py +48 -0
- coding_client/models/test_executor.py +204 -0
- coding_client/models/testing.py +673 -0
- coding_client/models/testing_config.py +200 -0
- coding_client/models/validation.py +127 -0
- coding_client/models_init.py +272 -0
- coding_client/telemetry.py +195 -0
- coding_client/testing.py +180 -0
- coding_client/types/__init__.py +130 -0
- coding_client/types/constraint_types.py +276 -0
- coding_client/types/core_types.py +112 -0
- coding_client/types/state_machine.py +443 -0
- coding_client/utils/__init__.py +86 -0
- coding_client/utils/archive_utils.py +387 -0
- coding_client/utils/ast_debug.py +45 -0
- coding_client/utils/async_timeout.py +364 -0
- coding_client/utils/constants.py +518 -0
- coding_client/utils/content_processing.py +326 -0
- coding_client/utils/database.py +916 -0
- coding_client/utils/encoding_helper.py +222 -0
- coding_client/utils/enum_helpers.py +166 -0
- coding_client/utils/file_operations.py +177 -0
- coding_client/utils/filesystem_ops.py +528 -0
- coding_client/utils/http_client.py +562 -0
- coding_client/utils/path_utils.py +471 -0
- coding_client/utils/profiling.py +421 -0
- coding_client/utils/response_builder.py +240 -0
- coding_client/utils/retry_helper.py +712 -0
- coding_client/utils/security_validation.py +423 -0
- coding_client-1.21.2.dist-info/METADATA +234 -0
- coding_client-1.21.2.dist-info/RECORD +109 -0
- coding_client-1.21.2.dist-info/WHEEL +5 -0
- coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
- coding_client-1.21.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tenant Query Helper for Database Operations.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for adding tenant isolation to database queries.
|
|
5
|
+
It ensures that all queries properly filter by tenant_id to prevent BOLA vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from db.tenant_query_helper import (
|
|
9
|
+
add_tenant_filter,
|
|
10
|
+
build_tenant_query,
|
|
11
|
+
TenantQueryBuilder,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Simple case - add tenant filter to existing query
|
|
15
|
+
query = add_tenant_filter("SELECT * FROM nodes", "tenant-123")
|
|
16
|
+
# Result: "SELECT * FROM nodes WHERE tenant_id = ?"
|
|
17
|
+
|
|
18
|
+
# Complex case - with existing WHERE clause
|
|
19
|
+
query = add_tenant_filter(
|
|
20
|
+
"SELECT * FROM nodes WHERE type = ?",
|
|
21
|
+
"tenant-123"
|
|
22
|
+
)
|
|
23
|
+
# Result: "SELECT * FROM nodes WHERE tenant_id = ? AND type = ?"
|
|
24
|
+
|
|
25
|
+
# Using the builder for more control
|
|
26
|
+
builder = TenantQueryBuilder("nodes", "tenant-123")
|
|
27
|
+
query, params = builder.select().where("type = ?", "function").build()
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
import re
|
|
34
|
+
from typing import Any, Protocol, runtime_checkable
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@runtime_checkable
|
|
38
|
+
class _HasTenantId(Protocol):
|
|
39
|
+
"""Protocol for objects that provide at minimum a tenant_id attribute."""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def tenant_id(self) -> str: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _extract_tenant_id(tenant: str) -> str:
|
|
49
|
+
"""Extract tenant_id string from a string value.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
tenant: A tenant_id string.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The tenant_id string.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If tenant_id is empty or None.
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(tenant, str):
|
|
61
|
+
if not tenant:
|
|
62
|
+
raise ValueError("tenant_id cannot be empty")
|
|
63
|
+
return tenant
|
|
64
|
+
|
|
65
|
+
# For TenantContext-like objects, try to get tenant_id attribute
|
|
66
|
+
if hasattr(tenant, "tenant_id"):
|
|
67
|
+
return tenant.tenant_id
|
|
68
|
+
|
|
69
|
+
raise ValueError(f"Cannot extract tenant_id from {type(tenant)}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Default tenant ID for legacy data (matches migration 003)
|
|
73
|
+
LEGACY_TENANT_ID = "legacy"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def add_tenant_filter(
|
|
77
|
+
query: str,
|
|
78
|
+
tenant: str,
|
|
79
|
+
table_alias: str = "",
|
|
80
|
+
param_placeholder: str = "?",
|
|
81
|
+
) -> tuple[str, list[Any]]:
|
|
82
|
+
"""
|
|
83
|
+
Add tenant_id filter to an existing SQL query.
|
|
84
|
+
|
|
85
|
+
This function handles different query structures:
|
|
86
|
+
- Queries without WHERE clause: adds WHERE tenant_id = ?
|
|
87
|
+
- Queries with WHERE clause: adds AND tenant_id = ? after WHERE
|
|
88
|
+
- Queries with ORDER BY/LIMIT: inserts filter before those clauses
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
query: The SQL query to modify
|
|
92
|
+
tenant: The tenant ID string
|
|
93
|
+
table_alias: Optional table alias prefix (e.g., "n" for "n.tenant_id")
|
|
94
|
+
param_placeholder: Placeholder for parameters (default "?" for SQLite)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Tuple of (modified_query, [tenant_id]) for use with parameterized queries
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> query, params = add_tenant_filter("SELECT * FROM nodes", "tenant-123")
|
|
101
|
+
>>> cursor.execute(query, params)
|
|
102
|
+
"""
|
|
103
|
+
tenant_id = _extract_tenant_id(tenant)
|
|
104
|
+
if not tenant_id:
|
|
105
|
+
logger.warning("add_tenant_filter called with empty tenant_id")
|
|
106
|
+
return query, []
|
|
107
|
+
|
|
108
|
+
# Build the tenant column reference
|
|
109
|
+
tenant_col = f"{table_alias}.tenant_id" if table_alias else "tenant_id"
|
|
110
|
+
|
|
111
|
+
# Normalize query for analysis
|
|
112
|
+
query_upper = query.upper()
|
|
113
|
+
|
|
114
|
+
# Find the position of WHERE if it exists
|
|
115
|
+
where_pos = -1
|
|
116
|
+
where_match = re.search(r"\bWHERE\b", query_upper)
|
|
117
|
+
if where_match:
|
|
118
|
+
where_pos = where_match.start()
|
|
119
|
+
|
|
120
|
+
# Find positions of ORDER BY, GROUP BY, LIMIT, etc.
|
|
121
|
+
end_markers = ["ORDER BY", "GROUP BY", "LIMIT", "OFFSET", "HAVING"]
|
|
122
|
+
end_pos = len(query)
|
|
123
|
+
for marker in end_markers:
|
|
124
|
+
marker_match = re.search(rf"\b{marker}\b", query_upper)
|
|
125
|
+
if marker_match and marker_match.start() < end_pos:
|
|
126
|
+
# Only consider markers after WHERE
|
|
127
|
+
if where_pos == -1 or marker_match.start() > where_pos:
|
|
128
|
+
end_pos = marker_match.start()
|
|
129
|
+
|
|
130
|
+
# Build the tenant filter
|
|
131
|
+
tenant_filter = f"{tenant_col} = {param_placeholder}"
|
|
132
|
+
|
|
133
|
+
if where_pos == -1:
|
|
134
|
+
# No WHERE clause - insert WHERE before end markers
|
|
135
|
+
modified_query = query[:end_pos].rstrip() + f" WHERE {tenant_filter} " + query[end_pos:]
|
|
136
|
+
else:
|
|
137
|
+
# WHERE clause exists - insert AND after WHERE
|
|
138
|
+
# Find the end of "WHERE " to insert after it
|
|
139
|
+
assert where_match is not None # guaranteed by where_pos != -1
|
|
140
|
+
where_end = where_match.end()
|
|
141
|
+
# Insert the tenant filter right after WHERE, with AND connecting to existing conditions
|
|
142
|
+
modified_query = query[:where_end] + f" {tenant_filter} AND " + query[where_end:].lstrip()
|
|
143
|
+
|
|
144
|
+
return modified_query.strip(), [tenant_id]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_tenant_query(
|
|
148
|
+
base_query: str,
|
|
149
|
+
tenant: str,
|
|
150
|
+
existing_params: list[Any] | None = None,
|
|
151
|
+
table_alias: str = "",
|
|
152
|
+
) -> tuple[str, list[Any]]:
|
|
153
|
+
"""
|
|
154
|
+
Build a complete query with tenant filtering and combine parameters.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
base_query: The SQL query to modify
|
|
158
|
+
tenant: The tenant ID string
|
|
159
|
+
existing_params: Existing query parameters
|
|
160
|
+
table_alias: Optional table alias prefix
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Tuple of (modified_query, combined_params)
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> query, params = build_tenant_query(
|
|
167
|
+
... "SELECT * FROM nodes WHERE type = ?",
|
|
168
|
+
... "tenant-123",
|
|
169
|
+
... existing_params=["function"]
|
|
170
|
+
... )
|
|
171
|
+
>>> # query = "SELECT * FROM nodes WHERE tenant_id = ? AND type = ?"
|
|
172
|
+
>>> # params = ["tenant-123", "function"]
|
|
173
|
+
"""
|
|
174
|
+
if existing_params is None:
|
|
175
|
+
existing_params = []
|
|
176
|
+
|
|
177
|
+
modified_query, tenant_params = add_tenant_filter(base_query, tenant, table_alias)
|
|
178
|
+
|
|
179
|
+
# Tenant ID should be the first parameter for WHERE tenant_id = ? AND ...
|
|
180
|
+
combined_params = tenant_params + list(existing_params)
|
|
181
|
+
|
|
182
|
+
return modified_query, combined_params
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TenantQueryBuilder:
|
|
186
|
+
"""
|
|
187
|
+
Fluent query builder with automatic tenant isolation.
|
|
188
|
+
|
|
189
|
+
This builder ensures all queries include proper tenant filtering
|
|
190
|
+
and helps construct complex queries with correct parameter ordering.
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
builder = TenantQueryBuilder("nodes", "tenant-123")
|
|
194
|
+
query, params = (
|
|
195
|
+
builder
|
|
196
|
+
.select("id", "type", "properties")
|
|
197
|
+
.where("type = ?", "function")
|
|
198
|
+
.where("json_extract(properties, '$.name') LIKE ?", "%handler%")
|
|
199
|
+
.order_by("created_at DESC")
|
|
200
|
+
.limit(50)
|
|
201
|
+
.offset(0)
|
|
202
|
+
.build()
|
|
203
|
+
)
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
table_name: str,
|
|
209
|
+
tenant: str,
|
|
210
|
+
table_alias: str = "",
|
|
211
|
+
):
|
|
212
|
+
"""
|
|
213
|
+
Initialize the query builder.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
table_name: Name of the primary table
|
|
217
|
+
tenant: Tenant ID string for isolation
|
|
218
|
+
table_alias: Optional alias for the table
|
|
219
|
+
"""
|
|
220
|
+
self.table_name = table_name
|
|
221
|
+
self.tenant_id = _extract_tenant_id(tenant)
|
|
222
|
+
self.table_alias = table_alias
|
|
223
|
+
|
|
224
|
+
self._columns: list[str] = ["*"]
|
|
225
|
+
self._joins: list[str] = []
|
|
226
|
+
self._where_conditions: list[str] = []
|
|
227
|
+
self._where_params: list[Any] = []
|
|
228
|
+
self._group_by: str | None = None
|
|
229
|
+
self._order_by: str | None = None
|
|
230
|
+
self._limit: int | None = None
|
|
231
|
+
self._offset: int | None = None
|
|
232
|
+
|
|
233
|
+
def select(self, *columns: str) -> TenantQueryBuilder:
|
|
234
|
+
"""Specify columns to select."""
|
|
235
|
+
if columns:
|
|
236
|
+
self._columns = list(columns)
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
def join(self, join_clause: str) -> TenantQueryBuilder:
|
|
240
|
+
"""Add a JOIN clause."""
|
|
241
|
+
self._joins.append(join_clause)
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
def where(self, condition: str, *params: Any) -> TenantQueryBuilder:
|
|
245
|
+
"""Add a WHERE condition with parameters."""
|
|
246
|
+
self._where_conditions.append(condition)
|
|
247
|
+
self._where_params.extend(params)
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def group_by(self, *columns: str) -> TenantQueryBuilder:
|
|
251
|
+
"""Add GROUP BY clause."""
|
|
252
|
+
self._group_by = ", ".join(columns)
|
|
253
|
+
return self
|
|
254
|
+
|
|
255
|
+
def order_by(self, *columns: str) -> TenantQueryBuilder:
|
|
256
|
+
"""Add ORDER BY clause."""
|
|
257
|
+
self._order_by = ", ".join(columns)
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
def limit(self, limit: int) -> TenantQueryBuilder:
|
|
261
|
+
"""Add LIMIT clause."""
|
|
262
|
+
self._limit = limit
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
def offset(self, offset: int) -> TenantQueryBuilder:
|
|
266
|
+
"""Add OFFSET clause."""
|
|
267
|
+
self._offset = offset
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
def build(self) -> tuple[str, list[Any]]:
|
|
271
|
+
"""
|
|
272
|
+
Build the complete query with tenant isolation.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Tuple of (query_string, parameters_list)
|
|
276
|
+
"""
|
|
277
|
+
# Build SELECT clause
|
|
278
|
+
columns_str = ", ".join(self._columns)
|
|
279
|
+
table_ref = f"{self.table_name} {self.table_alias}" if self.table_alias else self.table_name
|
|
280
|
+
query = f"SELECT {columns_str} FROM {table_ref}"
|
|
281
|
+
|
|
282
|
+
# Add JOINs
|
|
283
|
+
if self._joins:
|
|
284
|
+
query += " " + " ".join(self._joins)
|
|
285
|
+
|
|
286
|
+
# Build WHERE clause with tenant isolation first
|
|
287
|
+
tenant_col = f"{self.table_alias}.tenant_id" if self.table_alias else "tenant_id"
|
|
288
|
+
|
|
289
|
+
where_parts = [f"{tenant_col} = ?"]
|
|
290
|
+
params: list[Any] = [self.tenant_id]
|
|
291
|
+
|
|
292
|
+
for condition in self._where_conditions:
|
|
293
|
+
where_parts.append(condition)
|
|
294
|
+
params.extend(self._where_params)
|
|
295
|
+
|
|
296
|
+
query += " WHERE " + " AND ".join(where_parts)
|
|
297
|
+
|
|
298
|
+
# Add GROUP BY
|
|
299
|
+
if self._group_by:
|
|
300
|
+
query += f" GROUP BY {self._group_by}"
|
|
301
|
+
|
|
302
|
+
# Add ORDER BY
|
|
303
|
+
if self._order_by:
|
|
304
|
+
query += f" ORDER BY {self._order_by}"
|
|
305
|
+
|
|
306
|
+
# Add LIMIT
|
|
307
|
+
if self._limit is not None:
|
|
308
|
+
query += f" LIMIT {self._limit}"
|
|
309
|
+
|
|
310
|
+
# Add OFFSET
|
|
311
|
+
if self._offset is not None:
|
|
312
|
+
query += f" OFFSET {self._offset}"
|
|
313
|
+
|
|
314
|
+
return query, params
|
|
315
|
+
|
|
316
|
+
def build_count(self) -> tuple[str, list[Any]]:
|
|
317
|
+
"""
|
|
318
|
+
Build a COUNT(*) query with tenant isolation.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Tuple of (query_string, parameters_list)
|
|
322
|
+
"""
|
|
323
|
+
table_ref = f"{self.table_name} {self.table_alias}" if self.table_alias else self.table_name
|
|
324
|
+
query = f"SELECT COUNT(*) FROM {table_ref}"
|
|
325
|
+
|
|
326
|
+
# Add JOINs
|
|
327
|
+
if self._joins:
|
|
328
|
+
query += " " + " ".join(self._joins)
|
|
329
|
+
|
|
330
|
+
# Build WHERE clause with tenant isolation
|
|
331
|
+
tenant_col = f"{self.table_alias}.tenant_id" if self.table_alias else "tenant_id"
|
|
332
|
+
|
|
333
|
+
where_parts = [f"{tenant_col} = ?"]
|
|
334
|
+
params: list[Any] = [self.tenant_id]
|
|
335
|
+
|
|
336
|
+
for condition in self._where_conditions:
|
|
337
|
+
where_parts.append(condition)
|
|
338
|
+
params.extend(self._where_params)
|
|
339
|
+
|
|
340
|
+
query += " WHERE " + " AND ".join(where_parts)
|
|
341
|
+
|
|
342
|
+
# Add GROUP BY for count queries if needed
|
|
343
|
+
if self._group_by:
|
|
344
|
+
query += f" GROUP BY {self._group_by}"
|
|
345
|
+
|
|
346
|
+
return query, params
|
|
347
|
+
|
|
348
|
+
def build_delete(self) -> tuple[str, list[Any]]:
|
|
349
|
+
"""
|
|
350
|
+
Build a DELETE query with tenant isolation.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Tuple of (query_string, parameters_list)
|
|
354
|
+
"""
|
|
355
|
+
query = f"DELETE FROM {self.table_name}"
|
|
356
|
+
|
|
357
|
+
# Build WHERE clause with tenant isolation
|
|
358
|
+
where_parts = ["tenant_id = ?"]
|
|
359
|
+
params: list[Any] = [self.tenant_id]
|
|
360
|
+
|
|
361
|
+
for condition in self._where_conditions:
|
|
362
|
+
where_parts.append(condition)
|
|
363
|
+
params.extend(self._where_params)
|
|
364
|
+
|
|
365
|
+
query += " WHERE " + " AND ".join(where_parts)
|
|
366
|
+
|
|
367
|
+
return query, params
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def validate_tenant_access(
|
|
371
|
+
resource_tenant_id: str | None,
|
|
372
|
+
user_tenant: str | _HasTenantId,
|
|
373
|
+
allow_legacy: bool | None = None,
|
|
374
|
+
) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Validate that a user has access to a resource based on tenant IDs.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
resource_tenant_id: The tenant_id of the resource
|
|
380
|
+
user_tenant: The user's tenant_id string or an object with a tenant_id attribute
|
|
381
|
+
allow_legacy: Whether to allow access to legacy (NULL) tenant data.
|
|
382
|
+
Defaults to True if not specified.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
True if access is allowed, False otherwise
|
|
386
|
+
"""
|
|
387
|
+
user_tenant_id: str
|
|
388
|
+
# Handle objects with tenant_id attribute (e.g. TenantContext)
|
|
389
|
+
if isinstance(user_tenant, _HasTenantId):
|
|
390
|
+
if allow_legacy is None:
|
|
391
|
+
allow_legacy_attr: Any = getattr(user_tenant, "allow_legacy_access", None)
|
|
392
|
+
if allow_legacy_attr is not None:
|
|
393
|
+
allow_legacy = bool(allow_legacy_attr)
|
|
394
|
+
can_access_method: Any = getattr(user_tenant, "can_access", None)
|
|
395
|
+
if callable(can_access_method):
|
|
396
|
+
result: bool = can_access_method(resource_tenant_id)
|
|
397
|
+
return result
|
|
398
|
+
user_tenant_id = user_tenant.tenant_id
|
|
399
|
+
else:
|
|
400
|
+
user_tenant_id = user_tenant
|
|
401
|
+
|
|
402
|
+
if allow_legacy is None:
|
|
403
|
+
allow_legacy = True
|
|
404
|
+
|
|
405
|
+
if not user_tenant_id:
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
if resource_tenant_id is None:
|
|
409
|
+
# NULL tenant_id - allow if legacy access is permitted
|
|
410
|
+
return allow_legacy
|
|
411
|
+
|
|
412
|
+
if resource_tenant_id == LEGACY_TENANT_ID:
|
|
413
|
+
# Legacy tenant data - allow if legacy access is permitted
|
|
414
|
+
return allow_legacy
|
|
415
|
+
|
|
416
|
+
# Normal case - must match exactly
|
|
417
|
+
return resource_tenant_id == user_tenant_id
|
coding_client/db_init.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database configuration and session management for code_graph_helper.
|
|
3
|
+
|
|
4
|
+
This module defines a global SQLAlchemy async engine for PostgreSQL.
|
|
5
|
+
The database URL is pulled from the DATABASE_URL environment variable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from collections.abc import AsyncGenerator
|
|
10
|
+
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
12
|
+
from sqlalchemy.orm import declarative_base
|
|
13
|
+
|
|
14
|
+
# Grab the database URL from the environment.
|
|
15
|
+
#
|
|
16
|
+
# ⛔ NO DEFAULT. This used to fall back to
|
|
17
|
+
# ``postgres:postgres@localhost:5432/code_graph_helper`` -- a host Postgres that no
|
|
18
|
+
# longer exists (George ruling 2026-10-21: no service or test defaults to a host
|
|
19
|
+
# :5432). Importing this module without DATABASE_URL (or TEST_DATABASE_URL) now
|
|
20
|
+
# refuses with a message naming the variable, instead of building an engine that
|
|
21
|
+
# points at nothing. (As of 2026-10-21 no consumer of the coding-client package
|
|
22
|
+
# imports this module; coding-repo-jobs, the only one, uses its own engine.)
|
|
23
|
+
DATABASE_URL = os.getenv("DATABASE_URL") or os.getenv("TEST_DATABASE_URL")
|
|
24
|
+
|
|
25
|
+
if not DATABASE_URL:
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
"DATABASE_URL is not set -- coding_client.db_init has no default database. "
|
|
28
|
+
"Set DATABASE_URL=postgresql+psycopg://<user>:<pw>@<host>:<port>/<db> "
|
|
29
|
+
"(or TEST_DATABASE_URL in tests)."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Convert postgresql:// to postgresql+psycopg:// for async psycopg3 driver
|
|
33
|
+
if DATABASE_URL.startswith("postgresql://"):
|
|
34
|
+
DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+psycopg://", 1)
|
|
35
|
+
|
|
36
|
+
# Create the async engine
|
|
37
|
+
engine = create_async_engine(DATABASE_URL, future=True, echo=False)
|
|
38
|
+
|
|
39
|
+
# Factory for creating new async sessions
|
|
40
|
+
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
|
41
|
+
|
|
42
|
+
# Base class for declarative models
|
|
43
|
+
Base = declarative_base()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def get_session() -> AsyncGenerator[AsyncSession]:
|
|
47
|
+
"""Yield a database session for dependency injection."""
|
|
48
|
+
async with AsyncSessionLocal() as session:
|
|
49
|
+
yield session
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Custom exceptions for coding-client.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- API exceptions (HTTP-based, auto-convert to error responses)
|
|
5
|
+
- PydanticAI exceptions (for AI analysis operations)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .api_exceptions import (
|
|
9
|
+
AgentConfigurationError,
|
|
10
|
+
AITimeoutError,
|
|
11
|
+
AIValidationError,
|
|
12
|
+
AuthenticationException,
|
|
13
|
+
BaseAPIException,
|
|
14
|
+
ConflictException,
|
|
15
|
+
ContainerNotFoundException,
|
|
16
|
+
InternalServerException,
|
|
17
|
+
NotFoundException,
|
|
18
|
+
PermissionException,
|
|
19
|
+
ProcessingException,
|
|
20
|
+
RateLimitException,
|
|
21
|
+
ServiceUnavailableException,
|
|
22
|
+
ValidationException,
|
|
23
|
+
)
|
|
24
|
+
from .pydantic_ai_exceptions import (
|
|
25
|
+
AnalysisError,
|
|
26
|
+
APIKeyError,
|
|
27
|
+
LLMEmptyResponseError,
|
|
28
|
+
ModelNotFoundError,
|
|
29
|
+
ModelProviderError,
|
|
30
|
+
PydanticAIException,
|
|
31
|
+
RateLimitError,
|
|
32
|
+
TokenLimitError,
|
|
33
|
+
create_error_response,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Avoid importing AgentConfigurationError from pydantic_ai_exceptions
|
|
37
|
+
# as it conflicts with the one from api_exceptions. Import directly
|
|
38
|
+
# from the submodule if needed:
|
|
39
|
+
# from coding_client.exceptions.pydantic_ai_exceptions import AgentConfigurationError as PydanticAIAgentConfigError
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# API Exceptions
|
|
43
|
+
"BaseAPIException",
|
|
44
|
+
"AgentConfigurationError",
|
|
45
|
+
"AITimeoutError",
|
|
46
|
+
"AIValidationError",
|
|
47
|
+
"AuthenticationException",
|
|
48
|
+
"ConflictException",
|
|
49
|
+
"ContainerNotFoundException",
|
|
50
|
+
"InternalServerException",
|
|
51
|
+
"NotFoundException",
|
|
52
|
+
"PermissionException",
|
|
53
|
+
"ProcessingException",
|
|
54
|
+
"RateLimitException",
|
|
55
|
+
"ServiceUnavailableException",
|
|
56
|
+
"ValidationException",
|
|
57
|
+
# PydanticAI Exceptions
|
|
58
|
+
"AnalysisError",
|
|
59
|
+
"APIKeyError",
|
|
60
|
+
"LLMEmptyResponseError",
|
|
61
|
+
"ModelNotFoundError",
|
|
62
|
+
"ModelProviderError",
|
|
63
|
+
"PydanticAIException",
|
|
64
|
+
"RateLimitError",
|
|
65
|
+
"TokenLimitError",
|
|
66
|
+
"create_error_response",
|
|
67
|
+
]
|