Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,542 changes: 453 additions & 1,089 deletions .basedpyright/baseline.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/actions/setup-sdk-environment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ runs:
activate-environment: true
python-version: ${{ inputs.python-version }}
- name: Install dependencies from the ${{ inputs.deps-group }} group(s)
run: SDK_DEPS_GROUP="${{ inputs.deps-group }}" make uv-sync-ci
run: SDK_DEPS_GROUP="${{ inputs.deps-group }}" make ci-install
shell: bash
7 changes: 3 additions & 4 deletions .github/workflows/appinspect.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Validate SDK with Splunk AppInspect
on: [ push, workflow_dispatch ]
on: [push, workflow_dispatch]

env:
PYTHON_VERSION: 3.13
Expand All @@ -20,10 +20,9 @@ jobs:
run: |
mkdir -p ${{ env.MOCK_APP_PATH }}/bin/lib
uv pip install ".[openai, anthropic]" --target ${{ env.MOCK_APP_PATH }}/bin/lib
- name: Copy splunklib to a test app and package it as a mock app
- name: Copy splunklib into a mock app and package it
run: |
cd ${{ env.MOCK_APP_PATH }}
tar -czf mock_app.tgz --exclude="__pycache__" bin default metadata
- name: Validate mock app with splunk-appinspect
run: uvx splunk-appinspect inspect ${{ env.MOCK_APP_PATH }}/mock_app.tgz
--included-tags cloud
run: uvx splunk-appinspect inspect ${{ env.MOCK_APP_PATH }}/mock_app.tgz --included-tags cloud
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Python SDK Lint
on: [push, workflow_dispatch]

jobs:
lint-stage:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
Expand All @@ -12,5 +12,5 @@ jobs:
deps-group: lint
- name: Verify uv.lock is up-to-date
run: uv lock --check
- name: Verify against basedpyright baseline
run: uv run --frozen basedpyright
- name: Verify files are linted and formatted
run: make ci-lint
33 changes: 24 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,35 @@
# --no-config skips Splunk's internal PyPI mirror
UV_SYNC_CMD := uv sync --no-config

.PHONY: uv-sync
uv-sync:
.PHONY: install
install:
$(UV_SYNC_CMD) --dev

.PHONY: uv-upgrade
uv-upgrade:
.PHONY: upgrade
upgrade:
$(UV_SYNC_CMD) --dev --upgrade


# Workaround for make being unable to pass arguments to underlying cmd
# $ SDK_DEPS_GROUP="build" make uv-sync-ci
.PHONY: uv-sync-ci
uv-sync-ci:
uv sync --locked --group $(SDK_DEPS_GROUP)
UV_RUN_CMD := uv run --frozen --no-config
.PHONY: lint
lint: lint-python # TODO: Add mbake

.PHONY: lint-python
lint-python:
$(UV_RUN_CMD) basedpyright
$(UV_RUN_CMD) ruff check --fix
$(UV_RUN_CMD) ruff format

UV_RUN_CMD := uv run --frozen --no-config
.PHONY: ci-lint
ci-lint: ci-lint-python # TODO: Add mbake

.PHONY: ci-lint-python
ci-lint-python:
$(UV_RUN_CMD) basedpyright
$(UV_RUN_CMD) ruff check --fix-only
$(UV_RUN_CMD) ruff format --diff

.PHONY: clean
clean:
Expand Down Expand Up @@ -97,4 +112,4 @@ docker-splunk-restart:

.PHONY: docker-tail-python-log
docker-tail-python-log:
docker exec -it $(CONTAINER_NAME) sudo tail $(SPLUNK_HOME)/var/log/splunk/python.log
docker exec -it $(CONTAINER_NAME) sudo tail $(SPLUNK_HOME)/var/log/splunk/python.log
8 changes: 2 additions & 6 deletions examples/ai_custom_alert_app/bin/setup_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@ def setup_logging(app_name: str) -> logging.Logger:
logger = logging.getLogger(app_name)
logger.setLevel(logging.DEBUG)

handler = logging.handlers.RotatingFileHandler(
LOG_PATH, maxBytes=1024 * 1024, backupCount=5
)
handler.setFormatter(
logging.Formatter(f"%(asctime)s %(levelname)s [{app_name}] %(message)s")
)
handler = logging.handlers.RotatingFileHandler(LOG_PATH, maxBytes=1024 * 1024, backupCount=5)
handler.setFormatter(logging.Formatter(f"%(asctime)s %(levelname)s [{app_name}] %(message)s"))
logger.addHandler(handler)
return logger
8 changes: 2 additions & 6 deletions examples/ai_custom_alert_app/bin/threat_level_assessment.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@
# one that might not exist on the filesystem. In such case we unset the env, which
# causes the default Certificate Authorities to be used instead.
CA_TRUST_STORE = "/opt/splunk/openssl/cert.pem"
if os.environ.get("SSL_CERT_FILE") == CA_TRUST_STORE and not os.path.exists(
CA_TRUST_STORE
):
if os.environ.get("SSL_CERT_FILE") == CA_TRUST_STORE and not os.path.exists(CA_TRUST_STORE):
del os.environ["SSL_CERT_FILE"]


