From dc4f8e6565d18720a1a7a5a8b5324801fe7fb9f5 Mon Sep 17 00:00:00 2001 From: "John Xing (Amigo)" <150419128+johnxing-amigo@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:05:20 -0400 Subject: [PATCH 1/2] feat!: v1.0 breaking changes - ForbiddenError, close(), plural resources, exports BREAKING CHANGES: - Rename PermissionError to ForbiddenError (avoids shadowing builtins) - Rename sync client aclose() to close() (async keeps aclose()) - Normalize resource properties to plural: client.organization -> client.organizations client.service -> client.services client.conversation -> client.conversations - Add convenience aliases to all resources: UserResource: list(), create(), delete(), update(), get_model() ServiceResource: list() ConversationResource: list(), create(), interact(), finish(), messages() - Export AmigoConfig and all error classes from __init__.py - Add mypy to dev dependencies - Remove scripts/ from wheel build - Add license (MIT) and project URLs to pyproject.toml Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 10 +++- src/amigo_sdk/__init__.py | 26 +++++++++ src/amigo_sdk/errors.py | 4 +- src/amigo_sdk/http_client.py | 4 +- src/amigo_sdk/resources/conversation.py | 52 ++++++++++++++++++ src/amigo_sdk/resources/service.py | 12 +++++ src/amigo_sdk/resources/user.py | 52 ++++++++++++++++++ src/amigo_sdk/sdk_client.py | 46 ++++++++-------- .../test_conversation_integration.py | 54 +++++++++---------- .../test_organization_integration.py | 8 +-- tests/test_sdk_client.py | 16 +++--- 11 files changed, 219 insertions(+), 65 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ba7059e..1a9c2e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ description = "Amigo AI Python SDK" readme = "README.md" requires-python = ">= 3.11" authors=[{name="Amigo AI"}] +license = "MIT" classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", @@ -24,6 +25,13 @@ dependencies = [ "email-validator>=2.0,<3.0", ] +[project.urls] +Homepage = "https://github.com/amigo-ai/amigo-python-sdk" +Documentation = "https://docs.amigo.ai" +Repository = "https://github.com/amigo-ai/amigo-python-sdk" +Issues = "https://github.com/amigo-ai/amigo-python-sdk/issues" +Changelog = "https://github.com/amigo-ai/amigo-python-sdk/blob/main/CHANGELOG.md" + [project.scripts] gen-models = "scripts.gen_models:main" check = "scripts.check:main" @@ -36,6 +44,7 @@ dev = [ "pytest-httpx>=0.30,<1.0", "python-dotenv>=1.0,<2.0", "ruff>=0.1,<1.0", + "mypy>=1.0", "datamodel-code-generator[http]>=0.21,<1.0", ] docs = [ @@ -50,7 +59,6 @@ path = "src/amigo_sdk/__init__.py" [tool.hatch.build.targets.wheel] packages = [ "src/amigo_sdk", - "scripts", ] [tool.hatch.build.targets.sdist] diff --git a/src/amigo_sdk/__init__.py b/src/amigo_sdk/__init__.py index 8bad1ea..8aa8090 100644 --- a/src/amigo_sdk/__init__.py +++ b/src/amigo_sdk/__init__.py @@ -1,4 +1,18 @@ __version__ = "0.137.1" +from .config import AmigoConfig +from .errors import ( + AmigoError, + AuthenticationError, + BadRequestError, + ConflictError, + ForbiddenError, + NotFoundError, + RateLimitError, + SDKInternalError, + ServerError, + ServiceUnavailableError, + ValidationError, +) from .rate_limits import RateLimitInfo, parse_rate_limit_headers from .sdk_client import AmigoClient, AsyncAmigoClient from .webhooks import ( @@ -12,9 +26,21 @@ __all__ = [ "__version__", "AmigoClient", + "AmigoConfig", + "AmigoError", "AsyncAmigoClient", + "AuthenticationError", + "BadRequestError", + "ConflictError", "ConversationPostProcessingCompleteEvent", + "ForbiddenError", + "NotFoundError", + "RateLimitError", "RateLimitInfo", + "SDKInternalError", + "ServerError", + "ServiceUnavailableError", + "ValidationError", "WebhookEvent", "WebhookVerificationError", "parse_rate_limit_headers", diff --git a/src/amigo_sdk/errors.py b/src/amigo_sdk/errors.py index e6185ef..b4af5fb 100644 --- a/src/amigo_sdk/errors.py +++ b/src/amigo_sdk/errors.py @@ -75,7 +75,7 @@ class AuthenticationError(AmigoError): # 401 pass -class PermissionError(AmigoError): # 403 +class ForbiddenError(AmigoError): # 403 pass @@ -118,7 +118,7 @@ def get_error_class_for_status_code(status_code: int) -> type[AmigoError]: error_map = { 400: BadRequestError, 401: AuthenticationError, - 403: PermissionError, + 403: ForbiddenError, 404: NotFoundError, 409: ConflictError, 422: ValidationError, diff --git a/src/amigo_sdk/http_client.py b/src/amigo_sdk/http_client.py index 36f3d79..71b0c6f 100644 --- a/src/amigo_sdk/http_client.py +++ b/src/amigo_sdk/http_client.py @@ -397,7 +397,7 @@ def _yield_from_response(resp: httpx.Response) -> Iterator[str]: for ln in _yield_from_response(resp): yield ln - def aclose(self) -> None: + def close(self) -> None: """Close the underlying httpx client.""" self._client.close() @@ -405,4 +405,4 @@ def __enter__(self): return self def __exit__(self, *_): - self.aclose() + self.close() diff --git a/src/amigo_sdk/resources/conversation.py b/src/amigo_sdk/resources/conversation.py index 933f737..7f8e585 100644 --- a/src/amigo_sdk/resources/conversation.py +++ b/src/amigo_sdk/resources/conversation.py @@ -269,6 +269,32 @@ async def generate_conversation_starters( response.text ) + # --- Convenience aliases --- + + async def list( + self, params: GetConversationsParametersQuery + ) -> ConversationGetConversationsResponse: + """Alias for get_conversations.""" + return await self.get_conversations(params) + + def create(self, *args, **kwargs): + """Alias for create_conversation.""" + return self.create_conversation(*args, **kwargs) + + def interact(self, *args, **kwargs): + """Alias for interact_with_conversation.""" + return self.interact_with_conversation(*args, **kwargs) + + async def finish(self, conversation_id: str) -> None: + """Alias for finish_conversation.""" + return await self.finish_conversation(conversation_id) + + async def messages( + self, conversation_id: str, params: GetConversationMessagesParametersQuery + ) -> ConversationGetConversationMessagesResponse: + """Alias for get_conversation_messages.""" + return await self.get_conversation_messages(conversation_id, params) + class ConversationResource: """Conversation resource for synchronous operations.""" @@ -460,3 +486,29 @@ def generate_conversation_starters( return ConversationGenerateConversationStarterResponse.model_validate_json( response.text ) + + # --- Convenience aliases --- + + def list( + self, params: GetConversationsParametersQuery + ) -> ConversationGetConversationsResponse: + """Alias for get_conversations.""" + return self.get_conversations(params) + + def create(self, *args, **kwargs): + """Alias for create_conversation.""" + return self.create_conversation(*args, **kwargs) + + def interact(self, *args, **kwargs): + """Alias for interact_with_conversation.""" + return self.interact_with_conversation(*args, **kwargs) + + def finish(self, conversation_id: str) -> None: + """Alias for finish_conversation.""" + return self.finish_conversation(conversation_id) + + def messages( + self, conversation_id: str, params: GetConversationMessagesParametersQuery + ) -> ConversationGetConversationMessagesResponse: + """Alias for get_conversation_messages.""" + return self.get_conversation_messages(conversation_id, params) diff --git a/src/amigo_sdk/resources/service.py b/src/amigo_sdk/resources/service.py index 9ec58b4..8abb09f 100644 --- a/src/amigo_sdk/resources/service.py +++ b/src/amigo_sdk/resources/service.py @@ -25,6 +25,12 @@ async def get_services( ) return ServiceGetServicesResponse.model_validate_json(response.text) + async def list( + self, params: GetServicesParametersQuery | None = None + ) -> ServiceGetServicesResponse: + """Alias for get_services.""" + return await self.get_services(params) + class ServiceResource: """Service resource for synchronous operations.""" @@ -45,3 +51,9 @@ def get_services( else None, ) return ServiceGetServicesResponse.model_validate_json(response.text) + + def list( + self, params: GetServicesParametersQuery | None = None + ) -> ServiceGetServicesResponse: + """Alias for get_services.""" + return self.get_services(params) diff --git a/src/amigo_sdk/resources/user.py b/src/amigo_sdk/resources/user.py index 0475f38..6f0ce5f 100644 --- a/src/amigo_sdk/resources/user.py +++ b/src/amigo_sdk/resources/user.py @@ -63,6 +63,32 @@ async def get_user_model(self, user_id: str) -> UserGetUserModelResponse: ) return UserGetUserModelResponse.model_validate_json(response.text) + # --- Convenience aliases --- + + async def list( + self, params: GetUsersParametersQuery | None = None + ) -> UserGetUsersResponse: + """Alias for get_users.""" + return await self.get_users(params) + + async def create( + self, body: UserCreateInvitedUserRequest + ) -> UserCreateInvitedUserResponse: + """Alias for create_user.""" + return await self.create_user(body) + + async def delete(self, user_id: str) -> None: + """Alias for delete_user.""" + return await self.delete_user(user_id) + + async def update(self, user_id: str, body: UserUpdateUserInfoRequest) -> None: + """Alias for update_user.""" + return await self.update_user(user_id, body) + + async def get_model(self, user_id: str) -> UserGetUserModelResponse: + """Alias for get_user_model.""" + return await self.get_user_model(user_id) + class UserResource: """User resource (synchronous).""" @@ -114,3 +140,29 @@ def get_user_model(self, user_id: str) -> UserGetUserModelResponse: f"/v1/{self._organization_id}/user/{user_id}/user_model", ) return UserGetUserModelResponse.model_validate_json(response.text) + + # --- Convenience aliases --- + + def list( + self, params: GetUsersParametersQuery | None = None + ) -> UserGetUsersResponse: + """Alias for get_users.""" + return self.get_users(params) + + def create( + self, body: UserCreateInvitedUserRequest + ) -> UserCreateInvitedUserResponse: + """Alias for create_user.""" + return self.create_user(body) + + def delete(self, user_id: str) -> None: + """Alias for delete_user.""" + return self.delete_user(user_id) + + def update(self, user_id: str, body: UserUpdateUserInfoRequest) -> None: + """Alias for update_user.""" + return self.update_user(user_id, body) + + def get_model(self, user_id: str) -> UserGetUserModelResponse: + """Alias for get_user_model.""" + return self.get_user_model(user_id) diff --git a/src/amigo_sdk/sdk_client.py b/src/amigo_sdk/sdk_client.py index 55398d4..a7eedd8 100644 --- a/src/amigo_sdk/sdk_client.py +++ b/src/amigo_sdk/sdk_client.py @@ -67,11 +67,11 @@ def __init__( # Initialize HTTP client and resources self._http = AmigoAsyncHttpClient(self._cfg, **httpx_kwargs) - self._organization = AsyncOrganizationResource( + self._organizations = AsyncOrganizationResource( self._http, self._cfg.organization_id ) - self._service = AsyncServiceResource(self._http, self._cfg.organization_id) - self._conversation = AsyncConversationResource( + self._services = AsyncServiceResource(self._http, self._cfg.organization_id) + self._conversations = AsyncConversationResource( self._http, self._cfg.organization_id ) self._users = AsyncUserResource(self._http, self._cfg.organization_id) @@ -82,19 +82,19 @@ def config(self) -> AmigoConfig: return self._cfg @property - def organization(self) -> AsyncOrganizationResource: + def organizations(self) -> AsyncOrganizationResource: """Access organization resource.""" - return self._organization + return self._organizations @property - def service(self) -> AsyncServiceResource: + def services(self) -> AsyncServiceResource: """Access service resource.""" - return self._service + return self._services @property - def conversation(self) -> AsyncConversationResource: + def conversations(self) -> AsyncConversationResource: """Access conversation resource.""" - return self._conversation + return self._conversations @property def users(self) -> AsyncUserResource: @@ -163,9 +163,13 @@ def __init__( ) from e self._http = AmigoHttpClient(self._cfg, **httpx_kwargs) - self._organization = OrganizationResource(self._http, self._cfg.organization_id) - self._service = ServiceResource(self._http, self._cfg.organization_id) - self._conversation = ConversationResource(self._http, self._cfg.organization_id) + self._organizations = OrganizationResource( + self._http, self._cfg.organization_id + ) + self._services = ServiceResource(self._http, self._cfg.organization_id) + self._conversations = ConversationResource( + self._http, self._cfg.organization_id + ) self._users = UserResource(self._http, self._cfg.organization_id) @property @@ -174,31 +178,31 @@ def config(self) -> AmigoConfig: return self._cfg @property - def organization(self) -> OrganizationResource: + def organizations(self) -> OrganizationResource: """Access organization resource.""" - return self._organization + return self._organizations @property - def service(self) -> ServiceResource: + def services(self) -> ServiceResource: """Access service resource.""" - return self._service + return self._services @property - def conversation(self) -> ConversationResource: + def conversations(self) -> ConversationResource: """Access conversation resource.""" - return self._conversation + return self._conversations @property def users(self) -> UserResource: """Access user resource.""" return self._users - def aclose(self) -> None: + def close(self) -> None: """Close the HTTP client.""" - self._http.aclose() + self._http.close() def __enter__(self): return self def __exit__(self, *_): - self.aclose() + self.close() diff --git a/tests/integration/test_conversation_integration.py b/tests/integration/test_conversation_integration.py index 0de23fd..6315500 100644 --- a/tests/integration/test_conversation_integration.py +++ b/tests/integration/test_conversation_integration.py @@ -35,7 +35,7 @@ def _build_test_wav_bytes() -> bytes: async def _latest_conversation_message_time_async( client: AsyncAmigoClient, conversation_id: str ) -> datetime: - page = await client.conversation.get_conversation_messages( + page = await client.conversations.get_conversation_messages( conversation_id, GetConversationMessagesParametersQuery(limit=1, sort_by=["-created_at"]), ) @@ -50,7 +50,7 @@ async def _latest_conversation_message_time_async( def _latest_conversation_message_time_sync( client: AmigoClient, conversation_id: str ) -> datetime: - page = client.conversation.get_conversation_messages( + page = client.conversations.get_conversation_messages( conversation_id, GetConversationMessagesParametersQuery(limit=1, sort_by=["-created_at"]), ) @@ -69,7 +69,7 @@ async def pre_suite_cleanup() -> AsyncGenerator[None]: try: from amigo_sdk.generated.model import GetServicesParametersQuery - services = await client.service.get_services( + services = await client.services.get_services( GetServicesParametersQuery(id=[SERVICE_ID]) ) service_ids = [ @@ -83,7 +83,7 @@ async def pre_suite_cleanup() -> AsyncGenerator[None]: # Finish any ongoing conversations for this service (best-effort) try: - convs = await client.conversation.get_conversations( + convs = await client.conversations.get_conversations( GetConversationsParametersQuery( service_id=[SERVICE_ID], is_finished=False, @@ -93,7 +93,7 @@ async def pre_suite_cleanup() -> AsyncGenerator[None]: ) for c in getattr(convs, "conversations", []) or []: try: - await client.conversation.finish_conversation(c.id) + await client.conversations.finish_conversation(c.id) except Exception: pass except Exception: @@ -111,7 +111,7 @@ class TestConversationIntegration: async def test_create_conversation_streams_and_returns_ids(self): async with AsyncAmigoClient() as client: - events = await client.conversation.create_conversation( + events = await client.conversations.create_conversation( body=ConversationCreateConversationRequest( service_id=SERVICE_ID, service_version_set_name="release", @@ -144,7 +144,7 @@ async def test_recommend_responses_returns_suggestions(self): assert type(self).interaction_id is not None async with AsyncAmigoClient() as client: - recs = await client.conversation.recommend_responses_for_interaction( + recs = await client.conversations.recommend_responses_for_interaction( type(self).conversation_id, type(self).interaction_id ) @@ -155,7 +155,7 @@ async def test_get_conversations_filter_by_id(self): assert type(self).conversation_id is not None async with AsyncAmigoClient() as client: - resp = await client.conversation.get_conversations( + resp = await client.conversations.get_conversations( GetConversationsParametersQuery(id=[type(self).conversation_id]) ) @@ -167,7 +167,7 @@ async def test_interact_with_conversation_text_streams(self): assert type(self).conversation_id is not None async with AsyncAmigoClient() as client: - events = await client.conversation.interact_with_conversation( + events = await client.conversations.interact_with_conversation( type(self).conversation_id, params=InteractWithConversationParametersQuery( request_format="text", response_format="text" @@ -214,7 +214,7 @@ async def test_interact_with_conversation_external_event_streams(self): assert len(external_event_message_timestamp) == len( external_event_message_content ) - events = await client.conversation.interact_with_conversation( + events = await client.conversations.interact_with_conversation( type(self).conversation_id, params=InteractWithConversationParametersQuery( request_format="text", response_format="text" @@ -250,7 +250,7 @@ async def test_interact_with_conversation_voice_streams(self): assert type(self).conversation_id is not None async with AsyncAmigoClient() as client: - events = await client.conversation.interact_with_conversation( + events = await client.conversations.interact_with_conversation( type(self).conversation_id, params=InteractWithConversationParametersQuery( request_format="voice", @@ -291,7 +291,7 @@ async def test_get_conversation_messages_pagination(self): assert type(self).conversation_id is not None async with AsyncAmigoClient() as client: - page1 = await client.conversation.get_conversation_messages( + page1 = await client.conversations.get_conversation_messages( type(self).conversation_id, GetConversationMessagesParametersQuery( limit=1, sort_by=["+created_at"] @@ -304,7 +304,7 @@ async def test_get_conversation_messages_pagination(self): if page1.has_more: assert page1.continuation_token is not None - page2 = await client.conversation.get_conversation_messages( + page2 = await client.conversations.get_conversation_messages( type(self).conversation_id, GetConversationMessagesParametersQuery( limit=1, @@ -321,7 +321,7 @@ async def test_get_interaction_insights_returns_data(self): assert type(self).interaction_id is not None async with AsyncAmigoClient() as client: - insights = await client.conversation.get_interaction_insights( + insights = await client.conversations.get_interaction_insights( type(self).conversation_id, type(self).interaction_id ) assert insights is not None @@ -332,7 +332,7 @@ async def test_finish_conversation_returns_acceptable_outcome(self): async with AsyncAmigoClient() as client: try: - await client.conversation.finish_conversation( + await client.conversations.finish_conversation( type(self).conversation_id ) except Exception as e: @@ -358,7 +358,7 @@ def test_create_conversation_streams_and_returns_ids(self): # Best-effort: finish any lingering conversations if attempt > 0: try: - convs = client.conversation.get_conversations( + convs = client.conversations.get_conversations( GetConversationsParametersQuery( service_id=[SERVICE_ID], is_finished=False, @@ -367,14 +367,14 @@ def test_create_conversation_streams_and_returns_ids(self): ) for c in getattr(convs, "conversations", []) or []: try: - client.conversation.finish_conversation(c.id) + client.conversations.finish_conversation(c.id) except Exception: pass time.sleep(1) except Exception: pass - events = client.conversation.create_conversation( + events = client.conversations.create_conversation( body=ConversationCreateConversationRequest( service_id=SERVICE_ID, service_version_set_name="release", @@ -409,7 +409,7 @@ def test_recommend_responses_returns_suggestions(self): assert type(self).interaction_id is not None with AmigoClient() as client: - recs = client.conversation.recommend_responses_for_interaction( + recs = client.conversations.recommend_responses_for_interaction( type(self).conversation_id, type(self).interaction_id ) @@ -420,7 +420,7 @@ def test_get_conversations_filter_by_id(self): assert type(self).conversation_id is not None with AmigoClient() as client: - resp = client.conversation.get_conversations( + resp = client.conversations.get_conversations( GetConversationsParametersQuery(id=[type(self).conversation_id]) ) @@ -432,7 +432,7 @@ def test_interact_with_conversation_text_streams(self): assert type(self).conversation_id is not None with AmigoClient() as client: - events = client.conversation.interact_with_conversation( + events = client.conversations.interact_with_conversation( type(self).conversation_id, params=InteractWithConversationParametersQuery( request_format="text", response_format="text" @@ -480,7 +480,7 @@ def test_interact_with_conversation_external_event_streams(self): assert len(external_event_message_timestamp) == len( external_event_message_content ) - events = client.conversation.interact_with_conversation( + events = client.conversations.interact_with_conversation( type(self).conversation_id, params=InteractWithConversationParametersQuery( request_format="text", response_format="text" @@ -516,7 +516,7 @@ def test_interact_with_conversation_voice_streams(self): assert type(self).conversation_id is not None with AmigoClient() as client: - events = client.conversation.interact_with_conversation( + events = client.conversations.interact_with_conversation( type(self).conversation_id, params=InteractWithConversationParametersQuery( request_format="voice", @@ -557,7 +557,7 @@ def test_get_conversation_messages_pagination(self): assert type(self).conversation_id is not None with AmigoClient() as client: - page1 = client.conversation.get_conversation_messages( + page1 = client.conversations.get_conversation_messages( type(self).conversation_id, GetConversationMessagesParametersQuery( limit=1, sort_by=["+created_at"] @@ -570,7 +570,7 @@ def test_get_conversation_messages_pagination(self): if page1.has_more: assert page1.continuation_token is not None - page2 = client.conversation.get_conversation_messages( + page2 = client.conversations.get_conversation_messages( type(self).conversation_id, GetConversationMessagesParametersQuery( limit=1, @@ -587,7 +587,7 @@ def test_get_interaction_insights_returns_data(self): assert type(self).interaction_id is not None with AmigoClient() as client: - insights = client.conversation.get_interaction_insights( + insights = client.conversations.get_interaction_insights( type(self).conversation_id, type(self).interaction_id ) assert insights is not None @@ -598,6 +598,6 @@ def test_finish_conversation_returns_acceptable_outcome(self): with AmigoClient() as client: try: - client.conversation.finish_conversation(type(self).conversation_id) + client.conversations.finish_conversation(type(self).conversation_id) except Exception as e: assert isinstance(e, (ConflictError, NotFoundError)) diff --git a/tests/integration/test_organization_integration.py b/tests/integration/test_organization_integration.py index 2c36e72..196e429 100644 --- a/tests/integration/test_organization_integration.py +++ b/tests/integration/test_organization_integration.py @@ -65,7 +65,7 @@ async def test_get_organization(self): # Create client using environment variables async with AsyncAmigoClient() as client: # Get organization details - organization = await client.organization.get() + organization = await client.organizations.get() # Verify we got a valid response assert organization is not None @@ -99,7 +99,7 @@ async def test_invalid_credentials_raises_authentication_error(self): async with AsyncAmigoClient( api_key="invalid_key", ) as client: - await client.organization.get() + await client.organizations.get() async def test_client_config_property(self, required_env_vars): """Test that the client config property works correctly.""" @@ -139,7 +139,7 @@ def test_get_services(self): def test_get_organization(self): with AmigoClient() as client: - organization = client.organization.get() + organization = client.organizations.get() assert organization is not None assert isinstance(organization, OrganizationGetOrganizationResponse) @@ -156,7 +156,7 @@ def test_invalid_credentials_raises_authentication_error(self): with pytest.raises(AuthenticationError): with AmigoClient(api_key="invalid_key") as client: - client.organization.get() + client.organizations.get() def test_client_config_property(self, required_env_vars): with AmigoClient() as client: diff --git a/tests/test_sdk_client.py b/tests/test_sdk_client.py index 4f030f8..7df5dbb 100644 --- a/tests/test_sdk_client.py +++ b/tests/test_sdk_client.py @@ -66,10 +66,10 @@ def test_resources_are_accessible(self, mock_config): client = AsyncAmigoClient(config=mock_config) # Should have resources (basic smoke test) - assert client.organization is not None - assert client.service is not None - assert hasattr(client.organization, "_http") - assert hasattr(client.service, "_http") + assert client.organizations is not None + assert client.services is not None + assert hasattr(client.organizations, "_http") + assert hasattr(client.services, "_http") @pytest.mark.asyncio async def test_async_context_manager(self, mock_config): @@ -118,10 +118,10 @@ def test_missing_config_raises_error_sync(self): def test_resources_are_accessible_sync(self, mock_config): client = AmigoClient(config=mock_config) - assert client.organization is not None - assert client.service is not None - assert hasattr(client.organization, "_http") - assert hasattr(client.service, "_http") + assert client.organizations is not None + assert client.services is not None + assert hasattr(client.organizations, "_http") + assert hasattr(client.services, "_http") def test_context_manager_sync(self, mock_config): with AmigoClient(config=mock_config) as client: From 49df56280d0653102a31b27471fabc9b5d551950 Mon Sep 17 00:00:00 2001 From: "John Xing (Amigo)" <150419128+johnxing-amigo@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:24:12 -0400 Subject: [PATCH 2/2] feat: add agent, context graph resources and service CRUD endpoints New resources for the deploy-a-service workflow: - AsyncAgentResource/AgentResource: create_agent, get_agents, delete_agent, create_agent_version, get_agent_versions (with list/create/delete aliases) - AsyncContextGraphResource/ContextGraphResource: create_context_graph, get_context_graphs, create_context_graph_version, delete_context_graph, get_context_graph_versions (with list/create/delete aliases) Extended AsyncServiceResource/ServiceResource with: - create_service, update_service, upsert_version_set, delete_version_set (with list/create aliases) All 189 tests pass, 85.04% coverage. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/amigo_sdk/resources/agent.py | 190 ++++++++++++++++++++ src/amigo_sdk/resources/context_graph.py | 216 +++++++++++++++++++++++ src/amigo_sdk/resources/service.py | 106 ++++++++++- src/amigo_sdk/sdk_client.py | 33 ++++ tests/resources/test_agent.py | 92 ++++++++++ tests/resources/test_context_graph.py | 101 +++++++++++ tests/resources/test_service.py | 94 ++++++++++ 7 files changed, 829 insertions(+), 3 deletions(-) create mode 100644 src/amigo_sdk/resources/agent.py create mode 100644 src/amigo_sdk/resources/context_graph.py create mode 100644 tests/resources/test_agent.py create mode 100644 tests/resources/test_context_graph.py diff --git a/src/amigo_sdk/resources/agent.py b/src/amigo_sdk/resources/agent.py new file mode 100644 index 0000000..4ff0bb5 --- /dev/null +++ b/src/amigo_sdk/resources/agent.py @@ -0,0 +1,190 @@ +from amigo_sdk.generated.model import ( + CreateAgentVersionParametersQuery, + GetAgentsParametersQuery, + GetAgentVersionsParametersQuery, + OrganizationCreateAgentRequest, + OrganizationCreateAgentResponse, + OrganizationCreateAgentVersionRequest, + OrganizationCreateAgentVersionResponse, + OrganizationGetAgentsResponse, + OrganizationGetAgentVersionsResponse, +) +from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient + + +class AsyncAgentResource: + """Agent resource for Amigo API operations.""" + + def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None: + self._http = http_client + self._organization_id = organization_id + + async def create_agent( + self, body: OrganizationCreateAgentRequest + ) -> OrganizationCreateAgentResponse: + """Create a new agent in the organization.""" + response = await self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/agent", + json=body.model_dump(mode="json", exclude_none=True), + ) + return OrganizationCreateAgentResponse.model_validate_json(response.text) + + async def get_agents( + self, params: GetAgentsParametersQuery | None = None + ) -> OrganizationGetAgentsResponse: + """Get a list of agents in the organization.""" + response = await self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/agent", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationGetAgentsResponse.model_validate_json(response.text) + + async def delete_agent(self, agent_id: str) -> None: + """Delete an agent by ID. Returns None on success (e.g., 204).""" + await self._http.request( + "DELETE", + f"/v1/{self._organization_id}/organization/agent/{agent_id}/", + ) + + async def create_agent_version( + self, + agent_id: str, + body: OrganizationCreateAgentVersionRequest, + version: int | None = None, + ) -> OrganizationCreateAgentVersionResponse: + """Create a new version for an agent.""" + params = None + if version is not None: + query = CreateAgentVersionParametersQuery(version=version) + params = query.model_dump(mode="json", exclude_none=True) + response = await self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/agent/{agent_id}/", + json=body.model_dump(mode="json", exclude_none=True), + params=params, + ) + return OrganizationCreateAgentVersionResponse.model_validate_json(response.text) + + async def get_agent_versions( + self, agent_id: str, params: GetAgentVersionsParametersQuery | None = None + ) -> OrganizationGetAgentVersionsResponse: + """Get versions for a specific agent.""" + response = await self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/agent/{agent_id}/version", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationGetAgentVersionsResponse.model_validate_json(response.text) + + # --- Convenience aliases --- + + async def list( + self, params: GetAgentsParametersQuery | None = None + ) -> OrganizationGetAgentsResponse: + """Alias for get_agents.""" + return await self.get_agents(params) + + async def create( + self, body: OrganizationCreateAgentRequest + ) -> OrganizationCreateAgentResponse: + """Alias for create_agent.""" + return await self.create_agent(body) + + async def delete(self, agent_id: str) -> None: + """Alias for delete_agent.""" + return await self.delete_agent(agent_id) + + +class AgentResource: + """Agent resource (synchronous).""" + + def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None: + self._http = http_client + self._organization_id = organization_id + + def create_agent( + self, body: OrganizationCreateAgentRequest + ) -> OrganizationCreateAgentResponse: + """Create a new agent in the organization.""" + response = self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/agent", + json=body.model_dump(mode="json", exclude_none=True), + ) + return OrganizationCreateAgentResponse.model_validate_json(response.text) + + def get_agents( + self, params: GetAgentsParametersQuery | None = None + ) -> OrganizationGetAgentsResponse: + """Get a list of agents in the organization.""" + response = self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/agent", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationGetAgentsResponse.model_validate_json(response.text) + + def delete_agent(self, agent_id: str) -> None: + """Delete an agent by ID.""" + self._http.request( + "DELETE", + f"/v1/{self._organization_id}/organization/agent/{agent_id}/", + ) + + def create_agent_version( + self, + agent_id: str, + body: OrganizationCreateAgentVersionRequest, + version: int | None = None, + ) -> OrganizationCreateAgentVersionResponse: + """Create a new version for an agent.""" + params = None + if version is not None: + query = CreateAgentVersionParametersQuery(version=version) + params = query.model_dump(mode="json", exclude_none=True) + response = self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/agent/{agent_id}/", + json=body.model_dump(mode="json", exclude_none=True), + params=params, + ) + return OrganizationCreateAgentVersionResponse.model_validate_json(response.text) + + def get_agent_versions( + self, agent_id: str, params: GetAgentVersionsParametersQuery | None = None + ) -> OrganizationGetAgentVersionsResponse: + """Get versions for a specific agent.""" + response = self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/agent/{agent_id}/version", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationGetAgentVersionsResponse.model_validate_json(response.text) + + # --- Convenience aliases --- + + def list( + self, params: GetAgentsParametersQuery | None = None + ) -> OrganizationGetAgentsResponse: + """Alias for get_agents.""" + return self.get_agents(params) + + def create( + self, body: OrganizationCreateAgentRequest + ) -> OrganizationCreateAgentResponse: + """Alias for create_agent.""" + return self.create_agent(body) + + def delete(self, agent_id: str) -> None: + """Alias for delete_agent.""" + return self.delete_agent(agent_id) diff --git a/src/amigo_sdk/resources/context_graph.py b/src/amigo_sdk/resources/context_graph.py new file mode 100644 index 0000000..2b55c1c --- /dev/null +++ b/src/amigo_sdk/resources/context_graph.py @@ -0,0 +1,216 @@ +from amigo_sdk.generated.model import ( + CreateServiceHierarchicalStateMachineVersionParametersQuery, + GetServiceHierarchicalStateMachinesParametersQuery, + GetServiceHierarchicalStateMachineVersionsParametersQuery, + OrganizationCreateServiceHierarchicalStateMachineRequest, + OrganizationCreateServiceHierarchicalStateMachineResponse, + OrganizationCreateServiceHierarchicalStateMachineVersionRequest, + OrganizationCreateServiceHierarchicalStateMachineVersionResponse, + OrganizationGetServiceHierarchicalStateMachinesResponse, + OrganizationGetServiceHierarchicalStateMachineVersionsResponse, +) +from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient + + +class AsyncContextGraphResource: + """Context graph (HSM) resource for Amigo API operations.""" + + def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None: + self._http = http_client + self._organization_id = organization_id + + async def create_context_graph( + self, body: OrganizationCreateServiceHierarchicalStateMachineRequest + ) -> OrganizationCreateServiceHierarchicalStateMachineResponse: + """Create a new context graph.""" + response = await self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine", + json=body.model_dump(mode="json", exclude_none=True), + ) + return OrganizationCreateServiceHierarchicalStateMachineResponse.model_validate_json( + response.text + ) + + async def get_context_graphs( + self, + params: GetServiceHierarchicalStateMachinesParametersQuery | None = None, + ) -> OrganizationGetServiceHierarchicalStateMachinesResponse: + """List context graphs for the organization.""" + response = await self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return ( + OrganizationGetServiceHierarchicalStateMachinesResponse.model_validate_json( + response.text + ) + ) + + async def create_context_graph_version( + self, + context_graph_id: str, + body: OrganizationCreateServiceHierarchicalStateMachineVersionRequest, + params: CreateServiceHierarchicalStateMachineVersionParametersQuery + | None = None, + ) -> OrganizationCreateServiceHierarchicalStateMachineVersionResponse: + """Create a new version of a context graph.""" + response = await self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine/{context_graph_id}/", + json=body.model_dump(mode="json", exclude_none=True), + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationCreateServiceHierarchicalStateMachineVersionResponse.model_validate_json( + response.text + ) + + async def delete_context_graph(self, context_graph_id: str) -> None: + """Delete a context graph.""" + await self._http.request( + "DELETE", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine/{context_graph_id}/", + ) + + async def get_context_graph_versions( + self, + context_graph_id: str, + params: GetServiceHierarchicalStateMachineVersionsParametersQuery | None = None, + ) -> OrganizationGetServiceHierarchicalStateMachineVersionsResponse: + """Get versions of a context graph.""" + response = await self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine/{context_graph_id}/version", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationGetServiceHierarchicalStateMachineVersionsResponse.model_validate_json( + response.text + ) + + # --- Convenience aliases --- + + async def list( + self, + params: GetServiceHierarchicalStateMachinesParametersQuery | None = None, + ) -> OrganizationGetServiceHierarchicalStateMachinesResponse: + """Alias for get_context_graphs.""" + return await self.get_context_graphs(params) + + async def create( + self, body: OrganizationCreateServiceHierarchicalStateMachineRequest + ) -> OrganizationCreateServiceHierarchicalStateMachineResponse: + """Alias for create_context_graph.""" + return await self.create_context_graph(body) + + async def delete(self, context_graph_id: str) -> None: + """Alias for delete_context_graph.""" + return await self.delete_context_graph(context_graph_id) + + +class ContextGraphResource: + """Context graph (HSM) resource (synchronous).""" + + def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None: + self._http = http_client + self._organization_id = organization_id + + def create_context_graph( + self, body: OrganizationCreateServiceHierarchicalStateMachineRequest + ) -> OrganizationCreateServiceHierarchicalStateMachineResponse: + """Create a new context graph.""" + response = self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine", + json=body.model_dump(mode="json", exclude_none=True), + ) + return OrganizationCreateServiceHierarchicalStateMachineResponse.model_validate_json( + response.text + ) + + def get_context_graphs( + self, + params: GetServiceHierarchicalStateMachinesParametersQuery | None = None, + ) -> OrganizationGetServiceHierarchicalStateMachinesResponse: + """List context graphs for the organization.""" + response = self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return ( + OrganizationGetServiceHierarchicalStateMachinesResponse.model_validate_json( + response.text + ) + ) + + def create_context_graph_version( + self, + context_graph_id: str, + body: OrganizationCreateServiceHierarchicalStateMachineVersionRequest, + params: CreateServiceHierarchicalStateMachineVersionParametersQuery + | None = None, + ) -> OrganizationCreateServiceHierarchicalStateMachineVersionResponse: + """Create a new version of a context graph.""" + response = self._http.request( + "POST", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine/{context_graph_id}/", + json=body.model_dump(mode="json", exclude_none=True), + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationCreateServiceHierarchicalStateMachineVersionResponse.model_validate_json( + response.text + ) + + def delete_context_graph(self, context_graph_id: str) -> None: + """Delete a context graph.""" + self._http.request( + "DELETE", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine/{context_graph_id}/", + ) + + def get_context_graph_versions( + self, + context_graph_id: str, + params: GetServiceHierarchicalStateMachineVersionsParametersQuery | None = None, + ) -> OrganizationGetServiceHierarchicalStateMachineVersionsResponse: + """Get versions of a context graph.""" + response = self._http.request( + "GET", + f"/v1/{self._organization_id}/organization/service_hierarchical_state_machine/{context_graph_id}/version", + params=params.model_dump(mode="json", exclude_none=True) + if params + else None, + ) + return OrganizationGetServiceHierarchicalStateMachineVersionsResponse.model_validate_json( + response.text + ) + + # --- Convenience aliases --- + + def list( + self, + params: GetServiceHierarchicalStateMachinesParametersQuery | None = None, + ) -> OrganizationGetServiceHierarchicalStateMachinesResponse: + """Alias for get_context_graphs.""" + return self.get_context_graphs(params) + + def create( + self, body: OrganizationCreateServiceHierarchicalStateMachineRequest + ) -> OrganizationCreateServiceHierarchicalStateMachineResponse: + """Alias for create_context_graph.""" + return self.create_context_graph(body) + + def delete(self, context_graph_id: str) -> None: + """Alias for delete_context_graph.""" + return self.delete_context_graph(context_graph_id) diff --git a/src/amigo_sdk/resources/service.py b/src/amigo_sdk/resources/service.py index 8abb09f..7091fa0 100644 --- a/src/amigo_sdk/resources/service.py +++ b/src/amigo_sdk/resources/service.py @@ -1,6 +1,10 @@ from amigo_sdk.generated.model import ( GetServicesParametersQuery, + ServiceCreateServiceRequest, + ServiceCreateServiceResponse, ServiceGetServicesResponse, + ServiceUpdateServiceRequest, + ServiceUpsertServiceVersionSetRequest, ) from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient @@ -15,7 +19,7 @@ def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> N async def get_services( self, params: GetServicesParametersQuery | None = None ) -> ServiceGetServicesResponse: - """Get all services.""" + """List services for the organization.""" response = await self._http.request( "GET", f"/v1/{self._organization_id}/service/", @@ -25,15 +29,64 @@ async def get_services( ) return ServiceGetServicesResponse.model_validate_json(response.text) + async def create_service( + self, body: ServiceCreateServiceRequest + ) -> ServiceCreateServiceResponse: + """Create a new service.""" + response = await self._http.request( + "POST", + f"/v1/{self._organization_id}/service/", + json=body.model_dump(mode="json", exclude_none=True), + ) + return ServiceCreateServiceResponse.model_validate_json(response.text) + + async def update_service( + self, service_id: str, body: ServiceUpdateServiceRequest + ) -> None: + """Update a service.""" + await self._http.request( + "POST", + f"/v1/{self._organization_id}/service/{service_id}/", + json=body.model_dump(mode="json", exclude_none=True), + ) + + async def upsert_version_set( + self, + service_id: str, + version_set_name: str, + body: ServiceUpsertServiceVersionSetRequest, + ) -> None: + """Upsert a service version set.""" + await self._http.request( + "PUT", + f"/v1/{self._organization_id}/service/{service_id}/version_sets/{version_set_name}/", + json=body.model_dump(mode="json", exclude_none=True), + ) + + async def delete_version_set(self, service_id: str, version_set_name: str) -> None: + """Delete a service version set.""" + await self._http.request( + "DELETE", + f"/v1/{self._organization_id}/service/{service_id}/version_sets/{version_set_name}/", + ) + + # --- Convenience aliases --- + async def list( self, params: GetServicesParametersQuery | None = None ) -> ServiceGetServicesResponse: """Alias for get_services.""" return await self.get_services(params) + async def create( + self, body: ServiceCreateServiceRequest + ) -> ServiceCreateServiceResponse: + """Alias for create_service.""" + return await self.create_service(body) + class ServiceResource: - """Service resource for synchronous operations.""" + """Service resource (synchronous).""" def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None: self._http = http_client @@ -42,7 +95,7 @@ def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None: def get_services( self, params: GetServicesParametersQuery | None = None ) -> ServiceGetServicesResponse: - """Get all services for the organization.""" + """List services for the organization.""" response = self._http.request( "GET", f"/v1/{self._organization_id}/service/", @@ -52,8 +105,55 @@ def get_services( ) return ServiceGetServicesResponse.model_validate_json(response.text) + def create_service( + self, body: ServiceCreateServiceRequest + ) -> ServiceCreateServiceResponse: + """Create a new service.""" + response = self._http.request( + "POST", + f"/v1/{self._organization_id}/service/", + json=body.model_dump(mode="json", exclude_none=True), + ) + return ServiceCreateServiceResponse.model_validate_json(response.text) + + def update_service( + self, service_id: str, body: ServiceUpdateServiceRequest + ) -> None: + """Update a service.""" + self._http.request( + "POST", + f"/v1/{self._organization_id}/service/{service_id}/", + json=body.model_dump(mode="json", exclude_none=True), + ) + + def upsert_version_set( + self, + service_id: str, + version_set_name: str, + body: ServiceUpsertServiceVersionSetRequest, + ) -> None: + """Upsert a service version set.""" + self._http.request( + "PUT", + f"/v1/{self._organization_id}/service/{service_id}/version_sets/{version_set_name}/", + json=body.model_dump(mode="json", exclude_none=True), + ) + + def delete_version_set(self, service_id: str, version_set_name: str) -> None: + """Delete a service version set.""" + self._http.request( + "DELETE", + f"/v1/{self._organization_id}/service/{service_id}/version_sets/{version_set_name}/", + ) + + # --- Convenience aliases --- + def list( self, params: GetServicesParametersQuery | None = None ) -> ServiceGetServicesResponse: """Alias for get_services.""" return self.get_services(params) + + def create(self, body: ServiceCreateServiceRequest) -> ServiceCreateServiceResponse: + """Alias for create_service.""" + return self.create_service(body) diff --git a/src/amigo_sdk/sdk_client.py b/src/amigo_sdk/sdk_client.py index a7eedd8..8f2937a 100644 --- a/src/amigo_sdk/sdk_client.py +++ b/src/amigo_sdk/sdk_client.py @@ -2,6 +2,11 @@ from amigo_sdk.config import AmigoConfig from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient +from amigo_sdk.resources.agent import AgentResource, AsyncAgentResource +from amigo_sdk.resources.context_graph import ( + AsyncContextGraphResource, + ContextGraphResource, +) from amigo_sdk.resources.conversation import ( AsyncConversationResource, ConversationResource, @@ -75,6 +80,10 @@ def __init__( self._http, self._cfg.organization_id ) self._users = AsyncUserResource(self._http, self._cfg.organization_id) + self._agents = AsyncAgentResource(self._http, self._cfg.organization_id) + self._context_graphs = AsyncContextGraphResource( + self._http, self._cfg.organization_id + ) @property def config(self) -> AmigoConfig: @@ -86,6 +95,16 @@ def organizations(self) -> AsyncOrganizationResource: """Access organization resource.""" return self._organizations + @property + def agents(self) -> AsyncAgentResource: + """Access agent resource.""" + return self._agents + + @property + def context_graphs(self) -> AsyncContextGraphResource: + """Access context graph (HSM) resource.""" + return self._context_graphs + @property def services(self) -> AsyncServiceResource: """Access service resource.""" @@ -171,6 +190,10 @@ def __init__( self._http, self._cfg.organization_id ) self._users = UserResource(self._http, self._cfg.organization_id) + self._agents = AgentResource(self._http, self._cfg.organization_id) + self._context_graphs = ContextGraphResource( + self._http, self._cfg.organization_id + ) @property def config(self) -> AmigoConfig: @@ -182,6 +205,16 @@ def organizations(self) -> OrganizationResource: """Access organization resource.""" return self._organizations + @property + def agents(self) -> AgentResource: + """Access agent resource.""" + return self._agents + + @property + def context_graphs(self) -> ContextGraphResource: + """Access context graph (HSM) resource.""" + return self._context_graphs + @property def services(self) -> ServiceResource: """Access service resource.""" diff --git a/tests/resources/test_agent.py b/tests/resources/test_agent.py new file mode 100644 index 0000000..9ec303a --- /dev/null +++ b/tests/resources/test_agent.py @@ -0,0 +1,92 @@ +import pytest + +from amigo_sdk.config import AmigoConfig +from amigo_sdk.errors import NotFoundError +from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient +from amigo_sdk.resources.agent import AgentResource, AsyncAgentResource + +from .helpers import mock_http_request, mock_http_request_sync + + +@pytest.fixture +def mock_config(): + return AmigoConfig( + api_key="test-api-key", + api_key_id="test-api-key-id", + user_id="test-user-id", + organization_id="test-org-123", + base_url="https://api.example.com", + ) + + +@pytest.fixture +def agent_resource(mock_config) -> AsyncAgentResource: + http_client = AmigoAsyncHttpClient(mock_config) + return AsyncAgentResource(http_client, "test-org-123") + + +@pytest.mark.unit +class TestAgentResource: + @pytest.mark.asyncio + async def test_get_agents_returns_data(self, agent_resource): + mock_data = {"agents": [], "has_more": False, "continuation_token": None} + async with mock_http_request(mock_data): + result = await agent_resource.get_agents() + assert result.has_more is False + + @pytest.mark.asyncio + async def test_create_agent_returns_id(self, agent_resource): + from amigo_sdk.generated.model import OrganizationCreateAgentRequest + + mock_data = {"id": "agent-123"} + body = OrganizationCreateAgentRequest(agent_name="test-agent") + async with mock_http_request(mock_data): + result = await agent_resource.create_agent(body) + assert result.id == "agent-123" + + @pytest.mark.asyncio + async def test_delete_agent_returns_none(self, agent_resource): + async with mock_http_request("", status_code=204): + await agent_resource.delete_agent("agent-123") + + @pytest.mark.asyncio + async def test_get_agents_404_raises(self, agent_resource): + async with mock_http_request('{"detail": "not found"}', status_code=404): + with pytest.raises(NotFoundError): + await agent_resource.get_agents() + + @pytest.mark.asyncio + async def test_list_alias(self, agent_resource): + mock_data = {"agents": [], "has_more": False, "continuation_token": None} + async with mock_http_request(mock_data): + result = await agent_resource.list() + assert result.has_more is False + + +@pytest.mark.unit +class TestAgentResourceSync: + def _resource(self, mock_config) -> AgentResource: + http = AmigoHttpClient(mock_config) + return AgentResource(http, mock_config.organization_id) + + def test_get_agents_returns_data_sync(self, mock_config): + res = self._resource(mock_config) + mock_data = {"agents": [], "has_more": False, "continuation_token": None} + with mock_http_request_sync(mock_data): + result = res.get_agents() + assert result.has_more is False + + def test_create_agent_returns_id_sync(self, mock_config): + from amigo_sdk.generated.model import OrganizationCreateAgentRequest + + res = self._resource(mock_config) + mock_data = {"id": "agent-456"} + body = OrganizationCreateAgentRequest(agent_name="test-agent") + with mock_http_request_sync(mock_data): + result = res.create_agent(body) + assert result.id == "agent-456" + + def test_delete_agent_sync(self, mock_config): + res = self._resource(mock_config) + with mock_http_request_sync("", status_code=204): + res.delete_agent("agent-123") diff --git a/tests/resources/test_context_graph.py b/tests/resources/test_context_graph.py new file mode 100644 index 0000000..b096a1b --- /dev/null +++ b/tests/resources/test_context_graph.py @@ -0,0 +1,101 @@ +import pytest + +from amigo_sdk.config import AmigoConfig +from amigo_sdk.errors import NotFoundError +from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient +from amigo_sdk.resources.context_graph import ( + AsyncContextGraphResource, + ContextGraphResource, +) + +from .helpers import mock_http_request, mock_http_request_sync + + +@pytest.fixture +def mock_config(): + return AmigoConfig( + api_key="test-api-key", + api_key_id="test-api-key-id", + user_id="test-user-id", + organization_id="test-org-123", + base_url="https://api.example.com", + ) + + +@pytest.fixture +def cg_resource(mock_config) -> AsyncContextGraphResource: + http_client = AmigoAsyncHttpClient(mock_config) + return AsyncContextGraphResource(http_client, "test-org-123") + + +@pytest.mark.unit +class TestContextGraphResource: + @pytest.mark.asyncio + async def test_get_context_graphs_returns_data(self, cg_resource): + mock_data = { + "service_hierarchical_state_machines": [], + "has_more": False, + "continuation_token": None, + } + async with mock_http_request(mock_data): + result = await cg_resource.get_context_graphs() + assert result.has_more is False + + @pytest.mark.asyncio + async def test_create_context_graph_returns_id(self, cg_resource): + from amigo_sdk.generated.model import ( + OrganizationCreateServiceHierarchicalStateMachineRequest, + ) + + mock_data = {"id": "hsm-123"} + body = OrganizationCreateServiceHierarchicalStateMachineRequest( + state_machine_name="test-hsm" + ) + async with mock_http_request(mock_data): + result = await cg_resource.create_context_graph(body) + assert result.id == "hsm-123" + + @pytest.mark.asyncio + async def test_delete_context_graph_returns_none(self, cg_resource): + async with mock_http_request("", status_code=204): + await cg_resource.delete_context_graph("hsm-123") + + @pytest.mark.asyncio + async def test_get_context_graphs_404_raises(self, cg_resource): + async with mock_http_request('{"detail": "not found"}', status_code=404): + with pytest.raises(NotFoundError): + await cg_resource.get_context_graphs() + + @pytest.mark.asyncio + async def test_list_alias(self, cg_resource): + mock_data = { + "service_hierarchical_state_machines": [], + "has_more": False, + "continuation_token": None, + } + async with mock_http_request(mock_data): + result = await cg_resource.list() + assert result.has_more is False + + +@pytest.mark.unit +class TestContextGraphResourceSync: + def _resource(self, mock_config) -> ContextGraphResource: + http = AmigoHttpClient(mock_config) + return ContextGraphResource(http, mock_config.organization_id) + + def test_get_context_graphs_sync(self, mock_config): + res = self._resource(mock_config) + mock_data = { + "service_hierarchical_state_machines": [], + "has_more": False, + "continuation_token": None, + } + with mock_http_request_sync(mock_data): + result = res.get_context_graphs() + assert result.has_more is False + + def test_delete_context_graph_sync(self, mock_config): + res = self._resource(mock_config) + with mock_http_request_sync("", status_code=204): + res.delete_context_graph("hsm-123") diff --git a/tests/resources/test_service.py b/tests/resources/test_service.py index faa299c..704b417 100644 --- a/tests/resources/test_service.py +++ b/tests/resources/test_service.py @@ -61,6 +61,62 @@ async def test_get_services_nonexistent_organization_raises_not_found( with pytest.raises(NotFoundError): await service_resource.get_services() + @pytest.mark.asyncio + async def test_create_service(self, service_resource): + from amigo_sdk.generated.model import ServiceCreateServiceRequest + + mock_data = {"id": "svc-async-1"} + body = ServiceCreateServiceRequest( + name="test-svc", + description="desc", + keyterms=[], + tags={}, + is_active=False, + agent_id="aaaaaaaaaaaaaaaaaaaaaaaa", + service_hierarchical_state_machine_id="bbbbbbbbbbbbbbbbbbbbbbbb", + ) + async with mock_http_request(mock_data): + result = await service_resource.create_service(body) + assert result.id == "svc-async-1" + + @pytest.mark.asyncio + async def test_update_service(self, service_resource): + from amigo_sdk.generated.model import ServiceUpdateServiceRequest + + body = ServiceUpdateServiceRequest(is_active=True) + async with mock_http_request("", status_code=204): + await service_resource.update_service("svc-1", body) + + @pytest.mark.asyncio + async def test_delete_version_set(self, service_resource): + async with mock_http_request("", status_code=204): + await service_resource.delete_version_set("svc-1", "staging") + + @pytest.mark.asyncio + async def test_list_alias(self, service_resource): + mock_data = create_services_response_data() + async with mock_http_request(mock_data): + result = await service_resource.list() + assert isinstance(result, ServiceGetServicesResponse) + + @pytest.mark.asyncio + async def test_create_alias(self, service_resource): + from amigo_sdk.generated.model import ServiceCreateServiceRequest + + mock_data = {"id": "svc-alias-1"} + body = ServiceCreateServiceRequest( + name="test-svc", + description="desc", + keyterms=[], + tags={}, + is_active=False, + agent_id="aaaaaaaaaaaaaaaaaaaaaaaa", + service_hierarchical_state_machine_id="bbbbbbbbbbbbbbbbbbbbbbbb", + ) + async with mock_http_request(mock_data): + result = await service_resource.create(body) + assert result.id == "svc-alias-1" + @pytest.mark.unit class TestServiceResourceSync: @@ -88,3 +144,41 @@ def test_get_services_nonexistent_organization_raises_not_found_sync( ): with pytest.raises(NotFoundError): res.get_services() + + def test_create_service_sync(self, mock_config): + from amigo_sdk.generated.model import ServiceCreateServiceRequest + + res = self._resource(mock_config) + mock_data = {"id": "svc-123"} + body = ServiceCreateServiceRequest( + name="test-svc", + description="A test service", + keyterms=[], + tags={}, + is_active=False, + agent_id="aaaaaaaaaaaaaaaaaaaaaaaa", + service_hierarchical_state_machine_id="bbbbbbbbbbbbbbbbbbbbbbbb", + ) + with mock_http_request_sync(mock_data): + result = res.create_service(body) + assert result.id == "svc-123" + + def test_update_service_sync(self, mock_config): + from amigo_sdk.generated.model import ServiceUpdateServiceRequest + + res = self._resource(mock_config) + body = ServiceUpdateServiceRequest(is_active=True) + with mock_http_request_sync("", status_code=204): + res.update_service("svc-123", body) + + def test_delete_version_set_sync(self, mock_config): + res = self._resource(mock_config) + with mock_http_request_sync("", status_code=204): + res.delete_version_set("svc-123", "staging") + + def test_list_alias_sync(self, mock_config): + res = self._resource(mock_config) + mock_data = create_services_response_data() + with mock_http_request_sync(mock_data): + result = res.list() + assert isinstance(result, ServiceGetServicesResponse)