mca-sdk 0.1.dev20__py3-none-any.whl → 0.1.dev37__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.
- mca_sdk/__init__.py +1 -9
- mca_sdk/_version.py +8 -18
- mca_sdk/autolog/_base.py +33 -19
- mca_sdk/buffering/queue.py +27 -23
- mca_sdk/cli/_bootstrap.py +25 -0
- mca_sdk/cli/init.py +530 -72
- mca_sdk/cli/instrument.py +33 -2
- mca_sdk/cli/run.py +22 -2
- mca_sdk/config/settings.py +43 -39
- mca_sdk/core/client.py +470 -178
- mca_sdk/core/gcp_auth.py +1 -2
- mca_sdk/core/providers.py +65 -4
- mca_sdk/exporters/gcp_logging.py +12 -87
- mca_sdk/integrations/decorators.py +16 -9
- mca_sdk/integrations/genai.py +192 -177
- mca_sdk/integrations/litellm_callback.py +7 -6
- mca_sdk/registry/client.py +139 -31
- mca_sdk/registry/models.py +1 -1
- mca_sdk/security/certificate.py +3 -3
- mca_sdk/security/certificates.py +1 -1
- mca_sdk/security/encryption.py +4 -4
- mca_sdk/security/migration.py +3 -5
- mca_sdk/utils/phi_utils.py +10 -5
- mca_sdk/utils/serialization.py +266 -1
- mca_sdk/validation/__init__.py +15 -2
- mca_sdk/validation/attributes.py +114 -12
- mca_sdk/validation/telemetry_attributes.py +1 -2
- mca_sdk/version.py +17 -0
- {mca_sdk-0.1.dev20.dist-info → mca_sdk-0.1.dev37.dist-info}/METADATA +254 -159
- {mca_sdk-0.1.dev20.dist-info → mca_sdk-0.1.dev37.dist-info}/RECORD +34 -32
- {mca_sdk-0.1.dev20.dist-info → mca_sdk-0.1.dev37.dist-info}/WHEEL +1 -1
- mca_sdk-0.1.dev37.dist-info/entry_points.txt +4 -0
- {mca_sdk-0.1.dev20.dist-info → mca_sdk-0.1.dev37.dist-info}/licenses/LICENSE +0 -0
- {mca_sdk-0.1.dev20.dist-info → mca_sdk-0.1.dev37.dist-info}/top_level.txt +0 -0
mca_sdk/__init__.py
CHANGED
|
@@ -22,15 +22,7 @@ Example:
|
|
|
22
22
|
>>> client.shutdown()
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
from ._version import version as __version__
|
|
27
|
-
except ImportError:
|
|
28
|
-
try:
|
|
29
|
-
from importlib.metadata import version, PackageNotFoundError
|
|
30
|
-
|
|
31
|
-
__version__ = version("mca-sdk")
|
|
32
|
-
except (ImportError, PackageNotFoundError):
|
|
33
|
-
__version__ = "unknown"
|
|
25
|
+
from .version import __version__
|
|
34
26
|
|
|
35
27
|
from .core import MCAClient
|
|
36
28
|
from .config import MCAConfig
|
mca_sdk/_version.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
5
6
|
"__version__",
|
|
@@ -10,25 +11,14 @@ __all__ = [
|
|
|
10
11
|
"commit_id",
|
|
11
12
|
]
|
|
12
13
|
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
14
|
version: str
|
|
25
15
|
__version__: str
|
|
26
|
-
__version_tuple__:
|
|
27
|
-
version_tuple:
|
|
28
|
-
commit_id:
|
|
29
|
-
__commit_id__:
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
30
20
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1, '
|
|
21
|
+
__version__ = version = '0.1.dev37'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev37')
|
|
33
23
|
|
|
34
24
|
__commit_id__ = commit_id = None
|
mca_sdk/autolog/_base.py
CHANGED
|
@@ -171,36 +171,45 @@ class BasePatcher:
|
|
|
171
171
|
}
|
|
172
172
|
attrs.update(additional_attrs)
|
|
173
173
|
|
|
174
|
-
# Add input data if requested (with size limit)
|
|
175
|
-
if self.capture_input and args:
|
|
176
|
-
input_data = self._capture_data_safely(args[0], "input")
|
|
177
|
-
if input_data is not None:
|
|
178
|
-
attrs.update(input_data)
|
|
179
|
-
|
|
180
174
|
# Track timing manually for accurate latency metric
|
|
181
175
|
start_time = time.perf_counter()
|
|
182
176
|
|
|
177
|
+
# Story 8.5: Delegate to record_prediction() for unified schema
|
|
178
|
+
# Extract raw input/output data and let record_prediction() handle serialization
|
|
179
|
+
# Issue #8 Fix: Use same logic as decorator - single arg direct, else wrap
|
|
180
|
+
input_for_capture = None
|
|
181
|
+
if self.capture_input and (args or kwargs):
|
|
182
|
+
if len(args) == 1 and not kwargs:
|
|
183
|
+
# Single positional arg - pass directly
|
|
184
|
+
input_for_capture = args[0]
|
|
185
|
+
else:
|
|
186
|
+
# Multi-args, kwargs-only, or mixed - wrap everything to preserve all data
|
|
187
|
+
input_for_capture = {"args": args, "kwargs": kwargs}
|
|
188
|
+
|
|
189
|
+
output_for_capture = None # Set after method call
|
|
190
|
+
|
|
183
191
|
# Execute with instrumentation
|
|
184
192
|
try:
|
|
185
|
-
with client.trace(span_name, **attrs)
|
|
193
|
+
with client.trace(span_name, **attrs):
|
|
186
194
|
# Call the wrapped method (already bound to instance by wrapt)
|
|
187
195
|
result = original_method(*args, **kwargs)
|
|
188
196
|
|
|
189
197
|
# Calculate actual latency
|
|
190
198
|
latency_seconds = time.perf_counter() - start_time
|
|
191
199
|
|
|
192
|
-
#
|
|
200
|
+
# Capture output if requested
|
|
193
201
|
if self.capture_output:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
for key, value in output_data.items():
|
|
199
|
-
span.set_attribute(key, value)
|
|
200
|
-
|
|
201
|
-
# Record prediction metric with actual latency
|
|
202
|
+
output_for_capture = result
|
|
203
|
+
|
|
204
|
+
# Story 8.5: Delegate to record_prediction() with raw data
|
|
205
|
+
# This ensures consistent schema (data, shape, truncated) across all instrumentation methods
|
|
202
206
|
try:
|
|
203
|
-
client.record_prediction(
|
|
207
|
+
client.record_prediction(
|
|
208
|
+
input_data=input_for_capture,
|
|
209
|
+
output=output_for_capture,
|
|
210
|
+
latency=latency_seconds,
|
|
211
|
+
**attrs,
|
|
212
|
+
)
|
|
204
213
|
except Exception as metric_error:
|
|
205
214
|
logger.warning(f"Failed to record prediction metric: {metric_error}")
|
|
206
215
|
|
|
@@ -235,10 +244,15 @@ class BasePatcher:
|
|
|
235
244
|
# Log but don't crash if error recording fails
|
|
236
245
|
logger.warning(f"Failed to record exception in span: {span_error}")
|
|
237
246
|
|
|
247
|
+
# Story 8.5: Delegate to record_prediction() even for failed predictions
|
|
238
248
|
# Record failed prediction metric with actual latency
|
|
239
|
-
# This happens OUTSIDE the span try/except so it always executes
|
|
240
249
|
try:
|
|
241
|
-
client.record_prediction(
|
|
250
|
+
client.record_prediction(
|
|
251
|
+
input_data=input_for_capture,
|
|
252
|
+
output=None, # No output on error
|
|
253
|
+
latency=latency_seconds,
|
|
254
|
+
**attrs,
|
|
255
|
+
)
|
|
242
256
|
except Exception as metric_error:
|
|
243
257
|
logger.warning(f"Failed to record failed prediction metric: {metric_error}")
|
|
244
258
|
|
mca_sdk/buffering/queue.py
CHANGED
|
@@ -290,7 +290,7 @@ class TelemetryQueue:
|
|
|
290
290
|
f"Original: {path}, Expanded: {expanded}, Error: {type(e).__name__}"
|
|
291
291
|
)
|
|
292
292
|
raise BufferingError(
|
|
293
|
-
f"Invalid persist_path: {path}.
|
|
293
|
+
f"Invalid persist_path: {path}. Could not resolve to canonical path: {e}"
|
|
294
294
|
)
|
|
295
295
|
|
|
296
296
|
# Step 3: Check for path traversal in CANONICAL path (Finding #6)
|
|
@@ -333,7 +333,7 @@ class TelemetryQueue:
|
|
|
333
333
|
for prefix in sensitive_prefixes:
|
|
334
334
|
if canonical == prefix or canonical.startswith(prefix + "/"):
|
|
335
335
|
logger.warning(
|
|
336
|
-
f"SECURITY: Attempted write to sensitive directory.
|
|
336
|
+
f"SECURITY: Attempted write to sensitive directory. Path: {canonical}"
|
|
337
337
|
)
|
|
338
338
|
raise BufferingError(
|
|
339
339
|
f"Cannot persist queue to sensitive system directory: {canonical}. "
|
|
@@ -355,25 +355,25 @@ class TelemetryQueue:
|
|
|
355
355
|
# Add custom directories if provided, filtering empty strings (Finding #2)
|
|
356
356
|
if config and hasattr(config, "persist_allowed_dirs") and config.persist_allowed_dirs:
|
|
357
357
|
custom_dirs = [d.strip() for d in config.persist_allowed_dirs.split(",") if d.strip()]
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
358
|
+
static_allowed.extend(custom_dirs)
|
|
359
|
+
logger.debug(f"Using custom allowed directories: {custom_dirs}")
|
|
360
|
+
|
|
361
|
+
# Canonicalize all whitelist entries for consistent cross-platform comparison
|
|
362
|
+
canonical_allowed = [
|
|
363
|
+
os.path.realpath(os.path.expanduser(p), strict=False) for p in static_allowed
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
# Step 6: Whitelist validation using commonpath for proper directory boundary check
|
|
366
367
|
def _is_whitelisted(canonical_path: str, allowed: list) -> bool:
|
|
367
368
|
for prefix in allowed:
|
|
368
|
-
|
|
369
|
-
canonical_path == prefix
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return True
|
|
369
|
+
try:
|
|
370
|
+
if os.path.commonpath([canonical_path, prefix]) == prefix:
|
|
371
|
+
return True
|
|
372
|
+
except ValueError:
|
|
373
|
+
continue
|
|
374
374
|
return False
|
|
375
375
|
|
|
376
|
-
if not _is_whitelisted(canonical,
|
|
376
|
+
if not _is_whitelisted(canonical, canonical_allowed):
|
|
377
377
|
# Find closest valid directory for helpful error message (Finding #4)
|
|
378
378
|
def path_similarity(dir1: str, dir2: str) -> int:
|
|
379
379
|
"""Count matching path components"""
|
|
@@ -382,13 +382,13 @@ class TelemetryQueue:
|
|
|
382
382
|
return sum(1 for a, b in zip(parts1, parts2) if a == b)
|
|
383
383
|
|
|
384
384
|
sorted_allowed = sorted(
|
|
385
|
-
|
|
385
|
+
canonical_allowed,
|
|
386
386
|
key=lambda d: path_similarity(canonical, d),
|
|
387
387
|
reverse=True,
|
|
388
388
|
)
|
|
389
389
|
suggestion = sorted_allowed[0] if sorted_allowed else "~/.mca_sdk/"
|
|
390
390
|
|
|
391
|
-
logger.warning(f"SECURITY: Unauthorized persist_path.
|
|
391
|
+
logger.warning(f"SECURITY: Unauthorized persist_path. Attempted: {canonical}")
|
|
392
392
|
raise BufferingError(
|
|
393
393
|
f"Unauthorized persist_path: {path}. "
|
|
394
394
|
f"Resolved to: {canonical}. "
|
|
@@ -437,7 +437,11 @@ class TelemetryQueue:
|
|
|
437
437
|
tmp_base = tempfile.gettempdir().rstrip("/") # nosec B108
|
|
438
438
|
is_tmp_itself = parent_dir.rstrip("/") == tmp_base
|
|
439
439
|
|
|
440
|
-
if (
|
|
440
|
+
if (
|
|
441
|
+
os.name != "nt" # Windows NTFS always reports 0o777; skip Unix permission check
|
|
442
|
+
and (dir_perms & stat.S_IWOTH)
|
|
443
|
+
and not (has_sticky_bit and is_tmp_itself)
|
|
444
|
+
):
|
|
441
445
|
logger.error(
|
|
442
446
|
f"SECURITY: Directory is world-writable (777/other+w). "
|
|
443
447
|
f"Path: {parent_dir}, Permissions: {oct(dir_perms)}"
|
|
@@ -498,7 +502,7 @@ class TelemetryQueue:
|
|
|
498
502
|
# Log queue full event
|
|
499
503
|
if was_full:
|
|
500
504
|
logger.warning(
|
|
501
|
-
f"Telemetry queue full (size={self._max_size}),
|
|
505
|
+
f"Telemetry queue full (size={self._max_size}), oldest item will be evicted"
|
|
502
506
|
)
|
|
503
507
|
|
|
504
508
|
# deque with maxlen automatically evicts oldest when full
|
|
@@ -1171,8 +1175,8 @@ class TelemetryQueue:
|
|
|
1171
1175
|
|
|
1172
1176
|
if file_size > max_file_size:
|
|
1173
1177
|
logger.error(
|
|
1174
|
-
f"Queue file too large: {file_size / (1024*1024):.1f}MB "
|
|
1175
|
-
f"(max: {max_file_size / (1024*1024):.1f}MB). "
|
|
1178
|
+
f"Queue file too large: {file_size / (1024 * 1024):.1f}MB "
|
|
1179
|
+
f"(max: {max_file_size / (1024 * 1024):.1f}MB). "
|
|
1176
1180
|
f"File may be corrupted or contains excessive data. "
|
|
1177
1181
|
f"Renaming to .corrupted to prevent OOM."
|
|
1178
1182
|
)
|
mca_sdk/cli/_bootstrap.py
CHANGED
|
@@ -108,10 +108,17 @@ def _auto_init():
|
|
|
108
108
|
optional = {
|
|
109
109
|
"collector_endpoint": os.environ.get("MCA_COLLECTOR_ENDPOINT"),
|
|
110
110
|
"registry_url": os.environ.get("MCA_REGISTRY_URL"),
|
|
111
|
+
"use_gcp_auth": os.environ.get("MCA_USE_GCP_AUTH", "false").lower() == "true",
|
|
112
|
+
"allow_insecure_collector": os.environ.get("MCA_ALLOW_INSECURE_COLLECTOR", "false").lower()
|
|
113
|
+
== "true",
|
|
111
114
|
"model_version": os.environ.get("MCA_MODEL_VERSION"),
|
|
112
115
|
"model_type": os.environ.get("MCA_MODEL_TYPE"),
|
|
113
116
|
"model_category": os.environ.get("MCA_MODEL_CATEGORY"),
|
|
114
117
|
"environment": os.environ.get("MCA_ENV"),
|
|
118
|
+
"gcp_logging_enabled": os.environ.get("MCA_GCP_LOGGING_ENABLED", "false").lower() == "true",
|
|
119
|
+
"gcp_project_id": os.environ.get("MCA_GCP_PROJECT_ID"),
|
|
120
|
+
"gcp_log_name": os.environ.get("MCA_GCP_LOG_NAME"),
|
|
121
|
+
"gcp_trace_enabled": os.environ.get("MCA_GCP_TRACE_ENABLED", "false").lower() == "true",
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
# Filter out None values
|
|
@@ -127,6 +134,24 @@ def _auto_init():
|
|
|
127
134
|
|
|
128
135
|
mca_sdk._auto_client = client
|
|
129
136
|
|
|
137
|
+
# Story 8.5: Silently invoke autolog() to enable auto-instrumentation
|
|
138
|
+
# This enables structured telemetry capture for sklearn, xgboost, etc.
|
|
139
|
+
# Issue #6 Fix: Add kill switch to disable autolog in production if needed
|
|
140
|
+
if os.environ.get("MCA_DISABLE_AUTOLOG", "").lower() not in ("1", "true", "yes"):
|
|
141
|
+
try:
|
|
142
|
+
from mca_sdk import autolog
|
|
143
|
+
|
|
144
|
+
autolog() # Patches ML frameworks with unified instrumentation
|
|
145
|
+
logger.info("MCA SDK autolog enabled")
|
|
146
|
+
except ImportError as e:
|
|
147
|
+
# ML framework dependencies missing - graceful degradation
|
|
148
|
+
logger.debug("autolog not enabled (missing dependencies): %s", e)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
# Don't crash mca-instrument if autolog fails
|
|
151
|
+
logger.warning("autolog initialization failed: %s", e)
|
|
152
|
+
else:
|
|
153
|
+
logger.info("MCA SDK autolog disabled via MCA_DISABLE_AUTOLOG")
|
|
154
|
+
|
|
130
155
|
# Register shutdown hook with timeout protection
|
|
131
156
|
atexit.register(_shutdown_with_timeout, client, SHUTDOWN_TIMEOUT)
|
|
132
157
|
|