aixtools 0.1.6__tar.gz → 0.1.7__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 (138) hide show
  1. {aixtools-0.1.6 → aixtools-0.1.7}/PKG-INFO +1 -1
  2. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/_version.py +3 -3
  3. aixtools-0.1.7/aixtools/vault/vault.py +100 -0
  4. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools.egg-info/PKG-INFO +1 -1
  5. aixtools-0.1.7/tests/unit/vault/test_vault.py +171 -0
  6. aixtools-0.1.6/aixtools/vault/vault.py +0 -73
  7. aixtools-0.1.6/tests/unit/vault/test_vault.py +0 -114
  8. {aixtools-0.1.6 → aixtools-0.1.7}/.env_template +0 -0
  9. {aixtools-0.1.6 → aixtools-0.1.7}/.github/workflows/build_and_publish_docker.yml +0 -0
  10. {aixtools-0.1.6 → aixtools-0.1.7}/.github/workflows/lint-and-test.yml +0 -0
  11. {aixtools-0.1.6 → aixtools-0.1.7}/.github/workflows/release.yml +0 -0
  12. {aixtools-0.1.6 → aixtools-0.1.7}/.gitignore +0 -0
  13. {aixtools-0.1.6 → aixtools-0.1.7}/.python-version +0 -0
  14. {aixtools-0.1.6 → aixtools-0.1.7}/.roo/rules/rules-mcp.md +0 -0
  15. {aixtools-0.1.6 → aixtools-0.1.7}/.roo/rules/rules.md +0 -0
  16. {aixtools-0.1.6 → aixtools-0.1.7}/.vscode/settings.json +0 -0
  17. {aixtools-0.1.6 → aixtools-0.1.7}/README.md +0 -0
  18. {aixtools-0.1.6 → aixtools-0.1.7}/README.ori.md +0 -0
  19. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/config.toml +0 -0
  20. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/bn.json +0 -0
  21. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/en-US.json +0 -0
  22. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/gu.json +0 -0
  23. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/he-IL.json +0 -0
  24. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/hi.json +0 -0
  25. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/ja.json +0 -0
  26. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/kn.json +0 -0
  27. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/ml.json +0 -0
  28. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/mr.json +0 -0
  29. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/nl.json +0 -0
  30. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/ta.json +0 -0
  31. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/te.json +0 -0
  32. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/.chainlit/translations/zh-CN.json +0 -0
  33. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/__init__.py +0 -0
  34. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/app.py +0 -0
  35. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/google_sdk/__init__.py +0 -0
  36. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/google_sdk/card.py +0 -0
  37. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py +0 -0
  38. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py +0 -0
  39. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/google_sdk/remote_agent_connection.py +0 -0
  40. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/google_sdk/utils.py +0 -0
  41. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/a2a/utils.py +0 -0
  42. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/agents/__init__.py +0 -0
  43. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/agents/agent.py +0 -0
  44. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/agents/agent_batch.py +0 -0
  45. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/agents/prompt.py +0 -0
  46. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/app.py +0 -0
  47. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/chainlit.md +0 -0
  48. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/context.py +0 -0
  49. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/db/__init__.py +0 -0
  50. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/db/database.py +0 -0
  51. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/db/vector_db.py +0 -0
  52. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/google/client.py +0 -0
  53. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/log_view/__init__.py +0 -0
  54. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/log_view/app.py +0 -0
  55. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/log_view/display.py +0 -0
  56. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/log_view/export.py +0 -0
  57. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/log_view/filters.py +0 -0
  58. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/log_view/log_utils.py +0 -0
  59. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/log_view/node_summary.py +0 -0
  60. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logfilters/__init__.py +0 -0
  61. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logfilters/context_filter.py +0 -0
  62. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logging/__init__.py +0 -0
  63. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logging/log_objects.py +0 -0
  64. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logging/logging_config.py +0 -0
  65. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logging/mcp_log_models.py +0 -0
  66. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logging/mcp_logger.py +0 -0
  67. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logging/model_patch_logging.py +0 -0
  68. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/logging/open_telemetry.py +0 -0
  69. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/mcp/__init__.py +0 -0
  70. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/mcp/client.py +0 -0
  71. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/mcp/example_client.py +0 -0
  72. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/mcp/example_server.py +0 -0
  73. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/mcp/fast_mcp_log.py +0 -0
  74. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/mcp/faulty_mcp.py +0 -0
  75. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/model_patch/model_patch.py +0 -0
  76. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/server/__init__.py +0 -0
  77. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/server/app_mounter.py +0 -0
  78. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/server/path.py +0 -0
  79. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/server/utils.py +0 -0
  80. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/testing/__init__.py +0 -0
  81. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/testing/aix_test_model.py +0 -0
  82. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/testing/mock_tool.py +0 -0
  83. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/testing/model_patch_cache.py +0 -0
  84. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/tools/doctor/__init__.py +0 -0
  85. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/tools/doctor/tool_doctor.py +0 -0
  86. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/tools/doctor/tool_recommendation.py +0 -0
  87. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/__init__.py +0 -0
  88. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/chainlit/cl_agent_show.py +0 -0
  89. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/chainlit/cl_utils.py +0 -0
  90. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/config.py +0 -0
  91. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/config_util.py +0 -0
  92. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/enum_with_description.py +0 -0
  93. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/files.py +0 -0
  94. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/persisted_dict.py +0 -0
  95. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/utils/utils.py +0 -0
  96. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools/vault/__init__.py +0 -0
  97. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools.egg-info/SOURCES.txt +0 -0
  98. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools.egg-info/dependency_links.txt +0 -0
  99. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools.egg-info/entry_points.txt +0 -0
  100. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools.egg-info/requires.txt +0 -0
  101. {aixtools-0.1.6 → aixtools-0.1.7}/aixtools.egg-info/top_level.txt +0 -0
  102. {aixtools-0.1.6 → aixtools-0.1.7}/docker/mcp-base/Dockerfile +0 -0
  103. {aixtools-0.1.6 → aixtools-0.1.7}/notebooks/example_faulty_mcp_server.ipynb +0 -0
  104. {aixtools-0.1.6 → aixtools-0.1.7}/notebooks/example_mcp_server_stdio.ipynb +0 -0
  105. {aixtools-0.1.6 → aixtools-0.1.7}/notebooks/example_raw_mcp_client.ipynb +0 -0
  106. {aixtools-0.1.6 → aixtools-0.1.7}/notebooks/example_tool_doctor.ipynb +0 -0
  107. {aixtools-0.1.6 → aixtools-0.1.7}/pyproject.toml +0 -0
  108. {aixtools-0.1.6 → aixtools-0.1.7}/scripts/config.sh +0 -0
  109. {aixtools-0.1.6 → aixtools-0.1.7}/scripts/lint.sh +0 -0
  110. {aixtools-0.1.6 → aixtools-0.1.7}/scripts/log_view.sh +0 -0
  111. {aixtools-0.1.6 → aixtools-0.1.7}/scripts/run_example_mcp_server.sh +0 -0
  112. {aixtools-0.1.6 → aixtools-0.1.7}/scripts/run_faulty_mcp_server.sh +0 -0
  113. {aixtools-0.1.6 → aixtools-0.1.7}/scripts/run_server.sh +0 -0
  114. {aixtools-0.1.6 → aixtools-0.1.7}/scripts/test.sh +0 -0
  115. {aixtools-0.1.6 → aixtools-0.1.7}/setup.cfg +0 -0
  116. {aixtools-0.1.6 → aixtools-0.1.7}/tests/__init__.py +0 -0
  117. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/__init__.py +0 -0
  118. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/__init__.py +0 -0
  119. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/google_sdk/__init__.py +0 -0
  120. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/google_sdk/pydantic_ai_adapter/__init__.py +0 -0
  121. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_agent_executor.py +0 -0
  122. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_storage.py +0 -0
  123. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/google_sdk/test_card.py +0 -0
  124. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/google_sdk/test_remote_agent_connection.py +0 -0
  125. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/a2a/google_sdk/test_utils.py +0 -0
  126. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/agents/__init__.py +0 -0
  127. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/agents/test_prompt.py +0 -0
  128. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/google/__init__.py +0 -0
  129. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/google/test_client.py +0 -0
  130. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/mcp/__init__.py +0 -0
  131. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/mcp/test_client.py +0 -0
  132. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/server/__init__.py +0 -0
  133. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/server/test_path.py +0 -0
  134. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/server/test_utils.py +0 -0
  135. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/utils/__init__.py +0 -0
  136. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/utils/test_files.py +0 -0
  137. {aixtools-0.1.6 → aixtools-0.1.7}/tests/unit/vault/__init__.py +0 -0
  138. {aixtools-0.1.6 → aixtools-0.1.7}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aixtools
3
- Version: 0.1.6
3
+ Version: 0.1.7
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.7'
32
+ __version_tuple__ = version_tuple = (0, 1, 7)
33
33
 
34
- __commit_id__ = commit_id = 'g6abaa9c74'
34
+ __commit_id__ = commit_id = 'g78a5dc717'
@@ -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.7
4
4
  Summary: Tools for AI exploration and debugging
5
5
  Requires-Python: >=3.11.2
6
6
  Description-Content-Type: text/markdown
@@ -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,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
File without changes