Skip to content

Commit 6cfd4c4

Browse files
committed
add async create_from_* funcs
1 parent 5f8f365 commit 6cfd4c4

2 files changed

Lines changed: 369 additions & 0 deletions

File tree

src/runloop_api_client/sdk/async_.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,151 @@ async def create(
513513
)
514514
return AsyncAgent(self._client, agent_view.id, agent_view)
515515

516+
async def create_from_npm(
517+
self,
518+
*,
519+
package_name: str,
520+
npm_version: Optional[str] = None,
521+
registry_url: Optional[str] = None,
522+
agent_setup: Optional[list[str]] = None,
523+
**params: Unpack[SDKAgentCreateParams],
524+
) -> AsyncAgent:
525+
"""Create an agent from an NPM package.
526+
527+
:param package_name: NPM package name
528+
:type package_name: str
529+
:param npm_version: NPM version constraint, defaults to None
530+
:type npm_version: Optional[str], optional
531+
:param registry_url: NPM registry URL, defaults to None
532+
:type registry_url: Optional[str], optional
533+
:param agent_setup: Setup commands to run after installation, defaults to None
534+
:type agent_setup: Optional[list[str]], optional
535+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
536+
:return: Wrapper bound to the newly created agent
537+
:rtype: AsyncAgent
538+
:raises ValueError: If 'source' is provided in params
539+
"""
540+
if "source" in params:
541+
raise ValueError("Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration")
542+
543+
npm_config: dict = {"package_name": package_name}
544+
if npm_version is not None:
545+
npm_config["npm_version"] = npm_version
546+
if registry_url is not None:
547+
npm_config["registry_url"] = registry_url
548+
if agent_setup is not None:
549+
npm_config["agent_setup"] = agent_setup
550+
551+
return await self.create(
552+
source={"type": "npm", "npm": npm_config},
553+
**params,
554+
)
555+
556+
async def create_from_pip(
557+
self,
558+
*,
559+
package_name: str,
560+
pip_version: Optional[str] = None,
561+
registry_url: Optional[str] = None,
562+
agent_setup: Optional[list[str]] = None,
563+
**params: Unpack[SDKAgentCreateParams],
564+
) -> AsyncAgent:
565+
"""Create an agent from a Pip package.
566+
567+
:param package_name: Pip package name
568+
:type package_name: str
569+
:param pip_version: Pip version constraint, defaults to None
570+
:type pip_version: Optional[str], optional
571+
:param registry_url: Pip registry URL, defaults to None
572+
:type registry_url: Optional[str], optional
573+
:param agent_setup: Setup commands to run after installation, defaults to None
574+
:type agent_setup: Optional[list[str]], optional
575+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
576+
:return: Wrapper bound to the newly created agent
577+
:rtype: AsyncAgent
578+
:raises ValueError: If 'source' is provided in params
579+
"""
580+
if "source" in params:
581+
raise ValueError("Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration")
582+
583+
pip_config: dict = {"package_name": package_name}
584+
if pip_version is not None:
585+
pip_config["pip_version"] = pip_version
586+
if registry_url is not None:
587+
pip_config["registry_url"] = registry_url
588+
if agent_setup is not None:
589+
pip_config["agent_setup"] = agent_setup
590+
591+
return await self.create(
592+
source={"type": "pip", "pip": pip_config},
593+
**params,
594+
)
595+
596+
async def create_from_git(
597+
self,
598+
*,
599+
repository: str,
600+
ref: Optional[str] = None,
601+
agent_setup: Optional[list[str]] = None,
602+
**params: Unpack[SDKAgentCreateParams],
603+
) -> AsyncAgent:
604+
"""Create an agent from a Git repository.
605+
606+
:param repository: Git repository URL
607+
:type repository: str
608+
:param ref: Optional Git ref (branch/tag/commit), defaults to main/HEAD
609+
:type ref: Optional[str], optional
610+
:param agent_setup: Setup commands to run after cloning, defaults to None
611+
:type agent_setup: Optional[list[str]], optional
612+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
613+
:return: Wrapper bound to the newly created agent
614+
:rtype: AsyncAgent
615+
:raises ValueError: If 'source' is provided in params
616+
"""
617+
if "source" in params:
618+
raise ValueError("Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration")
619+
620+
git_config: dict = {"repository": repository}
621+
if ref is not None:
622+
git_config["ref"] = ref
623+
if agent_setup is not None:
624+
git_config["agent_setup"] = agent_setup
625+
626+
return await self.create(
627+
source={"type": "git", "git": git_config},
628+
**params,
629+
)
630+
631+
async def create_from_object(
632+
self,
633+
*,
634+
object_id: str,
635+
agent_setup: Optional[list[str]] = None,
636+
**params: Unpack[SDKAgentCreateParams],
637+
) -> AsyncAgent:
638+
"""Create an agent from a storage object.
639+
640+
:param object_id: Storage object ID
641+
:type object_id: str
642+
:param agent_setup: Setup commands to run after unpacking, defaults to None
643+
:type agent_setup: Optional[list[str]], optional
644+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
645+
:return: Wrapper bound to the newly created agent
646+
:rtype: AsyncAgent
647+
:raises ValueError: If 'source' is provided in params
648+
"""
649+
if "source" in params:
650+
raise ValueError("Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration")
651+
652+
object_config: dict = {"object_id": object_id}
653+
if agent_setup is not None:
654+
object_config["agent_setup"] = agent_setup
655+
656+
return await self.create(
657+
source={"type": "object", "object": object_config},
658+
**params,
659+
)
660+
516661
def from_id(self, agent_id: str) -> AsyncAgent:
517662
"""Attach to an existing agent by ID.
518663

tests/sdk/test_async_clients.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,230 @@ async def mock_retrieve(agent_id, **kwargs):
628628

629629
mock_async_client.agents.list.assert_called_once()
630630

631+
@pytest.mark.asyncio
632+
async def test_create_from_npm(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
633+
"""Test create_from_npm factory method."""
634+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
635+
636+
client = AsyncAgentOps(mock_async_client)
637+
agent = await client.create_from_npm(
638+
name="test-agent",
639+
package_name="@runloop/example-agent",
640+
)
641+
642+
assert isinstance(agent, AsyncAgent)
643+
assert agent.id == "agent_123"
644+
mock_async_client.agents.create.assert_awaited_once_with(
645+
source={
646+
"type": "npm",
647+
"npm": {
648+
"package_name": "@runloop/example-agent",
649+
},
650+
},
651+
name="test-agent",
652+
)
653+
654+
@pytest.mark.asyncio
655+
async def test_create_from_npm_with_all_options(
656+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
657+
) -> None:
658+
"""Test create_from_npm factory method with all optional parameters."""
659+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
660+
661+
client = AsyncAgentOps(mock_async_client)
662+
agent = await client.create_from_npm(
663+
name="test-agent",
664+
package_name="@runloop/example-agent",
665+
npm_version="1.2.3",
666+
registry_url="https://registry.example.com",
667+
agent_setup=["npm install", "npm run setup"],
668+
extra_headers={"X-Custom": "header"},
669+
)
670+
671+
assert isinstance(agent, AsyncAgent)
672+
assert agent.id == "agent_123"
673+
mock_async_client.agents.create.assert_awaited_once_with(
674+
source={
675+
"type": "npm",
676+
"npm": {
677+
"package_name": "@runloop/example-agent",
678+
"npm_version": "1.2.3",
679+
"registry_url": "https://registry.example.com",
680+
"agent_setup": ["npm install", "npm run setup"],
681+
},
682+
},
683+
name="test-agent",
684+
extra_headers={"X-Custom": "header"},
685+
)
686+
687+
@pytest.mark.asyncio
688+
async def test_create_from_npm_raises_when_source_provided(self, mock_async_client: AsyncMock) -> None:
689+
"""Test create_from_npm raises ValueError when source is provided in params."""
690+
client = AsyncAgentOps(mock_async_client)
691+
692+
with pytest.raises(ValueError, match="Cannot specify 'source' when using create_from_npm"):
693+
await client.create_from_npm(
694+
name="test-agent",
695+
package_name="@runloop/example-agent",
696+
source={"type": "git", "git": {"repository": "https://github.com/example/repo"}},
697+
)
698+
699+
@pytest.mark.asyncio
700+
async def test_create_from_pip(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
701+
"""Test create_from_pip factory method."""
702+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
703+
704+
client = AsyncAgentOps(mock_async_client)
705+
agent = await client.create_from_pip(
706+
name="test-agent",
707+
package_name="runloop-example-agent",
708+
)
709+
710+
assert isinstance(agent, AsyncAgent)
711+
assert agent.id == "agent_123"
712+
mock_async_client.agents.create.assert_awaited_once_with(
713+
source={
714+
"type": "pip",
715+
"pip": {
716+
"package_name": "runloop-example-agent",
717+
},
718+
},
719+
name="test-agent",
720+
)
721+
722+
@pytest.mark.asyncio
723+
async def test_create_from_pip_with_all_options(
724+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
725+
) -> None:
726+
"""Test create_from_pip factory method with all optional parameters."""
727+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
728+
729+
client = AsyncAgentOps(mock_async_client)
730+
agent = await client.create_from_pip(
731+
name="test-agent",
732+
package_name="runloop-example-agent",
733+
pip_version="1.2.3",
734+
registry_url="https://pypi.example.com",
735+
agent_setup=["pip install extra-deps"],
736+
)
737+
738+
assert isinstance(agent, AsyncAgent)
739+
assert agent.id == "agent_123"
740+
mock_async_client.agents.create.assert_awaited_once_with(
741+
source={
742+
"type": "pip",
743+
"pip": {
744+
"package_name": "runloop-example-agent",
745+
"pip_version": "1.2.3",
746+
"registry_url": "https://pypi.example.com",
747+
"agent_setup": ["pip install extra-deps"],
748+
},
749+
},
750+
name="test-agent",
751+
)
752+
753+
@pytest.mark.asyncio
754+
async def test_create_from_git(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
755+
"""Test create_from_git factory method."""
756+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
757+
758+
client = AsyncAgentOps(mock_async_client)
759+
agent = await client.create_from_git(
760+
name="test-agent",
761+
repository="https://github.com/example/agent-repo",
762+
)
763+
764+
assert isinstance(agent, AsyncAgent)
765+
assert agent.id == "agent_123"
766+
mock_async_client.agents.create.assert_awaited_once_with(
767+
source={
768+
"type": "git",
769+
"git": {
770+
"repository": "https://github.com/example/agent-repo",
771+
},
772+
},
773+
name="test-agent",
774+
)
775+
776+
@pytest.mark.asyncio
777+
async def test_create_from_git_with_all_options(
778+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
779+
) -> None:
780+
"""Test create_from_git factory method with all optional parameters."""
781+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
782+
783+
client = AsyncAgentOps(mock_async_client)
784+
agent = await client.create_from_git(
785+
name="test-agent",
786+
repository="https://github.com/example/agent-repo",
787+
ref="develop",
788+
agent_setup=["npm install", "npm run build"],
789+
)
790+
791+
assert isinstance(agent, AsyncAgent)
792+
assert agent.id == "agent_123"
793+
mock_async_client.agents.create.assert_awaited_once_with(
794+
source={
795+
"type": "git",
796+
"git": {
797+
"repository": "https://github.com/example/agent-repo",
798+
"ref": "develop",
799+
"agent_setup": ["npm install", "npm run build"],
800+
},
801+
},
802+
name="test-agent",
803+
)
804+
805+
@pytest.mark.asyncio
806+
async def test_create_from_object(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
807+
"""Test create_from_object factory method."""
808+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
809+
810+
client = AsyncAgentOps(mock_async_client)
811+
agent = await client.create_from_object(
812+
name="test-agent",
813+
object_id="obj_123",
814+
)
815+
816+
assert isinstance(agent, AsyncAgent)
817+
assert agent.id == "agent_123"
818+
mock_async_client.agents.create.assert_awaited_once_with(
819+
source={
820+
"type": "object",
821+
"object": {
822+
"object_id": "obj_123",
823+
},
824+
},
825+
name="test-agent",
826+
)
827+
828+
@pytest.mark.asyncio
829+
async def test_create_from_object_with_agent_setup(
830+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
831+
) -> None:
832+
"""Test create_from_object factory method with agent_setup."""
833+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
834+
835+
client = AsyncAgentOps(mock_async_client)
836+
agent = await client.create_from_object(
837+
name="test-agent",
838+
object_id="obj_123",
839+
agent_setup=["chmod +x setup.sh", "./setup.sh"],
840+
)
841+
842+
assert isinstance(agent, AsyncAgent)
843+
assert agent.id == "agent_123"
844+
mock_async_client.agents.create.assert_awaited_once_with(
845+
source={
846+
"type": "object",
847+
"object": {
848+
"object_id": "obj_123",
849+
"agent_setup": ["chmod +x setup.sh", "./setup.sh"],
850+
},
851+
},
852+
name="test-agent",
853+
)
854+
631855

632856
class TestAsyncRunloopSDK:
633857
"""Tests for AsyncRunloopSDK class."""

0 commit comments

Comments
 (0)