Expand Down Expand Up @@ -86,9 +84,7 @@ class AgenticSeverityAssessment(BaseModel):
recommended_action: str


async def invoke_agent(
service: client.Service, alert_data: AlertData
) -> AgenticSeverityAssessment:
async def invoke_agent(service: client.Service, alert_data: AlertData) -> AgenticSeverityAssessment:
async with Agent(
model=LLM_MODEL,
system_prompt=SYSTEM_PROMPT,
Expand Down
6 changes: 2 additions & 4 deletions examples/ai_custom_search_app/bin/agentic_reporting_csc.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from splunklib.searchcommands import (
Configuration,
Option,
dispatch, # pyright: ignore[reportPrivateLocalImportUsage]
dispatch,
validators,
)
from splunklib.searchcommands.eventing_command import EventingCommand
Expand All @@ -43,9 +43,7 @@
# one that might not exist on the filesystem. In such case we unset the env, which
# causes the default Certificate Authorities to be used instead.
CA_TRUST_STORE = "/opt/splunk/openssl/cert.pem"
if os.environ.get("SSL_CERT_FILE") == CA_TRUST_STORE and not os.path.exists(
CA_TRUST_STORE
):
if os.environ.get("SSL_CERT_FILE") == CA_TRUST_STORE and not os.path.exists(CA_TRUST_STORE):
del os.environ["SSL_CERT_FILE"]

APP_NAME = "ai_custom_search_app"
Expand Down
8 changes: 2 additions & 6 deletions examples/ai_custom_search_app/bin/setup_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,8 @@ def setup_logging(app_name: str) -> logging.Logger:
logger = logging.getLogger(app_name)
logger.setLevel(logging.DEBUG)

handler = logging.handlers.RotatingFileHandler(
LOG_FILE, maxBytes=1024 * 1024, backupCount=5
)
handler.setFormatter(
logging.Formatter(f"%(asctime)s %(levelname)s [{app_name}] %(message)s")
)
handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1024 * 1024, backupCount=5)
handler.setFormatter(logging.Formatter(f"%(asctime)s %(levelname)s [{app_name}] %(message)s"))
logger.addHandler(handler)

return logger
4 changes: 1 addition & 3 deletions examples/ai_modinput_app/bin/agentic_weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@
# one that might not exist on the filesystem. In such case we unset the env, which
# causes the default Certificate Authorities to be used instead.
CA_TRUST_STORE = "/opt/splunk/openssl/cert.pem"
if os.environ.get("SSL_CERT_FILE") == CA_TRUST_STORE and not os.path.exists(
CA_TRUST_STORE
):
if os.environ.get("SSL_CERT_FILE") == CA_TRUST_STORE and not os.path.exists(CA_TRUST_STORE):
del os.environ["SSL_CERT_FILE"]


Expand Down
8 changes: 2 additions & 6 deletions examples/ai_modinput_app/bin/setup_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,8 @@ def setup_logging(app_name: str) -> logging.Logger:
logger = logging.getLogger(app_name)
logger.setLevel(logging.DEBUG)

handler = logging.handlers.RotatingFileHandler(
LOG_FILE, maxBytes=1024 * 1024, backupCount=5
)
handler.setFormatter(
logging.Formatter(f"%(asctime)s %(levelname)s [{app_name}] %(message)s")
)
handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1024 * 1024, backupCount=5)
handler.setFormatter(logging.Formatter(f"%(asctime)s %(levelname)s [{app_name}] %(message)s"))
logger.addHandler(handler)

return logger
26 changes: 17 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,25 @@ dependencies = []
# Treat the same as NPM's `dependencies`
[project.optional-dependencies]
compat = ["six>=1.17.0"]
ai = ["httpx==0.28.1", "langchain>=1.2.15", "mcp>=1.27.0", "pydantic>=2.13.1"]
anthropic = ["splunk-sdk[ai]>=2.1.1", "langchain-anthropic>=1.4.0"]
openai = ["splunk-sdk[ai]>=2.1.1", "langchain-openai>=1.1.13"]
ai = ["httpx==0.28.1", "langchain>=1.2.16", "mcp>=1.27.0", "pydantic>=2.13.3"]
anthropic = ["splunk-sdk[ai]>=2.1.1", "langchain-anthropic>=1.4.3"]
openai = ["splunk-sdk[ai]>=2.1.1", "langchain-openai>=1.2.1"]

