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.
Files changed (109) hide show
  1. coding_client/__init__.py +1 -0
  2. coding_client/api/__init__.py +19 -0
  3. coding_client/api/exception_handlers.py +158 -0
  4. coding_client/api/logging_setup.py +44 -0
  5. coding_client/api/metrics.py +40 -0
  6. coding_client/api/openapi_config.py +124 -0
  7. coding_client/api/request_logging.py +60 -0
  8. coding_client/api/validation.py +150 -0
  9. coding_client/auth/__init__.py +78 -0
  10. coding_client/auth/_default_tenant.py +132 -0
  11. coding_client/auth/_header_trust.py +98 -0
  12. coding_client/auth/_onbehalf.py +315 -0
  13. coding_client/auth/_trusted_networks.py +94 -0
  14. coding_client/auth/dependencies.py +61 -0
  15. coding_client/auth/middleware.py +445 -0
  16. coding_client/auth/models.py +83 -0
  17. coding_client/auth/outbound.py +137 -0
  18. coding_client/auth/rbac.py +21 -0
  19. coding_client/auth/rbac_coding.py +5 -0
  20. coding_client/auth/tenant.py +650 -0
  21. coding_client/auth/websocket.py +148 -0
  22. coding_client/clients/__init__.py +24 -0
  23. coding_client/clients/base.py +131 -0
  24. coding_client/clients/coding_ai.py +33 -0
  25. coding_client/clients/coding_analysis.py +36 -0
  26. coding_client/clients/coding_containers.py +45 -0
  27. coding_client/clients/coding_github.py +127 -0
  28. coding_client/clients/coding_realtime.py +127 -0
  29. coding_client/clients/coding_security.py +51 -0
  30. coding_client/clients/coding_testing.py +31 -0
  31. coding_client/clients/coding_workspaces.py +265 -0
  32. coding_client/config/__init__.py +12 -0
  33. coding_client/config/base_settings.py +61 -0
  34. coding_client/config/yaml_config_loader.py +172 -0
  35. coding_client/db/__init__.py +42 -0
  36. coding_client/db/db_types.py +122 -0
  37. coding_client/db/exceptions.py +59 -0
  38. coding_client/db/postgres_connection.py +572 -0
  39. coding_client/db/tenant_query_helper.py +417 -0
  40. coding_client/db_init.py +49 -0
  41. coding_client/exceptions/__init__.py +67 -0
  42. coding_client/exceptions/api_exceptions.py +395 -0
  43. coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
  44. coding_client/interfaces/__init__.py +22 -0
  45. coding_client/interfaces/cache_interface.py +179 -0
  46. coding_client/interfaces/container_interface.py +49 -0
  47. coding_client/interfaces/docker_interface.py +283 -0
  48. coding_client/interfaces/graph_interface.py +28 -0
  49. coding_client/interfaces/security_interface.py +72 -0
  50. coding_client/interfaces/webhook_interface.py +67 -0
  51. coding_client/llm/__init__.py +54 -0
  52. coding_client/llm/base_agent.py +195 -0
  53. coding_client/llm/context.py +121 -0
  54. coding_client/llm/responses.py +102 -0
  55. coding_client/llm/routing.py +82 -0
  56. coding_client/llm/settings.py +102 -0
  57. coding_client/models/__init__.py +62 -0
  58. coding_client/models/agent.py +38 -0
  59. coding_client/models/ai_test_failure.py +745 -0
  60. coding_client/models/analysis.py +330 -0
  61. coding_client/models/architecture.py +685 -0
  62. coding_client/models/bug.py +128 -0
  63. coding_client/models/code_graph.py +317 -0
  64. coding_client/models/container.py +241 -0
  65. coding_client/models/core.py +878 -0
  66. coding_client/models/docker_container.py +104 -0
  67. coding_client/models/github.py +293 -0
  68. coding_client/models/logging.py +29 -0
  69. coding_client/models/package_manager.py +257 -0
  70. coding_client/models/pr_validation.py +440 -0
  71. coding_client/models/pydantic_ai.py +395 -0
  72. coding_client/models/repo_scanner.py +249 -0
  73. coding_client/models/responses.py +431 -0
  74. coding_client/models/security.py +185 -0
  75. coding_client/models/static_analysis.py +508 -0
  76. coding_client/models/system.py +48 -0
  77. coding_client/models/test_executor.py +204 -0
  78. coding_client/models/testing.py +673 -0
  79. coding_client/models/testing_config.py +200 -0
  80. coding_client/models/validation.py +127 -0
  81. coding_client/models_init.py +272 -0
  82. coding_client/telemetry.py +195 -0
  83. coding_client/testing.py +180 -0
  84. coding_client/types/__init__.py +130 -0
  85. coding_client/types/constraint_types.py +276 -0
  86. coding_client/types/core_types.py +112 -0
  87. coding_client/types/state_machine.py +443 -0
  88. coding_client/utils/__init__.py +86 -0
  89. coding_client/utils/archive_utils.py +387 -0
  90. coding_client/utils/ast_debug.py +45 -0
  91. coding_client/utils/async_timeout.py +364 -0
  92. coding_client/utils/constants.py +518 -0
  93. coding_client/utils/content_processing.py +326 -0
  94. coding_client/utils/database.py +916 -0
  95. coding_client/utils/encoding_helper.py +222 -0
  96. coding_client/utils/enum_helpers.py +166 -0
  97. coding_client/utils/file_operations.py +177 -0
  98. coding_client/utils/filesystem_ops.py +528 -0
  99. coding_client/utils/http_client.py +562 -0
  100. coding_client/utils/path_utils.py +471 -0
  101. coding_client/utils/profiling.py +421 -0
  102. coding_client/utils/response_builder.py +240 -0
  103. coding_client/utils/retry_helper.py +712 -0
  104. coding_client/utils/security_validation.py +423 -0
  105. coding_client-1.21.2.dist-info/METADATA +234 -0
  106. coding_client-1.21.2.dist-info/RECORD +109 -0
  107. coding_client-1.21.2.dist-info/WHEEL +5 -0
  108. coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
  109. coding_client-1.21.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,423 @@
