stkai 0.2.5__tar.gz → 0.3.2__tar.gz
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.
- {stkai-0.2.5/src/stkai.egg-info → stkai-0.3.2}/PKG-INFO +1 -1
- {stkai-0.2.5 → stkai-0.3.2}/pyproject.toml +1 -1
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/__init__.py +4 -0
- stkai-0.3.2/src/stkai/_cli.py +53 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_config.py +467 -17
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_http.py +11 -5
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_remote_quick_command.py +10 -11
- {stkai-0.2.5 → stkai-0.3.2/src/stkai.egg-info}/PKG-INFO +1 -1
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/SOURCES.txt +2 -0
- stkai-0.3.2/tests/test_cli.py +82 -0
- stkai-0.3.2/tests/test_config.py +1117 -0
- {stkai-0.2.5 → stkai-0.3.2}/tests/test_http.py +2 -0
- stkai-0.2.5/tests/test_config.py +0 -617
- {stkai-0.2.5 → stkai-0.3.2}/LICENSE +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/README.md +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/setup.cfg +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_auth.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_utils.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/agents/__init__.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/agents/_agent.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/agents/_models.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/__init__.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_event_listeners.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_handlers.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_models.py +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/dependency_links.txt +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/requires.txt +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/top_level.txt +0 -0
- {stkai-0.2.5 → stkai-0.3.2}/tests/test_auth.py +0 -0
|
@@ -80,9 +80,11 @@ from stkai._config import (
|
|
|
80
80
|
STKAI,
|
|
81
81
|
AgentConfig,
|
|
82
82
|
AuthConfig,
|
|
83
|
+
ConfigEntry,
|
|
83
84
|
RateLimitConfig,
|
|
84
85
|
RateLimitStrategy,
|
|
85
86
|
RqcConfig,
|
|
87
|
+
SdkConfig,
|
|
86
88
|
STKAIConfig,
|
|
87
89
|
)
|
|
88
90
|
from stkai._http import (
|
|
@@ -113,6 +115,8 @@ __all__ = [
|
|
|
113
115
|
# Configuration
|
|
114
116
|
"STKAI",
|
|
115
117
|
"STKAIConfig",
|
|
118
|
+
"SdkConfig",
|
|
119
|
+
"ConfigEntry",
|
|
116
120
|
"AuthConfig",
|
|
117
121
|
"RqcConfig",
|
|
118
122
|
"AgentConfig",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StackSpot CLI (oscli) integration.
|
|
3
|
+
|
|
4
|
+
This module provides an abstraction layer for interacting with the StackSpot CLI,
|
|
5
|
+
allowing the SDK to detect CLI mode and retrieve CLI-specific configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StkCLI:
|
|
12
|
+
"""
|
|
13
|
+
Abstraction for StackSpot CLI (oscli) integration.
|
|
14
|
+
|
|
15
|
+
Provides methods to:
|
|
16
|
+
- Check if running in CLI mode (oscli available)
|
|
17
|
+
- Retrieve CLI-specific configuration values
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> if StkCLI.is_available():
|
|
21
|
+
... base_url = StkCLI.get_codebuddy_base_url()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def is_available() -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Check if StackSpot CLI (oscli) is available.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if oscli can be imported (CLI mode), False otherwise.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
import oscli # noqa: F401
|
|
34
|
+
|
|
35
|
+
return True
|
|
36
|
+
except ImportError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def get_codebuddy_base_url() -> str | None:
|
|
41
|
+
"""
|
|
42
|
+
Get CodeBuddy base URL from CLI if available.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The CLI's __codebuddy_base_url__ if oscli is installed
|
|
46
|
+
and the attribute exists, None otherwise.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
from oscli import __codebuddy_base_url__
|
|
50
|
+
|
|
51
|
+
return __codebuddy_base_url__ if __codebuddy_base_url__ else None
|
|
52
|
+
except (ImportError, AttributeError):
|
|
53
|
+
return None
|
|
@@ -7,9 +7,10 @@ If not called, sensible defaults are used.
|
|
|
7
7
|
|
|
8
8
|
Hierarchy of precedence (highest to lowest):
|
|
9
9
|
1. *Options passed to client constructors
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
10
|
+
2. Values set via STKAI.configure()
|
|
11
|
+
3. StackSpot CLI values (oscli) - if CLI is available
|
|
12
|
+
4. Environment variables (STKAI_*) - when allow_env_override=True
|
|
13
|
+
5. Hardcoded defaults (in dataclass fields)
|
|
13
14
|
|
|
14
15
|
Example:
|
|
15
16
|
>>> from stkai import STKAI
|
|
@@ -27,7 +28,9 @@ Example:
|
|
|
27
28
|
from __future__ import annotations
|
|
28
29
|
|
|
29
30
|
import os
|
|
31
|
+
from collections.abc import Callable
|
|
30
32
|
from dataclasses import dataclass, field, fields, replace
|
|
33
|
+
from functools import wraps
|
|
31
34
|
from typing import Any, Literal, Self
|
|
32
35
|
|
|
33
36
|
# Type alias for rate limit strategies
|
|
@@ -100,6 +103,46 @@ class OverridableConfig:
|
|
|
100
103
|
# =============================================================================
|
|
101
104
|
|
|
102
105
|
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class SdkConfig:
|
|
108
|
+
"""
|
|
109
|
+
SDK metadata (read-only, not configurable).
|
|
110
|
+
|
|
111
|
+
Provides information about the SDK version and runtime environment.
|
|
112
|
+
These values are automatically detected and cannot be overridden.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
version: The installed SDK version.
|
|
116
|
+
cli_mode: Whether StackSpot CLI (oscli) is available.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> from stkai import STKAI
|
|
120
|
+
>>> STKAI.config.sdk.version
|
|
121
|
+
'0.2.8'
|
|
122
|
+
>>> STKAI.config.sdk.cli_mode
|
|
123
|
+
True
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
version: str
|
|
127
|
+
cli_mode: bool
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def detect(cls) -> SdkConfig:
|
|
131
|
+
"""
|
|
132
|
+
Detect SDK metadata from the runtime environment.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
SdkConfig with version and cli_mode auto-detected.
|
|
136
|
+
"""
|
|
137
|
+
from stkai import __version__
|
|
138
|
+
from stkai._cli import StkCLI
|
|
139
|
+
|
|
140
|
+
return cls(
|
|
141
|
+
version=__version__,
|
|
142
|
+
cli_mode=StkCLI.is_available(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
103
146
|
@dataclass(frozen=True)
|
|
104
147
|
class AuthConfig(OverridableConfig):
|
|
105
148
|
"""
|
|
@@ -330,15 +373,244 @@ class RateLimitConfig(OverridableConfig):
|
|
|
330
373
|
return super().with_overrides(processed, allow_none_fields=merged_allow_none)
|
|
331
374
|
|
|
332
375
|
|
|
376
|
+
@dataclass(frozen=True)
|
|
377
|
+
class ConfigEntry:
|
|
378
|
+
"""
|
|
379
|
+
A configuration field with its resolved value and source.
|
|
380
|
+
|
|
381
|
+
Represents a single configuration entry with metadata about where
|
|
382
|
+
the value came from. Used by explain_data() for structured output.
|
|
383
|
+
|
|
384
|
+
Attributes:
|
|
385
|
+
name: The field name (e.g., "request_timeout").
|
|
386
|
+
value: The resolved value.
|
|
387
|
+
source: Where the value came from:
|
|
388
|
+
- "default": Hardcoded default value
|
|
389
|
+
- "env:VAR_NAME": Environment variable
|
|
390
|
+
- "CLI": StackSpot CLI (oscli)
|
|
391
|
+
- "configure": Set via STKAI.configure()
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
>>> entry = ConfigEntry("request_timeout", 60, "configure")
|
|
395
|
+
>>> entry.name
|
|
396
|
+
'request_timeout'
|
|
397
|
+
>>> entry.value
|
|
398
|
+
60
|
|
399
|
+
>>> entry.source
|
|
400
|
+
'configure'
|
|
401
|
+
>>> entry.formatted_value
|
|
402
|
+
'60'
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
name: str
|
|
406
|
+
value: Any
|
|
407
|
+
source: str
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def formatted_value(self) -> str:
|
|
411
|
+
"""
|
|
412
|
+
Return value formatted for display.
|
|
413
|
+
|
|
414
|
+
Masks sensitive fields (e.g., client_secret) showing only
|
|
415
|
+
first and last 4 characters, and truncates long strings.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Formatted string representation of the value.
|
|
419
|
+
|
|
420
|
+
Examples:
|
|
421
|
+
>>> ConfigEntry("client_secret", "super-secret-key", "configure").formatted_value
|
|
422
|
+
'supe********-key'
|
|
423
|
+
>>> ConfigEntry("client_secret", "short", "configure").formatted_value
|
|
424
|
+
'********t'
|
|
425
|
+
"""
|
|
426
|
+
# Mask sensitive fields
|
|
427
|
+
if self.name in ("client_secret",) and self.value is not None:
|
|
428
|
+
secret = str(self.value)
|
|
429
|
+
# Long secrets: show first 4 and last 4 chars
|
|
430
|
+
if len(secret) >= 12:
|
|
431
|
+
return f"{secret[:4]}********{secret[-4:]}"
|
|
432
|
+
# Short secrets: show last 1/3 of chars
|
|
433
|
+
if len(secret) >= 3:
|
|
434
|
+
visible = max(1, len(secret) // 3)
|
|
435
|
+
return f"********{secret[-visible:]}"
|
|
436
|
+
return "********"
|
|
437
|
+
|
|
438
|
+
# Handle None
|
|
439
|
+
if self.value is None:
|
|
440
|
+
return "None"
|
|
441
|
+
|
|
442
|
+
# Convert to string and truncate if needed
|
|
443
|
+
str_value = str(self.value)
|
|
444
|
+
max_length = 50
|
|
445
|
+
if len(str_value) > max_length:
|
|
446
|
+
return str_value[: max_length - 3] + "..."
|
|
447
|
+
|
|
448
|
+
return str_value
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@dataclass(frozen=True)
|
|
452
|
+
class STKAIConfigTracker:
|
|
453
|
+
"""
|
|
454
|
+
Tracks the source of config field values.
|
|
455
|
+
|
|
456
|
+
An immutable tracker that records where each configuration value came from
|
|
457
|
+
(default, env var, CLI, or configure()). Used internally by STKAIConfig
|
|
458
|
+
for debugging via STKAI.explain().
|
|
459
|
+
|
|
460
|
+
Attributes:
|
|
461
|
+
sources: Dict tracking source of each field value.
|
|
462
|
+
Structure: {"section": {"field": "source"}}
|
|
463
|
+
Source values: "default", "env:VAR_NAME", "CLI", "configure"
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> tracker = STKAIConfigTracker()
|
|
467
|
+
>>> tracker = tracker.with_changes_tracked(old_cfg, new_cfg, "env")
|
|
468
|
+
>>> tracker.sources.get("rqc", {}).get("request_timeout")
|
|
469
|
+
'env:STKAI_RQC_REQUEST_TIMEOUT'
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
sources: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def track_changes(
|
|
476
|
+
source_type: str,
|
|
477
|
+
) -> Callable[[Callable[..., STKAIConfig]], Callable[..., STKAIConfig]]:
|
|
478
|
+
"""
|
|
479
|
+
Decorator that tracks config changes made by the decorated method.
|
|
480
|
+
|
|
481
|
+
Wraps methods that return a new STKAIConfig, automatically detecting
|
|
482
|
+
changes between the original config (self) and the returned config,
|
|
483
|
+
then recording those changes in the tracker.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
source_type: Source label for tracking ("env", "CLI", or "configure").
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Decorator function that wraps the method.
|
|
490
|
+
|
|
491
|
+
Example:
|
|
492
|
+
>>> @STKAIConfigTracker.track_changes("env")
|
|
493
|
+
... def with_env_vars(self) -> STKAIConfig:
|
|
494
|
+
... return STKAIConfig(...)
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
def decorator(
|
|
498
|
+
method: Callable[..., STKAIConfig],
|
|
499
|
+
) -> Callable[..., STKAIConfig]:
|
|
500
|
+
@wraps(method)
|
|
501
|
+
def wrapper(self: STKAIConfig, *args: Any, **kwargs: Any) -> STKAIConfig:
|
|
502
|
+
new_config = method(self, *args, **kwargs)
|
|
503
|
+
new_tracker = self._tracker.with_changes_tracked(
|
|
504
|
+
self, new_config, source_type
|
|
505
|
+
)
|
|
506
|
+
return replace(new_config, _tracker=new_tracker)
|
|
507
|
+
|
|
508
|
+
return wrapper
|
|
509
|
+
|
|
510
|
+
return decorator
|
|
511
|
+
|
|
512
|
+
def with_changes_tracked(
|
|
513
|
+
self,
|
|
514
|
+
old_config: STKAIConfig,
|
|
515
|
+
new_config: STKAIConfig,
|
|
516
|
+
source_type: str,
|
|
517
|
+
) -> STKAIConfigTracker:
|
|
518
|
+
"""
|
|
519
|
+
Return new tracker with changes between configs tracked.
|
|
520
|
+
|
|
521
|
+
Compares old and new configs, detects changed fields, and returns
|
|
522
|
+
a new tracker with those changes recorded.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
old_config: The config before changes.
|
|
526
|
+
new_config: The config after changes.
|
|
527
|
+
source_type: Source label ("env", "CLI", or "configure").
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
New STKAIConfigTracker with detected changes tracked.
|
|
531
|
+
"""
|
|
532
|
+
changes = self._detect_changes(old_config, new_config)
|
|
533
|
+
new_sources = self._merge_sources(changes, source_type)
|
|
534
|
+
return STKAIConfigTracker(sources=new_sources)
|
|
535
|
+
|
|
536
|
+
def _detect_changes(
|
|
537
|
+
self,
|
|
538
|
+
old_config: STKAIConfig,
|
|
539
|
+
new_config: STKAIConfig,
|
|
540
|
+
) -> dict[str, list[str]]:
|
|
541
|
+
"""
|
|
542
|
+
Detect which fields changed between two configs.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
old_config: The config before changes.
|
|
546
|
+
new_config: The config after changes.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Dict mapping section names to lists of changed field names.
|
|
550
|
+
Example: {"rqc": ["request_timeout", "base_url"]}
|
|
551
|
+
"""
|
|
552
|
+
changes: dict[str, list[str]] = {}
|
|
553
|
+
|
|
554
|
+
for section_name in ("auth", "rqc", "agent", "rate_limit"):
|
|
555
|
+
old_section = getattr(old_config, section_name)
|
|
556
|
+
new_section = getattr(new_config, section_name)
|
|
557
|
+
|
|
558
|
+
changed_fields = []
|
|
559
|
+
for f in fields(old_section):
|
|
560
|
+
old_val = getattr(old_section, f.name)
|
|
561
|
+
new_val = getattr(new_section, f.name)
|
|
562
|
+
if old_val != new_val:
|
|
563
|
+
changed_fields.append(f.name)
|
|
564
|
+
|
|
565
|
+
if changed_fields:
|
|
566
|
+
changes[section_name] = changed_fields
|
|
567
|
+
|
|
568
|
+
return changes
|
|
569
|
+
|
|
570
|
+
def _merge_sources(
|
|
571
|
+
self,
|
|
572
|
+
changes: dict[str, list[str]],
|
|
573
|
+
source_type: str,
|
|
574
|
+
) -> dict[str, dict[str, str]]:
|
|
575
|
+
"""
|
|
576
|
+
Merge detected changes into existing sources.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
changes: Dict of section -> list of changed field names.
|
|
580
|
+
source_type: Source label ("env", "CLI", or "configure").
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
New sources dict with changes merged in.
|
|
584
|
+
"""
|
|
585
|
+
new_sources = self._copy_sources()
|
|
586
|
+
|
|
587
|
+
for section, field_names in changes.items():
|
|
588
|
+
section_sources = new_sources.setdefault(section, {})
|
|
589
|
+
for field_name in field_names:
|
|
590
|
+
if source_type == "env":
|
|
591
|
+
# Generate env var name based on convention
|
|
592
|
+
env_var = f"STKAI_{section.upper()}_{field_name.upper()}"
|
|
593
|
+
section_sources[field_name] = f"env:{env_var}"
|
|
594
|
+
else:
|
|
595
|
+
section_sources[field_name] = source_type
|
|
596
|
+
|
|
597
|
+
return new_sources
|
|
598
|
+
|
|
599
|
+
def _copy_sources(self) -> dict[str, dict[str, str]]:
|
|
600
|
+
"""Create a deep copy of current sources."""
|
|
601
|
+
return {section: dict(flds) for section, flds in self.sources.items()}
|
|
602
|
+
|
|
603
|
+
|
|
333
604
|
@dataclass(frozen=True)
|
|
334
605
|
class STKAIConfig:
|
|
335
606
|
"""
|
|
336
607
|
Global configuration for the stkai SDK.
|
|
337
608
|
|
|
338
|
-
Aggregates all configuration sections: auth, rqc, agent, and rate_limit.
|
|
609
|
+
Aggregates all configuration sections: sdk, auth, rqc, agent, and rate_limit.
|
|
339
610
|
Access via the global `STKAI.config` property.
|
|
340
611
|
|
|
341
612
|
Attributes:
|
|
613
|
+
sdk: SDK metadata (version, cli_mode). Read-only.
|
|
342
614
|
auth: Authentication configuration.
|
|
343
615
|
rqc: RemoteQuickCommand configuration.
|
|
344
616
|
agent: Agent configuration.
|
|
@@ -346,6 +618,10 @@ class STKAIConfig:
|
|
|
346
618
|
|
|
347
619
|
Example:
|
|
348
620
|
>>> from stkai import STKAI
|
|
621
|
+
>>> STKAI.config.sdk.version
|
|
622
|
+
'0.2.8'
|
|
623
|
+
>>> STKAI.config.sdk.cli_mode
|
|
624
|
+
True
|
|
349
625
|
>>> STKAI.config.rqc.request_timeout
|
|
350
626
|
30
|
|
351
627
|
>>> STKAI.config.auth.has_credentials()
|
|
@@ -354,11 +630,14 @@ class STKAIConfig:
|
|
|
354
630
|
False
|
|
355
631
|
"""
|
|
356
632
|
|
|
633
|
+
sdk: SdkConfig = field(default_factory=SdkConfig.detect)
|
|
357
634
|
auth: AuthConfig = field(default_factory=AuthConfig)
|
|
358
635
|
rqc: RqcConfig = field(default_factory=RqcConfig)
|
|
359
636
|
agent: AgentConfig = field(default_factory=AgentConfig)
|
|
360
637
|
rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
|
|
638
|
+
_tracker: STKAIConfigTracker = field(default_factory=STKAIConfigTracker, repr=False)
|
|
361
639
|
|
|
640
|
+
@STKAIConfigTracker.track_changes("env")
|
|
362
641
|
def with_env_vars(self) -> STKAIConfig:
|
|
363
642
|
"""
|
|
364
643
|
Return a new config with environment variables applied on top.
|
|
@@ -376,12 +655,121 @@ class STKAIConfig:
|
|
|
376
655
|
>>> final = custom.with_env_vars()
|
|
377
656
|
"""
|
|
378
657
|
return STKAIConfig(
|
|
658
|
+
sdk=self.sdk,
|
|
379
659
|
auth=self.auth.with_overrides(_get_auth_from_env()),
|
|
380
660
|
rqc=self.rqc.with_overrides(_get_rqc_from_env()),
|
|
381
661
|
agent=self.agent.with_overrides(_get_agent_from_env()),
|
|
382
662
|
rate_limit=self.rate_limit.with_overrides(_get_rate_limit_from_env()),
|
|
383
663
|
)
|
|
384
664
|
|
|
665
|
+
@STKAIConfigTracker.track_changes("CLI")
|
|
666
|
+
def with_cli_defaults(self) -> STKAIConfig:
|
|
667
|
+
"""
|
|
668
|
+
Return a new config with CLI-provided values applied.
|
|
669
|
+
|
|
670
|
+
CLI values take precedence over env vars. When running in CLI mode,
|
|
671
|
+
the CLI knows the correct endpoints for the current environment.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
New STKAIConfig instance with CLI values applied.
|
|
675
|
+
|
|
676
|
+
Example:
|
|
677
|
+
>>> # Apply CLI defaults on top of env vars
|
|
678
|
+
>>> config = STKAIConfig().with_env_vars().with_cli_defaults()
|
|
679
|
+
"""
|
|
680
|
+
from stkai._cli import StkCLI
|
|
681
|
+
|
|
682
|
+
cli_rqc_overrides: dict[str, Any] = {}
|
|
683
|
+
if cli_base_url := StkCLI.get_codebuddy_base_url():
|
|
684
|
+
cli_rqc_overrides["base_url"] = cli_base_url
|
|
685
|
+
|
|
686
|
+
return STKAIConfig(
|
|
687
|
+
sdk=self.sdk,
|
|
688
|
+
auth=self.auth,
|
|
689
|
+
rqc=self.rqc.with_overrides(cli_rqc_overrides),
|
|
690
|
+
agent=self.agent,
|
|
691
|
+
rate_limit=self.rate_limit,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
@STKAIConfigTracker.track_changes("user")
|
|
695
|
+
def with_section_overrides(
|
|
696
|
+
self,
|
|
697
|
+
*,
|
|
698
|
+
auth: dict[str, Any] | None = None,
|
|
699
|
+
rqc: dict[str, Any] | None = None,
|
|
700
|
+
agent: dict[str, Any] | None = None,
|
|
701
|
+
rate_limit: dict[str, Any] | None = None,
|
|
702
|
+
) -> STKAIConfig:
|
|
703
|
+
"""
|
|
704
|
+
Return a new config with overrides applied to nested sections.
|
|
705
|
+
|
|
706
|
+
Each section dict is merged with the existing section config,
|
|
707
|
+
only overriding the specified fields.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
auth: Authentication config overrides.
|
|
711
|
+
rqc: RemoteQuickCommand config overrides.
|
|
712
|
+
agent: Agent config overrides.
|
|
713
|
+
rate_limit: Rate limiting config overrides.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
New STKAIConfig instance with overrides applied.
|
|
717
|
+
|
|
718
|
+
Example:
|
|
719
|
+
>>> config = STKAIConfig()
|
|
720
|
+
>>> custom = config.with_section_overrides(
|
|
721
|
+
... rqc={"request_timeout": 60},
|
|
722
|
+
... agent={"request_timeout": 120},
|
|
723
|
+
... )
|
|
724
|
+
"""
|
|
725
|
+
return STKAIConfig(
|
|
726
|
+
sdk=self.sdk,
|
|
727
|
+
auth=self.auth.with_overrides(auth or {}),
|
|
728
|
+
rqc=self.rqc.with_overrides(rqc or {}),
|
|
729
|
+
agent=self.agent.with_overrides(agent or {}),
|
|
730
|
+
rate_limit=self.rate_limit.with_overrides(rate_limit or {}),
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def explain_data(self) -> dict[str, list[ConfigEntry]]:
|
|
734
|
+
"""
|
|
735
|
+
Return config data structured for explain output.
|
|
736
|
+
|
|
737
|
+
Provides a structured representation of all config values and their
|
|
738
|
+
sources, useful for debugging, testing, or custom formatting.
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
Dict mapping section names to list of ConfigEntry objects.
|
|
742
|
+
|
|
743
|
+
Example:
|
|
744
|
+
>>> config = STKAIConfig().with_env_vars()
|
|
745
|
+
>>> data = config.explain_data()
|
|
746
|
+
>>> for entry in data["rqc"]:
|
|
747
|
+
... print(f"{entry.name}: {entry.value} ({entry.source})")
|
|
748
|
+
request_timeout: 30 (default)
|
|
749
|
+
...
|
|
750
|
+
"""
|
|
751
|
+
result: dict[str, list[ConfigEntry]] = {}
|
|
752
|
+
|
|
753
|
+
# SDK section (read-only, not tracked)
|
|
754
|
+
result["sdk"] = [
|
|
755
|
+
ConfigEntry(name=f.name, value=getattr(self.sdk, f.name), source="-")
|
|
756
|
+
for f in fields(self.sdk)
|
|
757
|
+
]
|
|
758
|
+
|
|
759
|
+
for section_name in ("auth", "rqc", "agent", "rate_limit"):
|
|
760
|
+
section_config = getattr(self, section_name)
|
|
761
|
+
section_sources = self._tracker.sources.get(section_name, {})
|
|
762
|
+
result[section_name] = [
|
|
763
|
+
ConfigEntry(
|
|
764
|
+
name=f.name,
|
|
765
|
+
value=getattr(section_config, f.name),
|
|
766
|
+
source=section_sources.get(f.name, "default"),
|
|
767
|
+
)
|
|
768
|
+
for f in fields(section_config)
|
|
769
|
+
]
|
|
770
|
+
|
|
771
|
+
return result
|
|
772
|
+
|
|
385
773
|
|
|
386
774
|
# =============================================================================
|
|
387
775
|
# Environment Variable Helpers
|
|
@@ -488,8 +876,8 @@ class _STKAI:
|
|
|
488
876
|
"""
|
|
489
877
|
|
|
490
878
|
def __init__(self) -> None:
|
|
491
|
-
"""Initialize with defaults and
|
|
492
|
-
self._config: STKAIConfig = STKAIConfig().with_env_vars()
|
|
879
|
+
"""Initialize with defaults, environment variables, and CLI values."""
|
|
880
|
+
self._config: STKAIConfig = STKAIConfig().with_env_vars().with_cli_defaults()
|
|
493
881
|
|
|
494
882
|
def configure(
|
|
495
883
|
self,
|
|
@@ -499,6 +887,7 @@ class _STKAI:
|
|
|
499
887
|
agent: dict[str, Any] | None = None,
|
|
500
888
|
rate_limit: dict[str, Any] | None = None,
|
|
501
889
|
allow_env_override: bool = True,
|
|
890
|
+
allow_cli_override: bool = True,
|
|
502
891
|
) -> STKAIConfig:
|
|
503
892
|
"""
|
|
504
893
|
Configure SDK settings.
|
|
@@ -513,6 +902,8 @@ class _STKAI:
|
|
|
513
902
|
rate_limit: Rate limiting config overrides (enabled, strategy, max_requests, etc.).
|
|
514
903
|
allow_env_override: If True (default), env vars are used as fallback
|
|
515
904
|
for fields NOT provided. If False, ignores env vars entirely.
|
|
905
|
+
allow_cli_override: If True (default), CLI values (oscli) are used as fallback
|
|
906
|
+
for fields NOT provided. If False, ignores CLI values entirely.
|
|
516
907
|
|
|
517
908
|
Returns:
|
|
518
909
|
The configured STKAIConfig instance.
|
|
@@ -520,11 +911,14 @@ class _STKAI:
|
|
|
520
911
|
Raises:
|
|
521
912
|
ValueError: If any dict contains unknown field names.
|
|
522
913
|
|
|
523
|
-
Precedence (
|
|
914
|
+
Precedence (both overrides True):
|
|
915
|
+
STKAI.configure() > CLI values > ENV vars > defaults
|
|
916
|
+
|
|
917
|
+
Precedence (allow_cli_override=False):
|
|
524
918
|
STKAI.configure() > ENV vars > defaults
|
|
525
919
|
|
|
526
920
|
Precedence (allow_env_override=False):
|
|
527
|
-
STKAI.configure() > defaults
|
|
921
|
+
STKAI.configure() > CLI values > defaults
|
|
528
922
|
|
|
529
923
|
Example:
|
|
530
924
|
>>> from stkai import STKAI
|
|
@@ -534,17 +928,19 @@ class _STKAI:
|
|
|
534
928
|
... rate_limit={"enabled": True, "max_requests": 10},
|
|
535
929
|
... )
|
|
536
930
|
"""
|
|
537
|
-
# Start with defaults, apply env vars as base layer
|
|
538
|
-
base = STKAIConfig()
|
|
931
|
+
# Start with defaults, apply env vars and CLI values as base layer
|
|
932
|
+
base = STKAIConfig()
|
|
539
933
|
if allow_env_override:
|
|
540
934
|
base = base.with_env_vars() # defaults + env vars
|
|
935
|
+
if allow_cli_override:
|
|
936
|
+
base = base.with_cli_defaults() # CLI values take precedence over env vars
|
|
541
937
|
|
|
542
938
|
# Apply user overrides on top - configure() always wins
|
|
543
|
-
self._config =
|
|
544
|
-
auth=
|
|
545
|
-
rqc=
|
|
546
|
-
agent=
|
|
547
|
-
rate_limit=
|
|
939
|
+
self._config = base.with_section_overrides(
|
|
940
|
+
auth=auth,
|
|
941
|
+
rqc=rqc,
|
|
942
|
+
agent=agent,
|
|
943
|
+
rate_limit=rate_limit,
|
|
548
944
|
)
|
|
549
945
|
|
|
550
946
|
return self._config
|
|
@@ -566,7 +962,7 @@ class _STKAI:
|
|
|
566
962
|
|
|
567
963
|
def reset(self) -> STKAIConfig:
|
|
568
964
|
"""
|
|
569
|
-
Reset configuration to defaults + env vars.
|
|
965
|
+
Reset configuration to defaults + env vars + CLI values.
|
|
570
966
|
|
|
571
967
|
Useful for testing to ensure clean state between tests.
|
|
572
968
|
|
|
@@ -577,9 +973,63 @@ class _STKAI:
|
|
|
577
973
|
>>> from stkai import STKAI
|
|
578
974
|
>>> STKAI.reset()
|
|
579
975
|
"""
|
|
580
|
-
self._config = STKAIConfig().with_env_vars()
|
|
976
|
+
self._config = STKAIConfig().with_env_vars().with_cli_defaults()
|
|
581
977
|
return self._config
|
|
582
978
|
|
|
979
|
+
def explain(
|
|
980
|
+
self,
|
|
981
|
+
output: Callable[[str], None] = print,
|
|
982
|
+
) -> None:
|
|
983
|
+
"""
|
|
984
|
+
Print current configuration with sources.
|
|
985
|
+
|
|
986
|
+
Useful for debugging and troubleshooting configuration issues.
|
|
987
|
+
Shows each config value and where it came from:
|
|
988
|
+
|
|
989
|
+
- "default": Using hardcoded default value
|
|
990
|
+
- "env:VAR_NAME": Value from environment variable
|
|
991
|
+
- "CLI": Value from StackSpot CLI (oscli)
|
|
992
|
+
- "configure": Value set via STKAI.configure()
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
output: Callable to output each line. Defaults to print.
|
|
996
|
+
Can be used with logging: `STKAI.explain(logger.info)`
|
|
997
|
+
|
|
998
|
+
Example:
|
|
999
|
+
>>> from stkai import STKAI
|
|
1000
|
+
>>> STKAI.explain()
|
|
1001
|
+
STKAI Configuration:
|
|
1002
|
+
====================
|
|
1003
|
+
[rqc]
|
|
1004
|
+
base_url .......... https://example.com (CLI)
|
|
1005
|
+
request_timeout ... 60 (configure)
|
|
1006
|
+
...
|
|
1007
|
+
|
|
1008
|
+
>>> # Using with logging
|
|
1009
|
+
>>> import logging
|
|
1010
|
+
>>> STKAI.explain(logging.info)
|
|
1011
|
+
"""
|
|
1012
|
+
name_width = 25 # field name + dots
|
|
1013
|
+
value_width = 50 # max value width (matches truncation)
|
|
1014
|
+
total_width = 2 + name_width + 2 + (value_width + 2) + 1 + 8 # matches separator
|
|
1015
|
+
|
|
1016
|
+
output("STKAI Configuration:")
|
|
1017
|
+
output("=" * total_width)
|
|
1018
|
+
|
|
1019
|
+
# Header
|
|
1020
|
+
output(f" {'Field':<{name_width}} │ {'Value':<{value_width}} │ Source")
|
|
1021
|
+
output(f"--{'-' * name_width}-+{'-' * (value_width + 2)}+--------")
|
|
1022
|
+
|
|
1023
|
+
for section_name, entries in self._config.explain_data().items():
|
|
1024
|
+
output(f"[{section_name}]")
|
|
1025
|
+
for entry in entries:
|
|
1026
|
+
dots = "." * (name_width - len(entry.name))
|
|
1027
|
+
value_padded = entry.formatted_value.ljust(value_width)
|
|
1028
|
+
marker = "✎" if entry.source not in ("default", "-") else " "
|
|
1029
|
+
output(f" {entry.name} {dots} {value_padded} {marker} {entry.source}")
|
|
1030
|
+
|
|
1031
|
+
output("=" * total_width)
|
|
1032
|
+
|
|
583
1033
|
def __repr__(self) -> str:
|
|
584
1034
|
return f"STKAI(config={self._config!r})"
|
|
585
1035
|
|