# Treat the same as NPM's `devDependencies`
[dependency-groups]
test = [
"splunk-sdk[ai]",
"splunk-sdk[ai]>=2.1.1",
"pytest>=9.0.3",
"pytest-cov>=7.1.0",
"pytest-asyncio>=1.3.0",
"python-dotenv>=1.2.2",
"vcrpy>=8.1.1",
]
release = ["build>=1.4.3", "jinja2>=3.1.6", "sphinx>=9.1.0", "twine>=6.2.0"]
lint = ["basedpyright>=1.39.0", "ruff>=0.15.10"]
release = ["build>=1.5.0", "jinja2>=3.1.6", "sphinx>=9.1.0", "twine>=6.2.0"]
lint = ["basedpyright>=1.39.3", "ruff>=0.15.12", "mbake>=1.4.6"]
dev = [
"rich>=14.3.3",
"splunk-sdk[openai, anthropic]",
"rich>=15.0.0",
"splunk-sdk[openai, anthropic]>=2.1.1",
{ include-group = "test" },
{ include-group = "lint" },
{ include-group = "release" },
Expand Down Expand Up @@ -85,17 +85,22 @@ reportUnknownMemberType = false
reportUnusedCallResult = false

# https://docs.astral.sh/ruff/configuration/
[tool.ruff]
line-length = 100

[tool.ruff.lint]
fixable = ["ALL"]
select = [
"ANN", # flake-8-annotations
"B", # flake8-bugbear
"C4", # comprehensions
"DOC", # pydocstyle
# "DOC", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PT", # flake-8-pytest-rules
"RUF", # ruff-specific rules
"SIM", # flake8-simplify
"UP", # pyupgrade
]
ignore = [
Expand All @@ -104,3 +109,6 @@ ignore = [

[tool.ruff.lint.isort]
combine-as-imports = true

[tool.ruff.format]
docstring-code-format = true
4 changes: 1 addition & 3 deletions splunklib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@
# To set the logging level of splunklib
# ex. To enable debug logs, call this method with parameter 'logging.DEBUG'
# default logging level is set to 'WARNING'
def setup_logging(
level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE_FORMAT
):
def setup_logging(level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE_FORMAT):
logging.basicConfig(level=level, format=log_format, datefmt=date_format)


Expand Down
28 changes: 7 additions & 21 deletions splunklib/ai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,7 @@ async def _start_agent(self) -> AsyncGenerator[Self]:
"internal error: _impl was not set to None after agent invocation"
)

splunk_username = await asyncio.to_thread(
lambda: _get_splunk_username(self._service)
)
splunk_username = await asyncio.to_thread(lambda: _get_splunk_username(self._service))
_validate_agent_privileges(splunk_username)

self.logger.debug(f"Creating agent {self.name=}; {self.trace_id=}")
Expand All @@ -201,9 +199,7 @@ async def _start_agent(self) -> AsyncGenerator[Self]:

self._impl = None

async def _load_tools(
self, stack: AsyncExitStack, splunk_username: str
) -> list[Tool]:
async def _load_tools(self, stack: AsyncExitStack, splunk_username: str) -> list[Tool]:
tools: list[Tool] = []
if not self.tool_settings.local and not self.tool_settings.remote:
return tools
Expand Down Expand Up @@ -234,9 +230,7 @@ async def _load_tools(
if self.tool_settings.remote:
self.logger.debug("Probing MCP Server App availability")
remote_session = await stack.enter_async_context(
connect_remote_mcp(
self._service, app_id, self.trace_id, splunk_username
)
connect_remote_mcp(self._service, app_id, self.trace_id, splunk_username)
)

if remote_session:
Expand All @@ -252,9 +246,7 @@ async def _load_tools(
allowlist = self.tool_settings.remote.allowlist
remote_tools = [rt for rt in remote_tools if allowlist.is_allowed(rt)]

self.logger.debug(
f"Loaded remote_tools={[t.name for t in remote_tools]}"
)
self.logger.debug(f"Loaded remote_tools={[t.name for t in remote_tools]}")
tools.extend(remote_tools)

return tools
Expand All @@ -265,13 +257,9 @@ async def __aenter__(self) -> Self:
self._agent_context_manager = self._start_agent()
return await self._agent_context_manager.__aenter__()

async def __aexit__(
self, exc_type: ..., exc_value: ..., traceback: ...
) -> bool | None:
async def __aexit__(self, exc_type: ..., exc_value: ..., traceback: ...) -> bool | None:
assert self._agent_context_manager is not None
result = await self._agent_context_manager.__aexit__(
exc_type, exc_value, traceback
)
result = await self._agent_context_manager.__aexit__(exc_type, exc_value, traceback)
self._agent_context_manager = None
return result

Expand Down Expand Up @@ -324,9 +312,7 @@ def _local_tools_path() -> tuple[str | None, str]:
app_id, app_dir = locate_app()
local_tools_path = build_local_tools_path(app_dir)

assert app_id is not None, (
"_load_tools_from_mcp was mocked, but _testing_app_id not"
)
assert app_id is not None, "_load_tools_from_mcp was mocked, but _testing_app_id not"

if not os.path.exists(local_tools_path):
local_tools_path = None
Expand Down
4 changes: 1 addition & 3 deletions splunklib/ai/conversation_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
class ConversationStore(Protocol):
async def get_messages(self, thread_id: str) -> Sequence[BaseMessage]: ...

async def store_messages(
self, thread_id: str, messages: list[BaseMessage]
) -> None: ...
async def store_messages(self, thread_id: str, messages: list[BaseMessage]) -> None: ...


class InMemoryStore(ConversationStore):
Expand Down
Loading
Loading