1
+ """File security and validation utilities.
2
+
3
+ This module provides utilities for file security validation, size checks,
4
+ hash calculations, and permission validation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import logging
11
+ import os
12
+ import stat
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from .constants import CHUNK_SIZE, MAX_FILE_SIZE
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class SecurityValidationError(Exception):
23
+ """Base exception for security validation operations."""
24
+
25
+ pass
26
+
27
+
28
+ class FileSizeError(SecurityValidationError):
29
+ """Raised when file size exceeds limits."""
30
+
31
+ pass
32
+
33
+
34
+ class FilePermissionError(SecurityValidationError):
35
+ """Raised when file permissions prevent operation."""
36
+
37
+ pass
38
+
39
+
40
+ class FileNotFoundError(SecurityValidationError):
41
+ """Raised when file is not found."""
42
+
43
+ pass
44
+
45
+
46
+ class InvalidPathError(SecurityValidationError):
47
+ """Raised when path is invalid or unsafe."""
48
+
49
+ pass
50
+
51
+
52
+ def validate_file_access(
53
+ file_path: str | Path,
54
+ max_size: int | None = MAX_FILE_SIZE,
55
+ check_readable: bool = True,
56
+ check_writable: bool = False,
57
+ ) -> None:
58
+ """Validate file access and basic security checks."""
59
+ path = Path(file_path)
60
+
61
+ # Check if file exists
62
+ if not path.exists():
63
+ raise FileNotFoundError(f"File not found: {path}")
64
+
65
+ # Check if it's actually a file
66
+ if not path.is_file():
67
+ raise InvalidPathError(f"Not a file: {path}")
68
+
69
+ # Check file size
70
+ if max_size is not None:
71
+ file_size = path.stat().st_size
72
+ if file_size > max_size:
73
+ raise FileSizeError(f"File too large: {file_size} bytes (max: {max_size})")
74
+
75
+ # Check permissions
76
+ if check_readable and not os.access(path, os.R_OK):
77
+ raise FilePermissionError(f"File not readable: {path}")
78
+
79
+ if check_writable and not os.access(path, os.W_OK):
80
+ raise FilePermissionError(f"File not writable: {path}")
81
+
82
+
83
+ def validate_directory_access(
84
+ dir_path: str | Path,
85
+ check_readable: bool = True,
86
+ check_writable: bool = False,
87
+ check_executable: bool = True,
88
+ ) -> None:
89
+ """Validate directory access and permissions."""
90
+ path = Path(dir_path)
91
+
92
+ # Check if directory exists
93
+ if not path.exists():
94
+ raise FileNotFoundError(f"Directory not found: {path}")
95
+
96
+ # Check if it's actually a directory
97
+ if not path.is_dir():
98
+ raise InvalidPathError(f"Not a directory: {path}")
99
+
100
+ # Check permissions
101
+ if check_readable and not os.access(path, os.R_OK):
102
+ raise FilePermissionError(f"Directory not readable: {path}")
103
+
104
+ if check_writable and not os.access(path, os.W_OK):
105
+ raise FilePermissionError(f"Directory not writable: {path}")
106
+
107
+ if check_executable and not os.access(path, os.X_OK):
108
+ raise FilePermissionError(f"Directory not executable: {path}")
109
+
110
+
111
+ def get_file_size(file_path: str | Path) -> int:
112
+ """Get file size in bytes."""
113
+ path = Path(file_path)
114
+ if not path.exists():
115
+ raise FileNotFoundError(f"File not found: {path}")
116
+
117
+ try:
118
+ return path.stat().st_size
119
+ except Exception as e:
120
+ raise SecurityValidationError(f"Failed to get file size: {e}") from e
121
+
122
+
123
+ def get_file_hash(
124
+ file_path: str | Path,
125
+ algorithm: str = "sha256",
126
+ chunk_size: int = CHUNK_SIZE,
127
+ ) -> str:
128
+ """Calculate file hash."""
129
+ path = Path(file_path)
130
+ if not path.exists():
131
+ raise FileNotFoundError(f"File not found: {path}")
132
+
133
+ try:
134
+ hash_obj = hashlib.new(algorithm)
135
+
136
+ with open(path, "rb") as f:
137
+ while chunk := f.read(chunk_size):
138
+ hash_obj.update(chunk)
139
+
140
+ return hash_obj.hexdigest()
141
+ except Exception as e:
142
+ raise SecurityValidationError(f"Failed to calculate hash: {e}") from e
143
+
144
+
145
+ def verify_file_hash(
146
+ file_path: str | Path,
147
+ expected_hash: str,
148
+ algorithm: str = "sha256",
149
+ ) -> bool:
150
+ """Verify file hash matches expected value."""
151
+ try:
152
+ actual_hash = get_file_hash(file_path, algorithm)
153
+ return actual_hash.lower() == expected_hash.lower()
154
+ except Exception as e:
155
+ raise SecurityValidationError(f"Hash verification failed: {e}") from e
156
+
157
+
158
+ def get_file_permissions(file_path: str | Path) -> dict[str, bool]:
159
+ """Get file permission details."""
160
+ path = Path(file_path)
161
+ if not path.exists():
162
+ raise FileNotFoundError(f"File not found: {path}")
163
+
164
+ try:
165
+ file_stat = path.stat()
166
+ mode = file_stat.st_mode
167
+
168
+ return {
169
+ "owner_read": bool(mode & stat.S_IRUSR),
170
+ "owner_write": bool(mode & stat.S_IWUSR),
171
+ "owner_execute": bool(mode & stat.S_IXUSR),
172
+ "group_read": bool(mode & stat.S_IRGRP),
173
+ "group_write": bool(mode & stat.S_IWGRP),
174
+ "group_execute": bool(mode & stat.S_IXGRP),
175
+ "other_read": bool(mode & stat.S_IROTH),
176
+ "other_write": bool(mode & stat.S_IWOTH),
177
+ "other_execute": bool(mode & stat.S_IXOTH),
178
+ "is_suid": bool(mode & stat.S_ISUID),
179
+ "is_sgid": bool(mode & stat.S_ISGID),
180
+ "is_sticky": bool(mode & stat.S_ISVTX),
181
+ }
182
+ except Exception as e:
183
+ raise SecurityValidationError(f"Failed to get permissions: {e}") from e
184
+
185
+
186
+ def is_safe_filename(filename: str) -> bool:
187
+ """Check if filename is safe (no path traversal, etc.)."""
188
+ # Check for empty or None
189
+ if not filename:
190
+ return False
191
+
192
+ # Check for path separators
193
+ if os.sep in filename or os.altsep and os.altsep in filename:
194
+ return False
195
+
196
+ # Check for current/parent directory references
197
+ if filename in (".", ".."):
198
+ return False
199
+
200
+ # Check for null bytes
201
+ if "\x00" in filename:
202
+ return False
203
+
204
+ # Check for control characters
205
+ if any(ord(char) < 32 for char in filename):
206
+ return False
207
+
208
+ # Check for reserved names on Windows
209
+ reserved_names = {
210
+ "CON",
211
+ "PRN",
212
+ "AUX",
213
+ "NUL",
214
+ "COM1",
215
+ "COM2",
216
+ "COM3",
217
+ "COM4",
218
+ "COM5",
219
+ "COM6",
220
+ "COM7",
221
+ "COM8",
222
+ "COM9",
223
+ "LPT1",
224
+ "LPT2",
225
+ "LPT3",
226
+ "LPT4",
227
+ "LPT5",
228
+ "LPT6",
229
+ "LPT7",
230
+ "LPT8",
231
+ "LPT9",
232
+ }
233
+
234
+ name_upper = filename.upper()
235
+ if name_upper in reserved_names or name_upper.split(".")[0] in reserved_names:
236
+ return False
237
+
238
+ return True
239
+
240
+
241
+ def sanitize_filename(filename: str, replacement: str = "_") -> str:
242
+ """Sanitize filename by replacing unsafe characters."""
243
+ if not filename:
244
+ return "unnamed"
245
+
246
+ # Replace path separators
247
+ safe_name = filename.replace(os.sep, replacement)
248
+ if os.altsep:
249
+ safe_name = safe_name.replace(os.altsep, replacement)
250
+
251
+ # Replace control characters and null bytes
252
+ safe_chars = []
253
+ for char in safe_name:
254
+ if ord(char) < 32 or char == "\x00":
255
+ safe_chars.append(replacement)
256
+ else:
257
+ safe_chars.append(char)
258
+
259
+ safe_name = "".join(safe_chars)
260
+
261
+ # Handle reserved names
262
+ reserved_names = {
263
+ "CON",
264
+ "PRN",
265
+ "AUX",
266
+ "NUL",
267
+ "COM1",
268
+ "COM2",
269
+ "COM3",
270
+ "COM4",
271
+ "COM5",
272
+ "COM6",
273
+ "COM7",
274
+ "COM8",
275
+ "COM9",
276
+ "LPT1",
277
+ "LPT2",
278
+ "LPT3",
279
+ "LPT4",
280
+ "LPT5",
281
+ "LPT6",
282
+ "LPT7",
283
+ "LPT8",
284
+ "LPT9",
285
+ }
286
+
287
+ name_upper = safe_name.upper()
288
+ if name_upper in reserved_names or name_upper.split(".")[0] in reserved_names:
289
+ safe_name = f"file_{safe_name}"
290
+
291
+ # Ensure not empty after sanitization
292
+ if not safe_name or safe_name == replacement:
293
+ safe_name = "unnamed"
294
+
295
+ return safe_name
296
+
297
+
298
+ def get_file_info(file_path: str | Path) -> dict[str, Any]:
299
+ """Get comprehensive file information including security details."""
300
+ path = Path(file_path)
301
+ if not path.exists():
302
+ raise FileNotFoundError(f"File not found: {path}")
303
+
304
+ try:
305
+ file_stat = path.stat()
306
+
307
+ return {
308
+ "path": str(path.absolute()),
309
+ "name": path.name,
310
+ "size": file_stat.st_size,
311
+ "created": datetime.fromtimestamp(file_stat.st_ctime),
312
+ "modified": datetime.fromtimestamp(file_stat.st_mtime),
313
+ "accessed": datetime.fromtimestamp(file_stat.st_atime),
314
+ "is_file": path.is_file(),
315
+ "is_dir": path.is_dir(),
316
+ "is_symlink": path.is_symlink(),
317
+ "suffix": path.suffix,
318
+ "suffixes": path.suffixes,
319
+ "stem": path.stem,
320
+ "parent": str(path.parent),
321
+ "permissions": get_file_permissions(path),
322
+ "owner_uid": file_stat.st_uid,
323
+ "group_gid": file_stat.st_gid,
324
+ "mode_octal": oct(file_stat.st_mode),
325
+ "inode": file_stat.st_ino,
326
+ "device": file_stat.st_dev,
327
+ "nlinks": file_stat.st_nlink,
328
+ }
329
+ except Exception as e:
330
+ raise SecurityValidationError(f"Failed to get file info: {e}") from e
331
+
332
+
333
+ def check_file_integrity(
334
+ file_path: str | Path,
335
+ expected_size: int | None = None,
336
+ expected_hash: str | None = None,
337
+ hash_algorithm: str = "sha256",
338
+ ) -> dict[str, bool | None]:
339
+ """Check file integrity against expected values."""
340
+ results = {
341
+ "exists": False,
342
+ "size_match": None,
343
+ "hash_match": None,
344
+ "overall_valid": False,
345
+ }
346
+
347
+ path = Path(file_path)
348
+
349
+ # Check if file exists
350
+ if not path.exists():
351
+ return results
352
+
353
+ results["exists"] = True
354
+
355
+ try:
356
+ # Check size if provided
357
+ if expected_size is not None:
358
+ actual_size = get_file_size(path)
359
+ results["size_match"] = actual_size == expected_size
360
+
361
+ # Check hash if provided
362
+ if expected_hash is not None:
363
+ results["hash_match"] = verify_file_hash(path, expected_hash, hash_algorithm)
364
+
365
+ # Overall validity
366
+ checks = [v for v in results.values() if v is not None and isinstance(v, bool)]
367
+ results["overall_valid"] = all(checks) if checks else True
368
+
369
+ return results
370
+ except Exception as e:
371
+ raise SecurityValidationError(f"Integrity check failed: {e}") from e
372
+
373
+
374
+ def is_executable_file(file_path: str | Path) -> bool:
375
+ """Check if file is executable."""
376
+ path = Path(file_path)
377
+ return path.exists() and os.access(path, os.X_OK)
378
+
379
+
380
+ def get_safe_temp_dir() -> Path:
381
+ """Get a safe temporary directory with appropriate permissions."""
382
+ import tempfile
383
+
384
+ try:
385
+ temp_dir = Path(tempfile.gettempdir())
386
+
387
+ # Verify temp directory is safe
388
+ if not temp_dir.exists():
389
+ raise SecurityValidationError("System temp directory doesn't exist")
390
+
391
+ if not os.access(temp_dir, os.R_OK | os.W_OK | os.X_OK):
392
+ raise SecurityValidationError("Insufficient permissions on temp directory")
393
+
394
+ return temp_dir
395
+ except Exception as e:
396
+ raise SecurityValidationError(f"Failed to get safe temp directory: {e}") from e
397
+
398
+
399
+ # Export all public functions
400
+ __all__ = [
401
+ # Exceptions
402
+ "SecurityValidationError",
403
+ "FileSizeError",
404
+ "FilePermissionError",
405
+ "FileNotFoundError",
406
+ "InvalidPathError",
407
+ # Validation
408
+ "validate_file_access",
409
+ "validate_directory_access",
410
+ # File info
411
+ "get_file_size",
412
+ "get_file_info",
413
+ "get_file_permissions",
414
+ # Hashing
415
+ "get_file_hash",
416
+ "verify_file_hash",
417
+ # Security checks
418
+ "is_safe_filename",
419
+ "sanitize_filename",
420
+ "check_file_integrity",
421
+ "is_executable_file",
422
+ "get_safe_temp_dir",
423
+ ]
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: coding-client
3
+ Version: 1.21.2
4
+ Summary: Shared library for coding-* microservices: models, auth, DB helpers, HTTP client
5
+ Author-email: Nonagentic <dev@nonagentic.ai>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://nonagentic.ai
8
+ Project-URL: Source, https://github.com/nonagenticai/coding-client
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: fastapi>=0.115.0
13
+ Requires-Dist: starlette>=1.0.1
14
+ Requires-Dist: pydantic>=2.12.5
15
+ Requires-Dist: pydantic-settings>=2.12.0
16
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
17
+ Requires-Dist: psycopg[binary,pool]>=3.2.0
18
+ Requires-Dist: httpx>=0.28.0
19
+ Requires-Dist: redis>=7.1.0
20
+ Requires-Dist: structlog>=24.4.0
21
+ Requires-Dist: PyJWT[crypto]>=2.15.1
22
+ Requires-Dist: cryptography>=46.0.6
23
+ Requires-Dist: opentelemetry-api>=1.45.1
24
+ Requires-Dist: opentelemetry-sdk>=1.24.0
25
+ Requires-Dist: tenacity>=8.2.0
26
+ Requires-Dist: aiofiles>=24.1.0
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: cachetools>=5.3.0
29
+ Requires-Dist: pydantic-ai<2.0.0,>=1.107.7
30
+ Requires-Dist: openai<3,>=1.0.0
31
+ Requires-Dist: pyasn1>=0.6.5
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
34
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
35
+ Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
36
+ Requires-Dist: ruff==0.17.4; extra == "dev"
37
+ Requires-Dist: ty==0.0.39; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # coding-client
41
+
42
+ [![CI](https://github.com/nonagenticai/coding-client/actions/workflows/ci.yml/badge.svg)](https://github.com/nonagenticai/coding-client/actions/workflows/ci.yml)
43
+ [![Release](https://img.shields.io/github/v/release/nonagenticai/coding-client)](https://github.com/nonagenticai/coding-client/releases)
44
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
45
+
46
+ Shared Python library for the coding-* microservices ecosystem — models, auth, database, service clients, and utilities.
47
+
48
+ | | |
49
+ |---|---|
50
+ | **Stack** | Python 3.14 · Pydantic 2.x · SQLAlchemy 2.x · psycopg 3.x · httpx · structlog |
51
+ | **Package** | `coding-client` (Apache-2.0) |
52
+ | **Used by** | coding-gateway, coding-analysis, coding-testing, coding-ai, coding-containers, coding-github, coding-security, coding-realtime |
53
+
54
+ ## What It Provides
55
+
56
+ **Models** (`coding_client.models`) — 23 domain-specific Pydantic models covering code graphs, architecture, analysis, testing, containers, GitHub, security, AI agents, bug tracking, package management, PR validation, and more.
57
+
58
+ **Auth** (`coding_client.auth`) — Istio header-based authentication middleware, RBAC helpers (`require_role`, `require_access_coding`), multi-tenant isolation (`TenantMiddleware`, `require_tenant`, `get_tenant_id`), and FastAPI dependencies (`get_current_user`).
59
+
60
+ **Database** (`coding_client.db`) — PostgreSQL connection management via PgBouncer, SQLite code graph database, transaction management (`TransactionManager`, `TransactionContext`), tenant-aware queries (`TenantQueryBuilder`), migrations, and 9 custom exceptions.
61
+
62
+ **Clients** (`coding_client.clients`) — service-to-service HTTP clients with retry and timeout, including the canonical `CodingWorkspacesClient` and the compatibility-only `CodingContainersClient`. All extend a common `ServiceClient` base.
63
+
64
+ **Utilities** (`coding_client.utils`) — 16 modules: file operations, filesystem ops, path utils, archive utils, async timeout, encoding helper, HTTP client, retry helper, profiling, response builder, content processing, database utils, constants, enum helpers, security validation, AST debug.
65
+
66
+ **Interfaces** (`coding_client.interfaces`) — 6 abstract contracts for dependency injection: `IGraphService`, `IDockerService`, `ISecurityService`, `IAnalysisCache`, `ICacheManager`, `IWebhookEventHandler`.
67
+
68
+ **Types** (`coding_client.types`) — Shared type definitions, enums, constraint types, and workflow state machine definitions.
69
+
70
+ **API** (`coding_client.api`) — FastAPI middleware (`RequestLoggingMiddleware`, `HealthCheckFilter`), exception handlers, and OpenAPI configuration helpers.
71
+
72
+ ## Installation
73
+
74
+ Consumed as a **git dependency**, pinned to a commit:
75
+
76
+ ```toml
77
+ [project]
78
+ dependencies = ["coding-client"]
79
+
80
+ [tool.uv.sources]
81
+ coding-client = { git = "https://github.com/nonagenticai/coding-client", rev = "<commit sha>" }
82
+ ```
83
+
84
+ ```bash
85
+ uv lock # records the pinned commit in uv.lock
86
+ uv sync
87
+ ```
88
+
89
+ No package index is involved, so this resolves identically on a laptop, in CI, and inside a
90
+ Docker build.
91
+
92
+ ## Quick Start
93
+
94
+ ```bash
95
+ uv sync
96
+ uv run pytest tests/ -v --cov=src
97
+ uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/
98
+ uv run ty check .
99
+ ```
100
+
101
+ ## Architecture
102
+
103
+ ```
104
+ src/
105
+ ├── __init__.py
106
+ ├── api/
107
+ │ ├── exception_handlers.py # FastAPI exception handlers
108
+ │ ├── openapi_config.py # OpenAPI customization
109
+ │ └── request_logging.py # Request logging middleware
110
+ ├── auth/
111
+ │ ├── dependencies.py # FastAPI auth dependencies (get_current_user)
112
+ │ ├── middleware.py # Istio header auth middleware
113
+ │ ├── models.py # User model
114
+ │ ├── rbac.py # Role-based access control
115
+ │ ├── rbac_coding.py # Coding-specific RBAC helpers
116
+ │ └── tenant.py # Multi-tenant isolation
117
+ ├── clients/
118
+ │ ├── base.py # ServiceClient base with retry/timeout
119
+ │ ├── coding_ai.py # coding-ai client
120
+ │ ├── coding_analysis.py # coding-analysis client
121
+ │ ├── coding_containers.py # coding-containers client
122
+ │ ├── coding_workspaces.py # canonical repository-workspace client
123
+ │ ├── coding_github.py # coding-github client
124
+ │ ├── coding_realtime.py # coding-realtime client
125
+ │ ├── coding_security.py # coding-security client
126
+ │ └── coding_testing.py # coding-testing client
127
+ ├── config/
128
+ │ └── yaml_config_loader.py # YAML config loading
129
+ ├── db/
130
+ │ ├── code_graph_database.py # SQLite graph DB
131
+ │ ├── database_path_manager.py # DB file path management
132
+ │ ├── db_types.py # Database type definitions
133
+ │ ├── exceptions.py # 9 custom DB exceptions
134
+ │ ├── migrations.py # Schema migration helpers
135
+ │ ├── models/
136
+ │ │ └── architecture.py # ORM models
137
+ │ ├── performance_optimizer.py # Query performance tuning
138
+ │ ├── postgres_connection.py # PostgreSQL via PgBouncer
139
+ │ ├── tenant_query_helper.py # Tenant-aware query builder
140
+ │ ├── transaction_manager.py # Transaction management
141
+ │ ├── transaction_mixin.py # Transaction mixin
142
+ │ └── transaction_patterns.py # Transaction patterns
143
+ ├── exceptions/
144
+ │ ├── api_exceptions.py # API exception hierarchy
145
+ │ └── pydantic_ai_exceptions.py # PydanticAI-specific exceptions
146
+ ├── interfaces/ # 6 abstract contracts
147
+ ├── models/ # 23 Pydantic model files
148
+ ├── types/
149
+ │ ├── constraint_types.py # Validation constraints
150
+ │ ├── core_types.py # Enums and type hints
151
+ │ └── state_machine.py # Workflow state definitions
152
+ └── utils/ # 16 utility modules
153
+ ```
154
+
155
+ ## Tests
156
+
157
+ ```bash
158
+ uv run pytest tests/ -v --cov=src --cov-report=term-missing # 29 test files
159
+ uv run ruff check src/ tests/ # Lint
160
+ uv run ruff format --check src/ tests/ # Format check
161
+ uv run ty check . # Type check
162
+ ```
163
+
164
+ Test structure mirrors source: `test_api/`, `test_auth/`, `test_clients/`, `test_config/`, `test_db/`, `test_exceptions/`, `test_models/`, `test_types/`, `test_utils/`.
165
+
166
+ The suite needs no database. ⛔ `coding_client.db_init` has **no default DSN** (it used to
167
+ fall back to `localhost:5432/code_graph_helper`, a host Postgres deleted 2026-10-21):
168
+ importing it without `DATABASE_URL` / `TEST_DATABASE_URL` raises `RuntimeError` naming the
169
+ variable. A consumer that wants a test DB starts an ephemeral `postgres:18` container on a
170
+ random port — never a host `:5432`.
171
+
172
+ ### Bypassing auth in a consumer's tests
173
+
174
+ `SKIP_AUTH=true` was **removed in 1.16.0**. It made `require_tenant`,
175
+ `optional_tenant` and `get_current_user_with_tenant` return the hardcoded tenant
176
+ `"test-tenant"` before examining any credential — an ambient, global switch
177
+ compiled into the production wheel, where one environment variable disabled
178
+ authentication for a whole service.
179
+
180
+ Use `coding_client.testing` instead. Same job for a test suite; per-app,
181
+ explicit, and impossible to switch on from the environment:
182
+
183
+ ```python
184
+ from coding_client.testing import override_tenant_auth
185
+
186
+ @pytest.fixture
187
+ def client(app):
188
+ undo = override_tenant_auth(app) # or: with tenant_auth_override(app):
189
+ yield TestClient(app)
190
+ undo() # restores the previous overrides exactly
191
+ ```
192
+
193
+ It neutralises **every** identity dependency this SDK exposes, from one list.
194
+ That matters: hand-rolled override lists drift, and one in this fleet already
195
+ had — a consumer's conftest overrode three dependencies and not `require_tenant`,
196
+ and only worked because it also set `SKIP_AUTH`.
197
+
198
+ Pass `extra={dep: replacement}` for a service's own forked dependency object or a
199
+ service-specific return shape.
200
+
201
+ ## CI
202
+
203
+ `.github/workflows/ci.yml` calls the public `nonagenticai/ci-workflows` python-library pipeline:
204
+ `ruff + ty + unit tests → uv build → bandit + pip-audit`, on GitHub-hosted runners with no
205
+ repository secrets. The wheel is a build artifact only — nothing is published to a package index;
206
+ consumers pin the git commit.
207
+
208
+ ## Dependencies
209
+
210
+ | Category | Packages |
211
+ |----------|----------|
212
+ | **Web/API** | fastapi, httpx, PyJWT[crypto] |
213
+ | **Data** | pydantic, pydantic-settings, sqlalchemy[asyncio] |
214
+ | **Database** | psycopg[binary,pool] (PostgreSQL v3.x), aiosqlite |
215
+ | **Async** | redis, tenacity, aiofiles |
216
+ | **Observability** | structlog, opentelemetry-api, opentelemetry-sdk |
217
+ | **Security** | cryptography |
218
+ | **Utilities** | uuid-v7, cachetools, pyyaml |
219
+
220
+ ## Security
221
+
222
+ Report vulnerabilities privately through the repository's Security tab — see
223
+ [`SECURITY.md`](SECURITY.md). Do not open a public issue for a security problem.
224
+
225
+ ## Contributing
226
+
227
+ Issues and pull requests are welcome. See
228
+ [`CONTRIBUTING.md`](https://github.com/nonagenticai/.github/blob/main/CONTRIBUTING.md) for the
229
+ organisation-wide rules; setup, test and lint commands are in this README. Commits carry DCO
230
+ sign-off.
231
+
232
+ ## License
233
+
234
+ Apache-2.0 — see [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).