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 CHANGED
@@ -22,15 +22,7 @@ Example:
22
22
  >>> client.shutdown()
23
23
  """
24
24
 
25
- try:
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 setuptools-scm
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__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: 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.dev20'
32
- __version_tuple__ = version_tuple = (0, 1, 'dev20')
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) as span:
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
- # Add output data if requested (with size limit)
200
+ # Capture output if requested
193
201
  if self.capture_output:
194
- output_data = self._capture_data_safely(result, "output")
195
- if output_data is not None:
196
- attrs.update(output_data)
197
- # Add output attributes to span as well
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(latency=latency_seconds, **attrs)
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(latency=latency_seconds, **attrs)
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
 
@@ -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}. " f"Could not resolve to canonical path: {e}"
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. " f"Path: {canonical}"
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
- # Canonicalize custom dirs to handle ~ and relative paths (Finding #9)
359
- canonical_custom = [
360
- os.path.realpath(os.path.expanduser(d), strict=False) for d in custom_dirs
361
- ]
362
- static_allowed.extend(canonical_custom)
363
- logger.debug(f"Using custom allowed directories: {canonical_custom}")
364
-
365
- # Step 6: Whitelist validation with proper directory boundary (Finding #1)
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
- if (
369
- canonical_path == prefix
370
- or canonical_path.startswith(prefix + "/")
371
- or canonical_path.startswith(prefix + os.sep)
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, static_allowed):
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
- static_allowed,
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. " f"Attempted: {canonical}")
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 (dir_perms & stat.S_IWOTH) and not (has_sticky_bit and is_tmp_itself):
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}), " "oldest item will be evicted"
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