aixtools 0.1.6__tar.gz → 0.1.8__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.

Potentially problematic release.


This version of aixtools might be problematic. Click here for more details.

Files changed (139) hide show
  1. {aixtools-0.1.6 → aixtools-0.1.8}/.env_template +0 -9
  2. aixtools-0.1.8/.github/workflows/release.yml +59 -0
  3. {aixtools-0.1.6 → aixtools-0.1.8}/PKG-INFO +1 -1
  4. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/_version.py +3 -3
  5. aixtools-0.1.8/aixtools/vault/vault.py +100 -0
  6. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools.egg-info/PKG-INFO +1 -1
  7. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools.egg-info/SOURCES.txt +0 -1
  8. {aixtools-0.1.6 → aixtools-0.1.8}/docker/mcp-base/Dockerfile +1 -1
  9. aixtools-0.1.8/tests/unit/vault/test_vault.py +171 -0
  10. aixtools-0.1.6/.github/workflows/build_and_publish_docker.yml +0 -34
  11. aixtools-0.1.6/.github/workflows/release.yml +0 -36
  12. aixtools-0.1.6/aixtools/vault/vault.py +0 -73
  13. aixtools-0.1.6/tests/unit/vault/test_vault.py +0 -114
  14. {aixtools-0.1.6 → aixtools-0.1.8}/.github/workflows/lint-and-test.yml +0 -0
  15. {aixtools-0.1.6 → aixtools-0.1.8}/.gitignore +0 -0
  16. {aixtools-0.1.6 → aixtools-0.1.8}/.python-version +0 -0
  17. {aixtools-0.1.6 → aixtools-0.1.8}/.roo/rules/rules-mcp.md +0 -0
  18. {aixtools-0.1.6 → aixtools-0.1.8}/.roo/rules/rules.md +0 -0
  19. {aixtools-0.1.6 → aixtools-0.1.8}/.vscode/settings.json +0 -0
  20. {aixtools-0.1.6 → aixtools-0.1.8}/README.md +0 -0
  21. {aixtools-0.1.6 → aixtools-0.1.8}/README.ori.md +0 -0
  22. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/config.toml +0 -0
  23. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/bn.json +0 -0
  24. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/en-US.json +0 -0
  25. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/gu.json +0 -0
  26. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/he-IL.json +0 -0
  27. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/hi.json +0 -0
  28. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/ja.json +0 -0
  29. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/kn.json +0 -0
  30. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/ml.json +0 -0
  31. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/mr.json +0 -0
  32. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/nl.json +0 -0
  33. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/ta.json +0 -0
  34. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/te.json +0 -0
  35. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/.chainlit/translations/zh-CN.json +0 -0
  36. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/__init__.py +0 -0
  37. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/app.py +0 -0
  38. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/google_sdk/__init__.py +0 -0
  39. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/google_sdk/card.py +0 -0
  40. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py +0 -0
  41. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py +0 -0
  42. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/google_sdk/remote_agent_connection.py +0 -0
  43. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/google_sdk/utils.py +0 -0
  44. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/a2a/utils.py +0 -0
  45. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/agents/__init__.py +0 -0
  46. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/agents/agent.py +0 -0
  47. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/agents/agent_batch.py +0 -0
  48. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/agents/prompt.py +0 -0
  49. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/app.py +0 -0
  50. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/chainlit.md +0 -0
  51. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/context.py +0 -0
  52. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/db/__init__.py +0 -0
  53. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/db/database.py +0 -0
  54. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/db/vector_db.py +0 -0
  55. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/google/client.py +0 -0
  56. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/log_view/__init__.py +0 -0
  57. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/log_view/app.py +0 -0
  58. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/log_view/display.py +0 -0
  59. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/log_view/export.py +0 -0
  60. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/log_view/filters.py +0 -0
  61. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/log_view/log_utils.py +0 -0
  62. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/log_view/node_summary.py +0 -0
  63. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logfilters/__init__.py +0 -0
  64. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logfilters/context_filter.py +0 -0
  65. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logging/__init__.py +0 -0
  66. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logging/log_objects.py +0 -0
  67. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logging/logging_config.py +0 -0
  68. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logging/mcp_log_models.py +0 -0
  69. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logging/mcp_logger.py +0 -0
  70. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logging/model_patch_logging.py +0 -0
  71. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/logging/open_telemetry.py +0 -0
  72. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/mcp/__init__.py +0 -0
  73. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/mcp/client.py +0 -0
  74. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/mcp/example_client.py +0 -0
  75. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/mcp/example_server.py +0 -0
  76. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/mcp/fast_mcp_log.py +0 -0
  77. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/mcp/faulty_mcp.py +0 -0
  78. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/model_patch/model_patch.py +0 -0
  79. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/server/__init__.py +0 -0
  80. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/server/app_mounter.py +0 -0
  81. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/server/path.py +0 -0
  82. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/server/utils.py +0 -0
  83. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/testing/__init__.py +0 -0
  84. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/testing/aix_test_model.py +0 -0
  85. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/testing/mock_tool.py +0 -0
  86. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/testing/model_patch_cache.py +0 -0
  87. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/tools/doctor/__init__.py +0 -0
  88. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/tools/doctor/tool_doctor.py +0 -0
  89. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/tools/doctor/tool_recommendation.py +0 -0
  90. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/__init__.py +0 -0
  91. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/chainlit/cl_agent_show.py +0 -0
  92. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/chainlit/cl_utils.py +0 -0
  93. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/config.py +0 -0
  94. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/config_util.py +0 -0
  95. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/enum_with_description.py +0 -0
  96. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/files.py +0 -0
  97. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/persisted_dict.py +0 -0
  98. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/utils/utils.py +0 -0
  99. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools/vault/__init__.py +0 -0
  100. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools.egg-info/dependency_links.txt +0 -0
  101. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools.egg-info/entry_points.txt +0 -0
  102. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools.egg-info/requires.txt +0 -0
  103. {aixtools-0.1.6 → aixtools-0.1.8}/aixtools.egg-info/top_level.txt +0 -0
  104. {aixtools-0.1.6 → aixtools-0.1.8}/notebooks/example_faulty_mcp_server.ipynb +0 -0
  105. {aixtools-0.1.6 → aixtools-0.1.8}/notebooks/example_mcp_server_stdio.ipynb +0 -0
  106. {aixtools-0.1.6 → aixtools-0.1.8}/notebooks/example_raw_mcp_client.ipynb +0 -0
  107. {aixtools-0.1.6 → aixtools-0.1.8}/notebooks/example_tool_doctor.ipynb +0 -0
  108. {aixtools-0.1.6 → aixtools-0.1.8}/pyproject.toml +0 -0
  109. {aixtools-0.1.6 → aixtools-0.1.8}/scripts/config.sh +0 -0
  110. {aixtools-0.1.6 → aixtools-0.1.8}/scripts/lint.sh +0 -0
  111. {aixtools-0.1.6 → aixtools-0.1.8}/scripts/log_view.sh +0 -0
  112. {aixtools-0.1.6 → aixtools-0.1.8}/scripts/run_example_mcp_server.sh +0 -0
  113. {aixtools-0.1.6 → aixtools-0.1.8}/scripts/run_faulty_mcp_server.sh +0 -0
  114. {aixtools-0.1.6 → aixtools-0.1.8}/scripts/run_server.sh +0 -0
  115. {aixtools-0.1.6 → aixtools-0.1.8}/scripts/test.sh +0 -0
  116. {aixtools-0.1.6 → aixtools-0.1.8}/setup.cfg +0 -0
  117. {aixtools-0.1.6 → aixtools-0.1.8}/tests/__init__.py +0 -0
  118. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/__init__.py +0 -0
  119. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/__init__.py +0 -0
  120. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/google_sdk/__init__.py +0 -0
  121. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/google_sdk/pydantic_ai_adapter/__init__.py +0 -0
  122. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_agent_executor.py +0 -0
  123. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_storage.py +0 -0
  124. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/google_sdk/test_card.py +0 -0
  125. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/google_sdk/test_remote_agent_connection.py +0 -0
  126. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/a2a/google_sdk/test_utils.py +0 -0
  127. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/agents/__init__.py +0 -0
  128. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/agents/test_prompt.py +0 -0
  129. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/google/__init__.py +0 -0
  130. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/google/test_client.py +0 -0
  131. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/mcp/__init__.py +0 -0
  132. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/mcp/test_client.py +0 -0
  133. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/server/__init__.py +0 -0
  134. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/server/test_path.py +0 -0
  135. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/server/test_utils.py +0 -0
  136. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/utils/__init__.py +0 -0
  137. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/utils/test_files.py +0 -0
  138. {aixtools-0.1.6 → aixtools-0.1.8}/tests/unit/vault/__init__.py +0 -0
  139. {aixtools-0.1.6 → aixtools-0.1.8}/uv.lock +0 -0
