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.
Files changed (29) hide show
  1. {stkai-0.2.5/src/stkai.egg-info → stkai-0.3.0}/PKG-INFO +1 -1
  2. {stkai-0.2.5 → stkai-0.3.0}/pyproject.toml +1 -1
  3. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/__init__.py +2 -0
  4. stkai-0.3.0/src/stkai/_cli.py +53 -0
  5. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_config.py +411 -16
  6. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_http.py +11 -5
  7. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_remote_quick_command.py +10 -11
  8. {stkai-0.2.5 → stkai-0.3.0/src/stkai.egg-info}/PKG-INFO +1 -1
  9. {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/SOURCES.txt +2 -0
  10. stkai-0.3.0/tests/test_cli.py +82 -0
  11. stkai-0.3.0/tests/test_config.py +1117 -0
  12. {stkai-0.2.5 → stkai-0.3.0}/tests/test_http.py +2 -0
  13. stkai-0.2.5/tests/test_config.py +0 -617
  14. {stkai-0.2.5 → stkai-0.3.0}/LICENSE +0 -0
  15. {stkai-0.2.5 → stkai-0.3.0}/README.md +0 -0
  16. {stkai-0.2.5 → stkai-0.3.0}/setup.cfg +0 -0
  17. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_auth.py +0 -0
  18. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/_utils.py +0 -0
  19. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/agents/__init__.py +0 -0
  20. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/agents/_agent.py +0 -0
  21. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/agents/_models.py +0 -0
  22. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/__init__.py +0 -0
  23. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_event_listeners.py +0 -0
  24. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_handlers.py +0 -0
  25. {stkai-0.2.5 → stkai-0.3.0}/src/stkai/rqc/_models.py +0 -0
  26. {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/dependency_links.txt +0 -0
  27. {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/requires.txt +0 -0
  28. {stkai-0.2.5 → stkai-0.3.0}/src/stkai.egg-info/top_level.txt +0 -0
  29. {stkai-0.2.5 → stkai-0.3.0}/tests/test_auth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stkai
3
- Version: 0.2.5
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for StackSpot AI - Remote Quick Commands and more
5
5
  Author-email: Rafael Ponte <rponte@gmail.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stkai"
7
- version = "0.2.5"
7
+ version = "0.3.0"
8
8
  description = "Python SDK for StackSpot AI - Remote Quick Commands and more"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.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. Environment variables (STKAI_*) - when allow_env_override=True
11
- 3. Values set via STKAI.configure()
12
- 4. Hardcoded defaults (in dataclass fields)
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 apply environment variables."""
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 (allow_env_override=True):
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 (if enabled)
538
- base = STKAIConfig() # only defaults
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 = STKAIConfig(
544
- auth=base.auth.with_overrides(auth or {}),
545
- rqc=base.rqc.with_overrides(rqc or {}),
546
- agent=base.agent.with_overrides(agent or {}),
547
- rate_limit=base.rate_limit.with_overrides(rate_limit or {}),
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
- try:
560
- import oscli # noqa: F401
568
+ from stkai._cli import StkCLI
561
569
 
562
- return True
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=no_cache_headers, timeout=opts.request_timeout
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__})"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stkai
3
- Version: 0.2.5
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for StackSpot AI - Remote Quick Commands and more
5
5
  Author-email: Rafael Ponte <rponte@gmail.com>
6
6
  License: Apache-2.0