stkai 0.2.5__tar.gz → 0.3.0__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.0}/PKG-INFO +1 -1
- {stkai-0.2.5 → stkai-0.3.0}/pyproject.toml +1 -1
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/__init__.py +2 -0
- stkai-0.3.0/src/stkai/_cli.py +53 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_config.py +411 -16
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_http.py +11 -5
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_remote_quick_command.py +10 -11
- {stkai-0.2.5 → stkai-0.3.0/src/stkai.egg-info}/PKG-INFO +1 -1
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/SOURCES.txt +2 -0
- stkai-0.3.0/tests/test_cli.py +82 -0
- stkai-0.3.0/tests/test_config.py +1117 -0
- {stkai-0.2.5 → stkai-0.3.0}/tests/test_http.py +2 -0
- stkai-0.2.5/tests/test_config.py +0 -617
- {stkai-0.2.5 → stkai-0.3.0}/LICENSE +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/README.md +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/setup.cfg +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_auth.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_utils.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/agents/__init__.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/agents/_agent.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/agents/_models.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/__init__.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_event_listeners.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_handlers.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_models.py +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/dependency_links.txt +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/requires.txt +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/top_level.txt +0 -0
- {stkai-0.2.5 → stkai-0.3.0}/tests/test_auth.py +0 -0
|
@@ -80,6 +80,7 @@ from stkai._config import (
|
|
|
80
80
|
STKAI,
|
|
81
81
|
AgentConfig,
|
|
82
82
|
AuthConfig,
|
|
83
|
+
ConfigEntry,
|
|
83
84
|
RateLimitConfig,
|
|
84
85
|
RateLimitStrategy,
|
|
85
86
|
RqcConfig,
|
|
@@ -113,6 +114,7 @@ __all__ = [
|
|
|
113
114
|
# Configuration
|
|
114
115
|
"STKAI",
|
|
115
116
|
"STKAIConfig",
|
|
117
|
+
"ConfigEntry",
|
|
116
118
|
"AuthConfig",
|
|
117
119
|
"RqcConfig",
|
|
118
120
|
"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
|
|
@@ -330,6 +333,234 @@ class RateLimitConfig(OverridableConfig):
|
|
|
330
333
|
return super().with_overrides(processed, allow_none_fields=merged_allow_none)
|
|
331
334
|
|
|
332
335
|
|
|
336
|
+
@dataclass(frozen=True)
|
|
337
|
+
class ConfigEntry:
|
|
338
|
+
"""
|
|
339
|
+
A configuration field with its resolved value and source.
|
|
340
|
+
|
|
341
|
+
Represents a single configuration entry with metadata about where
|
|
342
|
+
the value came from. Used by explain_data() for structured output.
|
|
343
|
+
|
|
344
|
+
Attributes:
|
|
345
|
+
name: The field name (e.g., "request_timeout").
|
|
346
|
+
value: The resolved value.
|
|
347
|
+
source: Where the value came from:
|
|
348
|
+
- "default": Hardcoded default value
|
|
349
|
+
- "env:VAR_NAME": Environment variable
|
|
350
|
+
- "CLI": StackSpot CLI (oscli)
|
|
351
|
+
- "configure": Set via STKAI.configure()
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> entry = ConfigEntry("request_timeout", 60, "configure")
|
|
355
|
+
>>> entry.name
|
|
356
|
+
'request_timeout'
|
|
357
|
+
>>> entry.value
|
|
358
|
+
60
|
|
359
|
+
>>> entry.source
|
|
360
|
+
'configure'
|
|
361
|
+
>>> entry.formatted_value
|
|
362
|
+
'60'
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
name: str
|
|
366
|
+
value: Any
|
|
367
|
+
source: str
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def formatted_value(self) -> str:
|
|
371
|
+
"""
|
|
372
|
+
Return value formatted for display.
|
|
373
|
+
|
|
374
|
+
Masks sensitive fields (e.g., client_secret) showing only
|
|
375
|
+
first and last 4 characters, and truncates long strings.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Formatted string representation of the value.
|
|
379
|
+
|
|
380
|
+
Examples:
|
|
381
|
+
>>> ConfigEntry("client_secret", "super-secret-key", "configure").formatted_value
|
|
382
|
+
'supe********-key'
|
|
383
|
+
>>> ConfigEntry("client_secret", "short", "configure").formatted_value
|
|
384
|
+
'********t'
|
|
385
|
+
"""
|
|
386
|
+
# Mask sensitive fields
|
|
387
|
+
if self.name in ("client_secret",) and self.value is not None:
|
|
388
|
+
secret = str(self.value)
|
|
389
|
+
# Long secrets: show first 4 and last 4 chars
|
|
390
|
+
if len(secret) >= 12:
|
|
391
|
+
return f"{secret[:4]}********{secret[-4:]}"
|
|
392
|
+
# Short secrets: show last 1/3 of chars
|
|
393
|
+
if len(secret) >= 3:
|
|
394
|
+
visible = max(1, len(secret) // 3)
|
|
395
|
+
return f"********{secret[-visible:]}"
|
|
396
|
+
return "********"
|
|
397
|
+
|
|
398
|
+
# Handle None
|
|
399
|
+
if self.value is None:
|
|
400
|
+
return "None"
|
|
401
|
+
|
|
402
|
+
# Convert to string and truncate if needed
|
|
403
|
+
str_value = str(self.value)
|
|
404
|
+
max_length = 50
|
|
405
|
+
if len(str_value) > max_length:
|
|
406
|
+
return str_value[: max_length - 3] + "..."
|
|
407
|
+
|
|
408
|
+
return str_value
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@dataclass(frozen=True)
|
|
412
|
+
class STKAIConfigTracker:
|
|
413
|
+
"""
|
|
414
|
+
Tracks the source of config field values.
|
|
415
|
+
|
|
416
|
+
An immutable tracker that records where each configuration value came from
|
|
417
|
+
(default, env var, CLI, or configure()). Used internally by STKAIConfig
|
|
418
|
+
for debugging via STKAI.explain().
|
|
419
|
+
|
|
420
|
+
Attributes:
|
|
421
|
+
sources: Dict tracking source of each field value.
|
|
422
|
+
Structure: {"section": {"field": "source"}}
|
|
423
|
+
Source values: "default", "env:VAR_NAME", "CLI", "configure"
|
|
424
|
+
|
|
425
|
+
Example:
|
|
426
|
+
>>> tracker = STKAIConfigTracker()
|
|
427
|
+
>>> tracker = tracker.with_changes_tracked(old_cfg, new_cfg, "env")
|
|
428
|
+
>>> tracker.sources.get("rqc", {}).get("request_timeout")
|
|
429
|
+
'env:STKAI_RQC_REQUEST_TIMEOUT'
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
sources: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
433
|
+
|
|
434
|
+
@staticmethod
|
|
435
|
+
def track_changes(
|
|
436
|
+
source_type: str,
|
|
437
|
+
) -> Callable[[Callable[..., STKAIConfig]], Callable[..., STKAIConfig]]:
|
|
438
|
+
"""
|
|
439
|
+
Decorator that tracks config changes made by the decorated method.
|
|
440
|
+
|
|
441
|
+
Wraps methods that return a new STKAIConfig, automatically detecting
|
|
442
|
+
changes between the original config (self) and the returned config,
|
|
443
|
+
then recording those changes in the tracker.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
source_type: Source label for tracking ("env", "CLI", or "configure").
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Decorator function that wraps the method.
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> @STKAIConfigTracker.track_changes("env")
|
|
453
|
+
... def with_env_vars(self) -> STKAIConfig:
|
|
454
|
+
... return STKAIConfig(...)
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
def decorator(
|
|
458
|
+
method: Callable[..., STKAIConfig],
|
|
459
|
+
) -> Callable[..., STKAIConfig]:
|
|
460
|
+
@wraps(method)
|
|
461
|
+
def wrapper(self: STKAIConfig, *args: Any, **kwargs: Any) -> STKAIConfig:
|
|
462
|
+
new_config = method(self, *args, **kwargs)
|
|
463
|
+
new_tracker = self._tracker.with_changes_tracked(
|
|
464
|
+
self, new_config, source_type
|
|
465
|
+
)
|
|
466
|
+
return replace(new_config, _tracker=new_tracker)
|
|
467
|
+
|
|
468
|
+
return wrapper
|
|
469
|
+
|
|
470
|
+
return decorator
|
|
471
|
+
|
|
472
|
+
def with_changes_tracked(
|
|
473
|
+
self,
|
|
474
|
+
old_config: STKAIConfig,
|
|
475
|
+
new_config: STKAIConfig,
|
|
476
|
+
source_type: str,
|
|
477
|
+
) -> STKAIConfigTracker:
|
|
478
|
+
"""
|
|
479
|
+
Return new tracker with changes between configs tracked.
|
|
480
|
+
|
|
481
|
+
Compares old and new configs, detects changed fields, and returns
|
|
482
|
+
a new tracker with those changes recorded.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
old_config: The config before changes.
|
|
486
|
+
new_config: The config after changes.
|
|
487
|
+
source_type: Source label ("env", "CLI", or "configure").
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
New STKAIConfigTracker with detected changes tracked.
|
|
491
|
+
"""
|
|
492
|
+
changes = self._detect_changes(old_config, new_config)
|
|
493
|
+
new_sources = self._merge_sources(changes, source_type)
|
|
494
|
+
return STKAIConfigTracker(sources=new_sources)
|
|
495
|
+
|
|
496
|
+
def _detect_changes(
|
|
497
|
+
self,
|
|
498
|
+
old_config: STKAIConfig,
|
|
499
|
+
new_config: STKAIConfig,
|
|
500
|
+
) -> dict[str, list[str]]:
|
|
501
|
+
"""
|
|
502
|
+
Detect which fields changed between two configs.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
old_config: The config before changes.
|
|
506
|
+
new_config: The config after changes.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Dict mapping section names to lists of changed field names.
|
|
510
|
+
Example: {"rqc": ["request_timeout", "base_url"]}
|
|
511
|
+
"""
|
|
512
|
+
changes: dict[str, list[str]] = {}
|
|
513
|
+
|
|
514
|
+
for section_name in ("auth", "rqc", "agent", "rate_limit"):
|
|
515
|
+
old_section = getattr(old_config, section_name)
|
|
516
|
+
new_section = getattr(new_config, section_name)
|
|
517
|
+
|
|
518
|
+
changed_fields = []
|
|
519
|
+
for f in fields(old_section):
|
|
520
|
+
old_val = getattr(old_section, f.name)
|
|
521
|
+
new_val = getattr(new_section, f.name)
|
|
522
|
+
if old_val != new_val:
|
|
523
|
+
changed_fields.append(f.name)
|
|
524
|
+
|
|
525
|
+
if changed_fields:
|
|
526
|
+
changes[section_name] = changed_fields
|
|
527
|
+
|
|
528
|
+
return changes
|
|
529
|
+
|
|
530
|
+
def _merge_sources(
|
|
531
|
+
self,
|
|
532
|
+
changes: dict[str, list[str]],
|
|
533
|
+
source_type: str,
|
|
534
|
+
) -> dict[str, dict[str, str]]:
|
|
535
|
+
"""
|
|
536
|
+
Merge detected changes into existing sources.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
changes: Dict of section -> list of changed field names.
|
|
540
|
+
source_type: Source label ("env", "CLI", or "configure").
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
New sources dict with changes merged in.
|
|
544
|
+
"""
|
|
545
|
+
new_sources = self._copy_sources()
|
|
546
|
+
|
|
547
|
+
for section, field_names in changes.items():
|
|
548
|
+
section_sources = new_sources.setdefault(section, {})
|
|
549
|
+
for field_name in field_names:
|
|
550
|
+
if source_type == "env":
|
|
551
|
+
# Generate env var name based on convention
|
|
552
|
+
env_var = f"STKAI_{section.upper()}_{field_name.upper()}"
|
|
553
|
+
section_sources[field_name] = f"env:{env_var}"
|
|
554
|
+
else:
|
|
555
|
+
section_sources[field_name] = source_type
|
|
556
|
+
|
|
557
|
+
return new_sources
|
|
558
|
+
|
|
559
|
+
def _copy_sources(self) -> dict[str, dict[str, str]]:
|
|
560
|
+
"""Create a deep copy of current sources."""
|
|
561
|
+
return {section: dict(flds) for section, flds in self.sources.items()}
|
|
562
|
+
|
|
563
|
+
|
|
333
564
|
@dataclass(frozen=True)
|
|
334
565
|
class STKAIConfig:
|
|
335
566
|
"""
|
|
@@ -358,7 +589,9 @@ class STKAIConfig:
|
|
|
358
589
|
rqc: RqcConfig = field(default_factory=RqcConfig)
|
|
359
590
|
agent: AgentConfig = field(default_factory=AgentConfig)
|
|
360
591
|
rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
|
|
592
|
+
_tracker: STKAIConfigTracker = field(default_factory=STKAIConfigTracker, repr=False)
|
|
361
593
|
|
|
594
|
+
@STKAIConfigTracker.track_changes("env")
|
|
362
595
|
def with_env_vars(self) -> STKAIConfig:
|
|
363
596
|
"""
|
|
364
597
|
Return a new config with environment variables applied on top.
|
|
@@ -382,6 +615,106 @@ class STKAIConfig:
|
|
|
382
615
|
rate_limit=self.rate_limit.with_overrides(_get_rate_limit_from_env()),
|
|
383
616
|
)
|
|
384
617
|
|
|
618
|
+
@STKAIConfigTracker.track_changes("CLI")
|
|
619
|
+
def with_cli_defaults(self) -> STKAIConfig:
|
|
620
|
+
"""
|
|
621
|
+
Return a new config with CLI-provided values applied.
|
|
622
|
+
|
|
623
|
+
CLI values take precedence over env vars. When running in CLI mode,
|
|
624
|
+
the CLI knows the correct endpoints for the current environment.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
New STKAIConfig instance with CLI values applied.
|
|
628
|
+
|
|
629
|
+
Example:
|
|
630
|
+
>>> # Apply CLI defaults on top of env vars
|
|
631
|
+
>>> config = STKAIConfig().with_env_vars().with_cli_defaults()
|
|
632
|
+
"""
|
|
633
|
+
from stkai._cli import StkCLI
|
|
634
|
+
|
|
635
|
+
cli_rqc_overrides: dict[str, Any] = {}
|
|
636
|
+
if cli_base_url := StkCLI.get_codebuddy_base_url():
|
|
637
|
+
cli_rqc_overrides["base_url"] = cli_base_url
|
|
638
|
+
|
|
639
|
+
return STKAIConfig(
|
|
640
|
+
auth=self.auth,
|
|
641
|
+
rqc=self.rqc.with_overrides(cli_rqc_overrides),
|
|
642
|
+
agent=self.agent,
|
|
643
|
+
rate_limit=self.rate_limit,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
@STKAIConfigTracker.track_changes("user")
|
|
647
|
+
def with_section_overrides(
|
|
648
|
+
self,
|
|
649
|
+
*,
|
|
650
|
+
auth: dict[str, Any] | None = None,
|
|
651
|
+
rqc: dict[str, Any] | None = None,
|
|
652
|
+
agent: dict[str, Any] | None = None,
|
|
653
|
+
rate_limit: dict[str, Any] | None = None,
|
|
654
|
+
) -> STKAIConfig:
|
|
655
|
+
"""
|
|
656
|
+
Return a new config with overrides applied to nested sections.
|
|
657
|
+
|
|
658
|
+
Each section dict is merged with the existing section config,
|
|
659
|
+
only overriding the specified fields.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
auth: Authentication config overrides.
|
|
663
|
+
rqc: RemoteQuickCommand config overrides.
|
|
664
|
+
agent: Agent config overrides.
|
|
665
|
+
rate_limit: Rate limiting config overrides.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
New STKAIConfig instance with overrides applied.
|
|
669
|
+
|
|
670
|
+
Example:
|
|
671
|
+
>>> config = STKAIConfig()
|
|
672
|
+
>>> custom = config.with_section_overrides(
|
|
673
|
+
... rqc={"request_timeout": 60},
|
|
674
|
+
... agent={"request_timeout": 120},
|
|
675
|
+
... )
|
|
676
|
+
"""
|
|
677
|
+
return STKAIConfig(
|
|
678
|
+
auth=self.auth.with_overrides(auth or {}),
|
|
679
|
+
rqc=self.rqc.with_overrides(rqc or {}),
|
|
680
|
+
agent=self.agent.with_overrides(agent or {}),
|
|
681
|
+
rate_limit=self.rate_limit.with_overrides(rate_limit or {}),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def explain_data(self) -> dict[str, list[ConfigEntry]]:
|
|
685
|
+
"""
|
|
686
|
+
Return config data structured for explain output.
|
|
687
|
+
|
|
688
|
+
Provides a structured representation of all config values and their
|
|
689
|
+
sources, useful for debugging, testing, or custom formatting.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
Dict mapping section names to list of ConfigEntry objects.
|
|
693
|
+
|
|
694
|
+
Example:
|
|
695
|
+
>>> config = STKAIConfig().with_env_vars()
|
|
696
|
+
>>> data = config.explain_data()
|
|
697
|
+
>>> for entry in data["rqc"]:
|
|
698
|
+
... print(f"{entry.name}: {entry.value} ({entry.source})")
|
|
699
|
+
request_timeout: 30 (default)
|
|
700
|
+
...
|
|
701
|
+
"""
|
|
702
|
+
result: dict[str, list[ConfigEntry]] = {}
|
|
703
|
+
|
|
704
|
+
for section_name in ("auth", "rqc", "agent", "rate_limit"):
|
|
705
|
+
section_config = getattr(self, section_name)
|
|
706
|
+
section_sources = self._tracker.sources.get(section_name, {})
|
|
707
|
+
result[section_name] = [
|
|
708
|
+
ConfigEntry(
|
|
709
|
+
name=f.name,
|
|
710
|
+
value=getattr(section_config, f.name),
|
|
711
|
+
source=section_sources.get(f.name, "default"),
|
|
712
|
+
)
|
|
713
|
+
for f in fields(section_config)
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
return result
|
|
717
|
+
|
|
385
718
|
|
|
386
719
|
# =============================================================================
|
|
387
720
|
# Environment Variable Helpers
|
|
@@ -488,8 +821,8 @@ class _STKAI:
|
|
|
488
821
|
"""
|
|
489
822
|
|
|
490
823
|
def __init__(self) -> None:
|
|
491
|
-
"""Initialize with defaults and
|
|
492
|
-
self._config: STKAIConfig = STKAIConfig().with_env_vars()
|
|
824
|
+
"""Initialize with defaults, environment variables, and CLI values."""
|
|
825
|
+
self._config: STKAIConfig = STKAIConfig().with_env_vars().with_cli_defaults()
|
|
493
826
|
|
|
494
827
|
def configure(
|
|
495
828
|
self,
|
|
@@ -499,6 +832,7 @@ class _STKAI:
|
|
|
499
832
|
agent: dict[str, Any] | None = None,
|
|
500
833
|
rate_limit: dict[str, Any] | None = None,
|
|
501
834
|
allow_env_override: bool = True,
|
|
835
|
+
allow_cli_override: bool = True,
|
|
502
836
|
) -> STKAIConfig:
|
|
503
837
|
"""
|
|
504
838
|
Configure SDK settings.
|
|
@@ -513,6 +847,8 @@ class _STKAI:
|
|
|
513
847
|
rate_limit: Rate limiting config overrides (enabled, strategy, max_requests, etc.).
|
|
514
848
|
allow_env_override: If True (default), env vars are used as fallback
|
|
515
849
|
for fields NOT provided. If False, ignores env vars entirely.
|
|
850
|
+
allow_cli_override: If True (default), CLI values (oscli) are used as fallback
|
|
851
|
+
for fields NOT provided. If False, ignores CLI values entirely.
|
|
516
852
|
|
|
517
853
|
Returns:
|
|
518
854
|
The configured STKAIConfig instance.
|
|
@@ -520,11 +856,14 @@ class _STKAI:
|
|
|
520
856
|
Raises:
|
|
521
857
|
ValueError: If any dict contains unknown field names.
|
|
522
858
|
|
|
523
|
-
Precedence (
|
|
859
|
+
Precedence (both overrides True):
|
|
860
|
+
STKAI.configure() > CLI values > ENV vars > defaults
|
|
861
|
+
|
|
862
|
+
Precedence (allow_cli_override=False):
|
|
524
863
|
STKAI.configure() > ENV vars > defaults
|
|
525
864
|
|
|
526
865
|
Precedence (allow_env_override=False):
|
|
527
|
-
STKAI.configure() > defaults
|
|
866
|
+
STKAI.configure() > CLI values > defaults
|
|
528
867
|
|
|
529
868
|
Example:
|
|
530
869
|
>>> from stkai import STKAI
|
|
@@ -534,17 +873,19 @@ class _STKAI:
|
|
|
534
873
|
... rate_limit={"enabled": True, "max_requests": 10},
|
|
535
874
|
... )
|
|
536
875
|
"""
|
|
537
|
-
# Start with defaults, apply env vars as base layer
|
|
538
|
-
base = STKAIConfig()
|
|
876
|
+
# Start with defaults, apply env vars and CLI values as base layer
|
|
877
|
+
base = STKAIConfig()
|
|
539
878
|
if allow_env_override:
|
|
540
879
|
base = base.with_env_vars() # defaults + env vars
|
|
880
|
+
if allow_cli_override:
|
|
881
|
+
base = base.with_cli_defaults() # CLI values take precedence over env vars
|
|
541
882
|
|
|
542
883
|
# Apply user overrides on top - configure() always wins
|
|
543
|
-
self._config =
|
|
544
|
-
auth=
|
|
545
|
-
rqc=
|
|
546
|
-
agent=
|
|
547
|
-
rate_limit=
|
|
884
|
+
self._config = base.with_section_overrides(
|
|
885
|
+
auth=auth,
|
|
886
|
+
rqc=rqc,
|
|
887
|
+
agent=agent,
|
|
888
|
+
rate_limit=rate_limit,
|
|
548
889
|
)
|
|
549
890
|
|
|
550
891
|
return self._config
|
|
@@ -566,7 +907,7 @@ class _STKAI:
|
|
|
566
907
|
|
|
567
908
|
def reset(self) -> STKAIConfig:
|
|
568
909
|
"""
|
|
569
|
-
Reset configuration to defaults + env vars.
|
|
910
|
+
Reset configuration to defaults + env vars + CLI values.
|
|
570
911
|
|
|
571
912
|
Useful for testing to ensure clean state between tests.
|
|
572
913
|
|
|
@@ -577,9 +918,63 @@ class _STKAI:
|
|
|
577
918
|
>>> from stkai import STKAI
|
|
578
919
|
>>> STKAI.reset()
|
|
579
920
|
"""
|
|
580
|
-
self._config = STKAIConfig().with_env_vars()
|
|
921
|
+
self._config = STKAIConfig().with_env_vars().with_cli_defaults()
|
|
581
922
|
return self._config
|
|
582
923
|
|
|
924
|
+
def explain(
|
|
925
|
+
self,
|
|
926
|
+
output: Callable[[str], None] = print,
|
|
927
|
+
) -> None:
|
|
928
|
+
"""
|
|
929
|
+
Print current configuration with sources.
|
|
930
|
+
|
|
931
|
+
Useful for debugging and troubleshooting configuration issues.
|
|
932
|
+
Shows each config value and where it came from:
|
|
933
|
+
|
|
934
|
+
- "default": Using hardcoded default value
|
|
935
|
+
- "env:VAR_NAME": Value from environment variable
|
|
936
|
+
- "CLI": Value from StackSpot CLI (oscli)
|
|
937
|
+
- "configure": Value set via STKAI.configure()
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
output: Callable to output each line. Defaults to print.
|
|
941
|
+
Can be used with logging: `STKAI.explain(logger.info)`
|
|
942
|
+
|
|
943
|
+
Example:
|
|
944
|
+
>>> from stkai import STKAI
|
|
945
|
+
>>> STKAI.explain()
|
|
946
|
+
STKAI Configuration:
|
|
947
|
+
====================
|
|
948
|
+
[rqc]
|
|
949
|
+
base_url .......... https://example.com (CLI)
|
|
950
|
+
request_timeout ... 60 (configure)
|
|
951
|
+
...
|
|
952
|
+
|
|
953
|
+
>>> # Using with logging
|
|
954
|
+
>>> import logging
|
|
955
|
+
>>> STKAI.explain(logging.info)
|
|
956
|
+
"""
|
|
957
|
+
name_width = 25 # field name + dots
|
|
958
|
+
value_width = 50 # max value width (matches truncation)
|
|
959
|
+
total_width = 2 + name_width + 2 + (value_width + 2) + 1 + 8 # matches separator
|
|
960
|
+
|
|
961
|
+
output("STKAI Configuration:")
|
|
962
|
+
output("=" * total_width)
|
|
963
|
+
|
|
964
|
+
# Header
|
|
965
|
+
output(f" {'Field':<{name_width}} │ {'Value':<{value_width}} │ Source")
|
|
966
|
+
output(f"--{'-' * name_width}-+{'-' * (value_width + 2)}+--------")
|
|
967
|
+
|
|
968
|
+
for section_name, entries in self._config.explain_data().items():
|
|
969
|
+
output(f"[{section_name}]")
|
|
970
|
+
for entry in entries:
|
|
971
|
+
dots = "." * (name_width - len(entry.name))
|
|
972
|
+
value_padded = entry.formatted_value.ljust(value_width)
|
|
973
|
+
marker = "✎" if entry.source != "default" else " "
|
|
974
|
+
output(f" {entry.name} {dots} {value_padded} {marker} {entry.source}")
|
|
975
|
+
|
|
976
|
+
output("=" * total_width)
|
|
977
|
+
|
|
583
978
|
def __repr__(self) -> str:
|
|
584
979
|
return f"STKAI(config={self._config!r})"
|
|
585
980
|
|
|
@@ -209,6 +209,7 @@ class StkCLIHttpClient(HttpClient):
|
|
|
209
209
|
url=url,
|
|
210
210
|
timeout=timeout,
|
|
211
211
|
headers=headers,
|
|
212
|
+
use_cache=False, # disables client-side caching
|
|
212
213
|
)
|
|
213
214
|
return response
|
|
214
215
|
|
|
@@ -469,6 +470,14 @@ class EnvironmentAwareHttpClient(HttpClient):
|
|
|
469
470
|
"EnvironmentAwareHttpClient: StackSpot CLI (oscli) detected. "
|
|
470
471
|
"Using StkCLIHttpClient."
|
|
471
472
|
)
|
|
473
|
+
# Warn if credentials are also configured (they will be ignored)
|
|
474
|
+
from stkai._config import STKAI
|
|
475
|
+
|
|
476
|
+
if STKAI.config.auth.has_credentials():
|
|
477
|
+
logger.warning(
|
|
478
|
+
"⚠️ Auth credentials detected (via env vars or configure) but running in CLI mode. "
|
|
479
|
+
"Authentication will be handled by oscli. Credentials will be ignored."
|
|
480
|
+
)
|
|
472
481
|
return StkCLIHttpClient()
|
|
473
482
|
|
|
474
483
|
# 2. Try standalone with credentials
|
|
@@ -556,12 +565,9 @@ class EnvironmentAwareHttpClient(HttpClient):
|
|
|
556
565
|
Returns:
|
|
557
566
|
True if oscli can be imported, False otherwise.
|
|
558
567
|
"""
|
|
559
|
-
|
|
560
|
-
import oscli # noqa: F401
|
|
568
|
+
from stkai._cli import StkCLI
|
|
561
569
|
|
|
562
|
-
|
|
563
|
-
except ImportError:
|
|
564
|
-
return False
|
|
570
|
+
return StkCLI.is_available()
|
|
565
571
|
|
|
566
572
|
@override
|
|
567
573
|
def get(
|
|
@@ -578,8 +578,6 @@ class RemoteQuickCommand:
|
|
|
578
578
|
assert context is not None, "🌀 Sanity check | Event context not provided to polling phase."
|
|
579
579
|
assert request.execution_id, "🌀 Sanity check | Execution ID not provided to polling phase."
|
|
580
580
|
|
|
581
|
-
import random
|
|
582
|
-
|
|
583
581
|
# Get options and assert for type narrowing
|
|
584
582
|
opts = self.options.get_result
|
|
585
583
|
assert opts is not None
|
|
@@ -591,14 +589,6 @@ class RemoteQuickCommand:
|
|
|
591
589
|
start_time = time.time()
|
|
592
590
|
execution_id = request.execution_id
|
|
593
591
|
|
|
594
|
-
# Build full URL using base_url
|
|
595
|
-
nocache_param = random.randint(0, 1000000)
|
|
596
|
-
url = f"{self.base_url}/v1/quick-commands/callback/{execution_id}?nocache={nocache_param}"
|
|
597
|
-
no_cache_headers = {
|
|
598
|
-
"Cache-Control": "no-cache, no-store",
|
|
599
|
-
"Pragma": "no-cache",
|
|
600
|
-
}
|
|
601
|
-
|
|
602
592
|
last_status: RqcExecutionStatus = RqcExecutionStatus.CREATED # Starts at CREATED since we notify PENDING → CREATED before polling
|
|
603
593
|
created_since: float | None = None
|
|
604
594
|
|
|
@@ -614,8 +604,17 @@ class RemoteQuickCommand:
|
|
|
614
604
|
)
|
|
615
605
|
|
|
616
606
|
try:
|
|
607
|
+
# Build full URL using base_url and prevents client-side caching
|
|
608
|
+
import uuid
|
|
609
|
+
nocache_param = str(uuid.uuid4())
|
|
610
|
+
url = f"{self.base_url}/v1/quick-commands/callback/{execution_id}?nocache={nocache_param}"
|
|
611
|
+
nocache_headers = {
|
|
612
|
+
"Cache-Control": "no-cache, no-store",
|
|
613
|
+
"Pragma": "no-cache",
|
|
614
|
+
}
|
|
615
|
+
|
|
617
616
|
response = self.http_client.get(
|
|
618
|
-
url=url, headers=
|
|
617
|
+
url=url, headers=nocache_headers, timeout=opts.request_timeout
|
|
619
618
|
)
|
|
620
619
|
assert isinstance(response, requests.Response), \
|
|
621
620
|
f"🌀 Sanity check | Object returned by `get` method is not an instance of `requests.Response`. ({response.__class__})"
|