@@ -45,14 +45,5 @@ OLLAMA_URL=http://localhost:11434/v1
45
45
  OPENAI_VDB_EMBEDDINGS_MODEL_NAME=text-embedding-3-small
46
46
  OLLAMA_VDB_EMBEDDINGS_MODEL_NAME=snowflake-arctic-embed2:latest
47
47
 
48
- # Docker and CI/CD Configuration (optional)
49
- # These variables are used for Docker builds and GitHub workflows
50
- DOCKER_WORKFLOW_REPO=your-org/your-reusable-workflows
51
- DOCKER_REPO_NAME=your-docker-repo
52
- DOCKER_REPO_PATH=your-path
53
- DOCKER_REGISTRY=your-registry.com:443
54
- DOCKER_ARTIFACTORY_USERNAME=your_username
55
- # DOCKER_ARTIFACTORY_API_KEY should be set as a GitHub secret
56
-
57
48
  # Custom certificate for Docker builds (optional)
58
49
  # CUSTOM_CERT_FILE=/path/to/your/certificate.crt
@@ -0,0 +1,59 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ runs-on: ubuntu-latest
10
+ environment: release
11
+ env:
12
+ DOCKER_IMAGE_NAME: mcp-base
13
+ AZ_ARTIFACTORY_ODSP_DOCKER_REGISTRY: docker-l-odsp.artifactory.astrazeneca.com
14
+ AZ_ARTIFACTORY_ODSP_DOCKER_NAMESPACE: navari
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0 # Fetch full history for setuptools_scm
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v4
24
+
25
+ - name: Install build dependencies
26
+ run: uv sync --group dev
27
+
28
+ - name: Build Python package
29
+ run: uv run python -m build
30
+
31
+ - name: Check Python package contents
32
+ run: |
33
+ ls -la dist/
34
+ uv run python -m twine check dist/*
35
+
36
+ - name: Publish Python package to PyPI
37
+ uses: pypa/gh-action-pypi-publish@release/v1
38
+ with:
39
+ password: ${{ secrets.PYPI_API_TOKEN }}
40
+ skip-existing: true
41
+
42
+ - name: Login to Docker Registry (JFrog Artifactory 🐸)
43
+ uses: docker/login-action@v3
44
+ with:
45
+ registry: ${{ env.AZ_ARTIFACTORY_ODSP_DOCKER_REGISTRY }}
46
+ username: ${{ vars.AZ_ARTIFACTORY_ODSP_DOCKER_USER }}
47
+ password: ${{ secrets.AZ_ARTIFACTORY_ODSP_DOCKER_PASSWORD }}
48
+
49
+ - name: Build and push Docker image 🐳
50
+ id: docker_build
51
+ uses: docker/build-push-action@v6
52
+ env:
53
+ AZ_ARTIFACTORY_ODSP_DOCKER_IMAGE_REF: ${{ env.AZ_ARTIFACTORY_ODSP_DOCKER_REGISTRY }}/${{ env.AZ_ARTIFACTORY_ODSP_DOCKER_NAMESPACE }}/${{ env.DOCKER_IMAGE_NAME }}
54
+ with:
55
+ push: true
56
+ context: docker/${{ env.DOCKER_IMAGE_NAME }}
57
+ tags: |
58
+ ${{ env.AZ_ARTIFACTORY_ODSP_DOCKER_IMAGE_REF }}:${{ github.ref_name }}
59
+ ${{ env.AZ_ARTIFACTORY_ODSP_DOCKER_IMAGE_REF }}:latest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aixtools
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Tools for AI exploration and debugging
5
5
  Requires-Python: >=3.11.2
6
6
  Description-Content-Type: text/markdown
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.6'
32
- __version_tuple__ = version_tuple = (0, 1, 6)
31
+ __version__ = version = '0.1.8'
32
+ __version_tuple__ = version_tuple = (0, 1, 8)
33
33
 
34
- __commit_id__ = commit_id = 'g6abaa9c74'
34
+ __commit_id__ = commit_id = 'gb9edf75d4'
@@ -0,0 +1,100 @@
1
+ # ruff: noqa: PLR0913
2
+ """
3
+ Provides a Vault client for storing and retrieving user service api keys.
4
+ """
5
+
6
+ import logging
7
+ from typing import Dict, Optional
8
+
9
+ import hvac
10
+ from hvac.exceptions import InvalidPath
11
+
12
+ from aixtools.utils import config
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class VaultAuthError(Exception):
18
+ """Exception raised for vault authentication errors."""
19
+
20
+
21
+ class VaultClient:
22
+ """Vault client for storing and retrieving user service api keys."""
23
+
24
+ def __init__(self):
25
+ self.client = hvac.Client(url=config.VAULT_ADDRESS, token=config.VAULT_TOKEN)
26
+
27
+ if not self.client.is_authenticated():
28
+ raise VaultAuthError("Vault client authentication failed. Check vault_token.")
29
+
30
+ def _get_secret_path(self, user_id: str, service_name: str) -> str:
31
+ """Generate the vault secret path for a user and service."""
32
+ return f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}/{service_name}"
33
+
34
+ def store_user_service_api_key(self, *, user_id: str, service_name: str, user_api_key: str):
35
+ """
36
+ Store user's service api key in the Vault at the specified vault mount
37
+ point, where the path is <path_prefix>/<env>/<user_id>/<service_name>.
38
+
39
+ This is a convenience method for storing a single API key.
40
+ For storing multiple secrets, use store_user_service_secret().
41
+ """
42
+ secret_dict = {"user-api-key": user_api_key}
43
+ self.store_user_service_secret(user_id=user_id, service_name=service_name, secret_data=secret_dict)
44
+
45
+ def read_user_service_api_key(self, *, user_id: str, service_name: str) -> Optional[str]:
46
+ """
47
+ Read user's service api key in from vault at the specified mount point,
48
+ where the path is <path_prefix>/<env>/<user_id>/<service_name>.
49
+
50
+ This is a convenience method for reading a single API key.
51
+ For reading multiple secrets, use read_user_service_secret().
52
+ """
53
+ secret_data = self.read_user_service_secret(user_id=user_id, service_name=service_name)
54
+ if secret_data is None:
55
+ return None
56
+ return secret_data.get("user-api-key")
57
+
58
+ def store_user_service_secret(self, *, user_id: str, service_name: str, secret_data: Dict[str, str]):
59
+ """
60
+ Store complete user service secret with multiple key-value pairs in the Vault
61
+ at the specified vault mount point, where the path is <path_prefix>/<env>/<user_id>/<service_name>.
62
+ """
63
+ secret_path = None
64
+ try:
65
+ secret_path = self._get_secret_path(user_id, service_name)
66
+ logger.info("Writing complete secret to path %s", secret_path)
67
+ self.client.secrets.kv.v2.create_or_update_secret(
68
+ secret_path, secret=secret_data, mount_point=config.VAULT_MOUNT_POINT
69
+ )
70
+
71
+ logger.info("Complete secret written to path %s", secret_path)
72
+ except Exception as e:
73
+ logger.error("Failed to write complete secret to path %s: %s", secret_path, str(e))
74
+ raise VaultAuthError(e) from e
75
+
76
+ def read_user_service_secret(self, *, user_id: str, service_name: str) -> Optional[Dict[str, str]]:
77
+ """
78
+ Read complete user service secret from vault at the specified mount point,
79
+ where the path is <path_prefix>/<env>/<user_id>/<service_name>.
80
+ Returns all key-value pairs in the secret or None if the secret doesn't exist.
81
+ """
82
+ secret_path = None
83
+
84
+ try:
85
+ secret_path = self._get_secret_path(user_id, service_name)
86
+ logger.info("Reading complete secret from path %s", secret_path)
87
+ response = self.client.secrets.kv.v2.read_secret_version(
88
+ secret_path, mount_point=config.VAULT_MOUNT_POINT, raise_on_deleted_version=True
89
+ )
90
+ secret_data = response["data"]["data"]
91
+ logger.info("Complete secret read from path %s", secret_path)
92
+ return secret_data
93
+ except InvalidPath:
94
+ # Secret path does not exist
95
+ logger.warning("Secret path does not exist %s", secret_path)
96
+ return None
97
+
98
+ except Exception as e:
99
+ logger.error("Failed to read complete secret from path %s: %s", secret_path, str(e))
100
+ raise VaultAuthError(e) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aixtools
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Tools for AI exploration and debugging
5
5
  Requires-Python: >=3.11.2
6
6
  Description-Content-Type: text/markdown
@@ -5,7 +5,6 @@ README.md
5
5
  README.ori.md
6
6
  pyproject.toml
7
7
  uv.lock
8
- .github/workflows/build_and_publish_docker.yml
9
8
  .github/workflows/lint-and-test.yml
10
9
  .github/workflows/release.yml
11
10
  .roo/rules/rules-mcp.md
@@ -1,4 +1,4 @@
1
- FROM ubuntu:24.04
1
+ FROM ubuntu:22.04
2
2
 
3
3
  RUN apt-get -y update && \
4
4
  apt-get -y install git curl sudo ca-certificates
@@ -0,0 +1,171 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+ from hvac.exceptions import InvalidPath
5
+
6
+ from aixtools.vault.vault import VaultAuthError, VaultClient
7
+
8
+
9
+ @pytest.fixture
10
+ def patched_vault_client():
11
+ with patch("aixtools.vault.vault.hvac.Client") as mock_hvac_client_cls:
12
+ fake_hvac_client = MagicMock()
13
+ fake_hvac_client.is_authenticated.return_value = True
14
+ mock_hvac_client_cls.return_value = fake_hvac_client
15
+
16
+ client = VaultClient()
17
+ return client
18
+
19
+
20
+ @pytest.fixture
21
+ def valid_params():
22
+ return {
23
+ "vault_mount_point": "secret",
24
+ "path_prefix": "path",
25
+ "env": "dev",
26
+ "user_id": "test-user",
27
+ "service_name": "test-service",
28
+ "user_api_key": "test-api-key",
29
+ }
30
+
31
+
32
+ def test_store_user_service_api_key_success(patched_vault_client, valid_params):
33
+ patched_vault_client.client.secrets.kv.v2.create_or_update_secret.return_value = {}
34
+
35
+ patched_vault_client.store_user_service_api_key(
36
+ user_id=valid_params["user_id"],
37
+ service_name=valid_params["service_name"],
38
+ user_api_key=valid_params["user_api_key"],
39
+ )
40
+
41
+ secret_path = (
42
+ f"{valid_params['path_prefix']}/{valid_params['env']}/{valid_params['user_id']}/{valid_params['service_name']}"
43
+ )
44
+
45
+ patched_vault_client.client.secrets.kv.v2.create_or_update_secret.assert_called_once_with(
46
+ secret_path,
47
+ secret={"user-api-key": valid_params["user_api_key"]},
48
+ mount_point=valid_params["vault_mount_point"],
49
+ )
50
+
51
+
52
+ def test_store_user_service_api_key_invalid_path(patched_vault_client, valid_params):
53
+ patched_vault_client.client.secrets.kv.v2.create_or_update_secret.side_effect = Exception("Invalid Path")
54
+
55
+ with pytest.raises(VaultAuthError, match="Invalid Path"):
56
+ patched_vault_client.store_user_service_api_key(
57
+ user_id=valid_params["user_id"],
58
+ service_name=valid_params["service_name"],
59
+ user_api_key=valid_params["user_api_key"],
60
+ )
61
+ patched_vault_client.client.assert_not_called()
62
+
63
+
64
+ def test_read_user_service_api_key_success(patched_vault_client):
65
+ """Test successful read of user service API key."""
66
+ mock_response = {"data": {"data": {"user-api-key": "test-api-key"}}}
67
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.return_value = mock_response
68
+
69
+ result = patched_vault_client.read_user_service_api_key(user_id="test-user", service_name="test-service")
70
+
71
+ assert result == "test-api-key"
72
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
73
+ "path/dev/test-user/test-service", mount_point="secret", raise_on_deleted_version=True
74
+ )
75
+
76
+
77
+ def test_read_user_service_api_key_secret_not_found(patched_vault_client):
78
+ """Test read_user_service_api_key when the secret path does not exist."""
79
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = InvalidPath
80
+
81
+ result = patched_vault_client.read_user_service_api_key(user_id="test-user", service_name="test-service")
82
+
83
+ assert result is None
84
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
85
+ "path/dev/test-user/test-service", mount_point="secret", raise_on_deleted_version=True
86
+ )
87
+
88
+
89
+ def test_read_user_service_api_key_unexpected_error(patched_vault_client):
90
+ """Test read_user_service_api_key when an unexpected exception occurs."""
91
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = Exception("Unexpected error")
92
+
93
+ with pytest.raises(VaultAuthError, match="Unexpected error"):
94
+ patched_vault_client.read_user_service_api_key(user_id="test-user", service_name="test-service")
95
+
96
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
97
+ "path/dev/test-user/test-service", mount_point="secret", raise_on_deleted_version=True
98
+ )
99
+
100
+
101
+ def test_store_user_service_secret_success(patched_vault_client, valid_params):
102
+ """Test successful storage of complete user service secret."""
103
+ secret_data = {"api_key": "test-api-key", "token": "test-token", "endpoint": "https://api.example.com"}
104
+ patched_vault_client.client.secrets.kv.v2.create_or_update_secret.return_value = {}
105
+
106
+ patched_vault_client.store_user_service_secret(
107
+ user_id=valid_params["user_id"],
108
+ service_name=valid_params["service_name"],
109
+ secret_data=secret_data,
110
+ )
111
+
112
+ secret_path = (
113
+ f"{valid_params['path_prefix']}/{valid_params['env']}/{valid_params['user_id']}/{valid_params['service_name']}"
114
+ )
115
+
116
+ patched_vault_client.client.secrets.kv.v2.create_or_update_secret.assert_called_once_with(
117
+ secret_path,
118
+ secret=secret_data,
119
+ mount_point=valid_params["vault_mount_point"],
120
+ )
121
+
122
+
123
+ def test_store_user_service_secret_error(patched_vault_client, valid_params):
124
+ """Test store_user_service_secret when an error occurs."""
125
+ secret_data = {"api_key": "test-api-key", "token": "test-token"}
126
+ patched_vault_client.client.secrets.kv.v2.create_or_update_secret.side_effect = Exception("Storage error")
127
+
128
+ with pytest.raises(VaultAuthError, match="Storage error"):
129
+ patched_vault_client.store_user_service_secret(
130
+ user_id=valid_params["user_id"],
131
+ service_name=valid_params["service_name"],
132
+ secret_data=secret_data,
133
+ )
134
+
135
+
136
+ def test_read_user_service_secret_success(patched_vault_client):
137
+ """Test successful read of complete user service secret."""
138
+ secret_data = {"api_key": "test-api-key", "token": "test-token", "endpoint": "https://api.example.com"}
139
+ mock_response = {"data": {"data": secret_data}}
140
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.return_value = mock_response
141
+
142
+ result = patched_vault_client.read_user_service_secret(user_id="test-user", service_name="test-service")
143
+
144
+ assert result == secret_data
145
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
146
+ "path/dev/test-user/test-service", mount_point="secret", raise_on_deleted_version=True
147
+ )
148
+
149
+
150
+ def test_read_user_service_secret_not_found(patched_vault_client):
151
+ """Test read_user_service_secret when the secret path does not exist."""
152
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = InvalidPath
153
+
154
+ result = patched_vault_client.read_user_service_secret(user_id="test-user", service_name="test-service")
155
+
156
+ assert result is None
157
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
158
+ "path/dev/test-user/test-service", mount_point="secret", raise_on_deleted_version=True
159
+ )
160
+
161
+
162
+ def test_read_user_service_secret_error(patched_vault_client):
163
+ """Test read_user_service_secret when an unexpected exception occurs."""
164
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = Exception("Read error")
165
+
166
+ with pytest.raises(VaultAuthError, match="Read error"):
167
+ patched_vault_client.read_user_service_secret(user_id="test-user", service_name="test-service")
168
+
169
+ patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
170
+ "path/dev/test-user/test-service", mount_point="secret", raise_on_deleted_version=True
171
+ )
@@ -1,34 +0,0 @@
1
- name: Build and Publish
2
-
3
- on:
4
- push:
5
- branches: [main]
6
-
7
- jobs:
8
- build-and-publish-docker-images:
9
- runs-on: ubuntu-latest
10
- steps:
11
- - name: build and publish
12
- uses: ${{ vars.DOCKER_WORKFLOW_REPO }}/.github/workflows/publish_docker_artifactory.yml@main
13
- with:
14
- REPO_NAME: ${{ vars.DOCKER_REPO_NAME }}
15
- REPO_PATH: ${{ vars.DOCKER_REPO_PATH }}
16
- IMAGE_NAME: mcp-base
17
- DOCKER_TAG: ${{ github.sha }}
18
- CONTEXT: docker/mcp-base
19
- # secrets:
20
- DOS_ARTIFACTORY_USERNAME: ${{ vars.DOCKER_ARTIFACTORY_USERNAME }}
21
- DOS_ARTIFACTORY_API_KEY: ${{ secrets.DOCKER_ARTIFACTORY_API_KEY }}
22
- - name: test docker interaction
23
- run: |
24
- echo "Testing Docker interaction..."
25
- docker --version
26
- docker images
27
- docker rm ${{ vars.DOCKER_REGISTRY }}/${{ vars.DOCKER_REPO_PATH }}/mcp-base:${{ github.sha }} || true
28
- docker images
29
- docker pull ${{ vars.DOCKER_REGISTRY }}/${{ vars.DOCKER_REPO_PATH }}/mcp-base:${{ github.sha }} || true
30
- docker images
31
- echo "Docker interaction test completed."
32
- env:
33
- DOCKER_BUILDKIT: 1
34
- DOCKER_CLI_EXPERIMENTAL: enabled
@@ -1,36 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- jobs:
8
- build-and-publish:
9
- runs-on: ubuntu-latest
10
- environment: release
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v4
15
- with:
16
- fetch-depth: 0 # Fetch full history for setuptools_scm
17
-
18
- - name: Install uv
19
- uses: astral-sh/setup-uv@v4
20
-
21
- - name: Install build dependencies
22
- run: uv sync --group dev
23
-
24
- - name: Build package
25
- run: uv run python -m build
26
-
27
- - name: Check package contents
28
- run: |
29
- ls -la dist/
30
- uv run python -m twine check dist/*
31
-
32
- - name: Publish to PyPI
33
- uses: pypa/gh-action-pypi-publish@release/v1
34
- with:
35
- password: ${{ secrets.PYPI_API_TOKEN }}
36
- skip-existing: true
@@ -1,73 +0,0 @@
1
- # ruff: noqa: PLR0913
2
- """
3
- Provides a Vault client for storing and retrieving user service api keys.
4
- """
5
-
6
- import logging
7
- from typing import Optional
8
-
9
- import hvac
10
- from hvac.exceptions import InvalidPath
11
-
12
- from aixtools.utils import config
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class VaultAuthError(Exception):
18
- """Exception raised for vault authentication errors."""
19
-
20
-
21
- class VaultClient:
22
- """Vault client for storing and retrieving user service api keys."""
23
-
24
- def __init__(self):
25
- self.client = hvac.Client(url=config.VAULT_ADDRESS, token=config.VAULT_TOKEN)
26
-
27
- if not self.client.is_authenticated():
28
- raise VaultAuthError("Vault client authentication failed. Check vault_token.")
29
-
30
- def store_user_service_api_key(self, *, user_id: str, service_name: str, user_api_key: str):
31
- """
32
- Store user's service api key in the Vault at the specified vault mount
33
- point, where the path is <path_prefix>/<env>/<user_id>/<service_name>.
34
- """
35
- secret_path = None
36
- try:
37
- secret_path = f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}/{service_name}"
38
- print("secret_path", secret_path)
39
- secret_dict = {"user-api-key": user_api_key}
40
- self.client.secrets.kv.v2.create_or_update_secret(
41
- secret_path, secret=secret_dict, mount_point=config.VAULT_MOUNT_POINT
42
- )
43
-
44
- logger.info("Secret written to path %s", secret_path)
45
- except Exception as e:
46
- logger.error("Failed to write secret to path %s: %s", secret_path, str(e))
47
- raise VaultAuthError(e) from e
48
-
49
- def read_user_service_api_key(self, *, user_id: str, service_name) -> Optional[str]:
50
- """
51
- Read user's service api key in from vault at the specified mount point,
52
- where the path is <path_prefix>/<env>/<user_id>/<service_name>.
53
- """
54
- secret_path = None
55
-
56
- try:
57
- secret_path = f"{config.VAULT_PATH_PREFIX}/{config.VAULT_ENV}/{user_id}/{service_name}"
58
- logger.info("Reading secret from path %s", secret_path)
59
- response = self.client.secrets.kv.v2.read_secret_version(
60
- secret_path, mount_point=config.VAULT_MOUNT_POINT, raise_on_deleted_version=True
61
- )
62
- secret_data = response["data"]["data"]
63
- user_api_key = secret_data["user-api-key"]
64
- logger.info("Secret read from path %s ", secret_path)
65
- return user_api_key
66
- except InvalidPath:
67
- # Secret path does not exist
68
- logger.warning("Secret path does not exist %s ", secret_path)
69
- return None
70
-
71
- except Exception as e:
72
- logger.error("Failed to read secret from path %s: %s", secret_path, str(e))
73
- raise VaultAuthError(e) from e
@@ -1,114 +0,0 @@
1
- from unittest.mock import MagicMock, patch
2
-
3
- import pytest
4
- from hvac.exceptions import InvalidPath
5
-
6
- from aixtools.vault.vault import VaultClient, VaultAuthError
7
-
8
-
9
- @pytest.fixture
10
- def patched_vault_client():
11
- with patch("aixtools.vault.vault.hvac.Client") as mock_hvac_client_cls:
12
- fake_hvac_client = MagicMock()
13
- fake_hvac_client.is_authenticated.return_value = True
14
- mock_hvac_client_cls.return_value = fake_hvac_client
15
-
16
- client = VaultClient()
17
- return client
18
-
19
-
20
- @pytest.fixture
21
- def valid_params():
22
- return {
23
- "vault_mount_point": "secret",
24
- "path_prefix": "path",
25
- "env": "dev",
26
- "user_id": "test-user",
27
- "service_name": "test-service",
28
- "user_api_key": "test-api-key"
29
- }
30
-
31
-
32
- def test_store_user_service_api_key_success(patched_vault_client, valid_params):
33
- patched_vault_client.client.secrets.kv.v2.create_or_update_secret.return_value = {}
34
-
35
- patched_vault_client.store_user_service_api_key(user_id=valid_params["user_id"],
36
- service_name=valid_params['service_name'],
37
- user_api_key=valid_params['user_api_key'])
38
-
39
- secret_path = f"{valid_params['path_prefix']}/{valid_params['env']}/{valid_params['user_id']}/{valid_params['service_name']}"
40
-
41
- print("expected secret path", secret_path)
42
- patched_vault_client.client.secrets.kv.v2.create_or_update_secret.assert_called_once_with(
43
- secret_path,
44
- secret={"user-api-key": valid_params["user_api_key"]},
45
- mount_point=valid_params["vault_mount_point"]
46
- )
47
-
48
-
49
- def test_store_user_service_api_key_invalid_path(patched_vault_client, valid_params):
50
- patched_vault_client.client.secrets.kv.v2.create_or_update_secret.side_effect = Exception("Invalid Path")
51
-
52
- with pytest.raises(VaultAuthError, match="Invalid Path"):
53
- patched_vault_client.store_user_service_api_key(user_id=valid_params["user_id"],
54
- service_name=valid_params['service_name'],
55
- user_api_key=valid_params['user_api_key'])
56
- patched_vault_client.client.assert_not_called()
57
-
58
-
59
- def test_read_user_service_api_key_success(patched_vault_client):
60
- """Test successful read of user service API key."""
61
- mock_response = {
62
- "data": {
63
- "data": {
64
- "user-api-key": "test-api-key"
65
- }
66
- }
67
- }
68
- patched_vault_client.client.secrets.kv.v2.read_secret_version.return_value = mock_response
69
-
70
- result = patched_vault_client.read_user_service_api_key(
71
- user_id="test-user",
72
- service_name="test-service"
73
- )
74
-
75
- assert result == "test-api-key"
76
- patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
77
- "path/dev/test-user/test-service",
78
- mount_point='secret',
79
- raise_on_deleted_version=True
80
- )
81
-
82
-
83
- def test_read_user_service_api_key_secret_not_found(patched_vault_client):
84
- """Test read_user_service_api_key when the secret path does not exist."""
85
- patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = InvalidPath
86
-
87
- result = patched_vault_client.read_user_service_api_key(
88
- user_id="test-user",
89
- service_name="test-service"
90
- )
91
-
92
- assert result is None
93
- patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
94
- "path/dev/test-user/test-service",
95
- mount_point="secret",
96
- raise_on_deleted_version=True
97
- )
98
-
99
-
100
- def test_read_user_service_api_key_unexpected_error(patched_vault_client):
101
- """Test read_user_service_api_key when an unexpected exception occurs."""
102
- patched_vault_client.client.secrets.kv.v2.read_secret_version.side_effect = Exception("Unexpected error")
103
-
104
- with pytest.raises(VaultAuthError, match="Unexpected error"):
105
- patched_vault_client.read_user_service_api_key(
106
- user_id="test-user",
107
- service_name="test-service"
108
- )
109
-
110
- patched_vault_client.client.secrets.kv.v2.read_secret_version.assert_called_once_with(
111
- "path/dev/test-user/test-service",
112
- mount_point="secret",
113
- raise_on_deleted_version=True
114
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes