Skip to content

Commit 8299c83

Browse files
committed
add async create_from_* funcs
1 parent 90ecd1e commit 8299c83

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
@@ -580,6 +580,151 @@ async def create(
580580
)
581581
return AsyncAgent(self._client, agent_view.id, agent_view)
582582

583+
async def create_from_npm(
584+
self,
585+
*,
586+
package_name: str,
587+
npm_version: Optional[str] = None,
588+
registry_url: Optional[str] = None,
589+
agent_setup: Optional[list[str]] = None,
590+
**params: Unpack[SDKAgentCreateParams],
591+
) -> AsyncAgent:
592+
"""Create an agent from an NPM package.
593+
594+
:param package_name: NPM package name
595+
:type package_name: str
596+
:param npm_version: NPM version constraint, defaults to None
597+
:type npm_version: Optional[str], optional
598+
:param registry_url: NPM registry URL, defaults to None
599+
:type registry_url: Optional[str], optional
600+
:param agent_setup: Setup commands to run after installation, defaults to None
601+
:type agent_setup: Optional[list[str]], optional
602+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
603+
:return: Wrapper bound to the newly created agent
604+
:rtype: AsyncAgent
605+
:raises ValueError: If 'source' is provided in params
606+
"""
607+
if "source" in params:
608+
raise ValueError("Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration")
609+
610+
npm_config: dict = {"package_name": package_name}
611+
if npm_version is not None:
612+
npm_config["npm_version"] = npm_version
613+
if registry_url is not None:
614+
npm_config["registry_url"] = registry_url
615+
if agent_setup is not None:
616+
npm_config["agent_setup"] = agent_setup
617+
618+
return await self.create(
619+
source={"type": "npm", "npm": npm_config},
620+
**params,
621+
)
622+
623+
async def create_from_pip(
624+
self,
625+
*,
626+
package_name: str,
627+
pip_version: Optional[str] = None,
628+
registry_url: Optional[str] = None,
629+
agent_setup: Optional[list[str]] = None,
630+
**params: Unpack[SDKAgentCreateParams],
631+
) -> AsyncAgent:
632+
"""Create an agent from a Pip package.
633+
634+
:param package_name: Pip package name
635+
:type package_name: str
636+
:param pip_version: Pip version constraint, defaults to None
637+
:type pip_version: Optional[str], optional
638+
:param registry_url: Pip registry URL, defaults to None
639+
:type registry_url: Optional[str], optional
640+
:param agent_setup: Setup commands to run after installation, defaults to None
641+
:type agent_setup: Optional[list[str]], optional
642+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
643+
:return: Wrapper bound to the newly created agent
644+
:rtype: AsyncAgent
645+
:raises ValueError: If 'source' is provided in params
646+
"""
647+
if "source" in params:
648+
raise ValueError("Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration")
649+
650+
pip_config: dict = {"package_name": package_name}
651+
if pip_version is not None:
652+
pip_config["pip_version"] = pip_version
653+
if registry_url is not None:
654+
pip_config["registry_url"] = registry_url
655+
if agent_setup is not None:
656+
pip_config["agent_setup"] = agent_setup
657+
658+
return await self.create(
659+
source={"type": "pip", "pip": pip_config},
660+
**params,
661+
)
662+
663+
async def create_from_git(
664+
self,
665+
*,
666+
repository: str,
667+
ref: Optional[str] = None,
668+
agent_setup: Optional[list[str]] = None,
669+
**params: Unpack[SDKAgentCreateParams],
670+
) -> AsyncAgent:
671+
"""Create an agent from a Git repository.
672+
673+
:param repository: Git repository URL
674+
:type repository: str
675+
:param ref: Optional Git ref (branch/tag/commit), defaults to main/HEAD
676+
:type ref: Optional[str], optional
677+
:param agent_setup: Setup commands to run after cloning, defaults to None
678+
:type agent_setup: Optional[list[str]], optional
679+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
680+
:return: Wrapper bound to the newly created agent
681+
:rtype: AsyncAgent
682+
:raises ValueError: If 'source' is provided in params
683+
"""
684+
if "source" in params:
685+
raise ValueError("Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration")
686+
687+
git_config: dict = {"repository": repository}
688+
if ref is not None:
689+
git_config["ref"] = ref
690+
if agent_setup is not None:
691+
git_config["agent_setup"] = agent_setup
692+
693+
return await self.create(
694+
source={"type": "git", "git": git_config},
695+
**params,
696+
)
697+
698+
async def create_from_object(
699+
self,
700+
*,
701+
object_id: str,
702+
agent_setup: Optional[list[str]] = None,
703+
**params: Unpack[SDKAgentCreateParams],
704+
) -> AsyncAgent:
705+
"""Create an agent from a storage object.
706+
707+
:param object_id: Storage object ID
708+
:type object_id: str
709+
:param agent_setup: Setup commands to run after unpacking, defaults to None
710+
:type agent_setup: Optional[list[str]], optional
711+
:param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source')
712+
:return: Wrapper bound to the newly created agent
713+
:rtype: AsyncAgent
714+
:raises ValueError: If 'source' is provided in params
715+
"""
716+
if "source" in params:
717+
raise ValueError("Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration")
718+
719+
object_config: dict = {"object_id": object_id}
720+
if agent_setup is not None:
721+
object_config["agent_setup"] = agent_setup
722+
723+
return await self.create(
724+
source={"type": "object", "object": object_config},
725+
**params,
726+
)
727+
583728
def from_id(self, agent_id: str) -> AsyncAgent:
584729
"""Attach to an existing agent by ID.
585730

tests/sdk/test_async_ops.py

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

836836
mock_async_client.agents.list.assert_called_once()
837837

838+
@pytest.mark.asyncio
839+
async def test_create_from_npm(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
840+
"""Test create_from_npm factory method."""
841+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
842+
843+
client = AsyncAgentOps(mock_async_client)
844+
agent = await client.create_from_npm(
845+
name="test-agent",
846+
package_name="@runloop/example-agent",
847+
)
848+
849+
assert isinstance(agent, AsyncAgent)
850+
assert agent.id == "agent_123"
851+
mock_async_client.agents.create.assert_awaited_once_with(
852+
source={
853+
"type": "npm",
854+
"npm": {
855+
"package_name": "@runloop/example-agent",
856+
},
857+
},
858+
name="test-agent",
859+
)
860+
861+
@pytest.mark.asyncio
862+
async def test_create_from_npm_with_all_options(
863+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
864+
) -> None:
865+
"""Test create_from_npm factory method with all optional parameters."""
866+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
867+
868+
client = AsyncAgentOps(mock_async_client)
869+
agent = await client.create_from_npm(
870+
name="test-agent",
871+
package_name="@runloop/example-agent",
872+
npm_version="1.2.3",
873+
registry_url="https://registry.example.com",
874+
agent_setup=["npm install", "npm run setup"],
875+
extra_headers={"X-Custom": "header"},
876+
)
877+
878+
assert isinstance(agent, AsyncAgent)
879+
assert agent.id == "agent_123"
880+
mock_async_client.agents.create.assert_awaited_once_with(
881+
source={
882+
"type": "npm",
883+
"npm": {
884+
"package_name": "@runloop/example-agent",
885+
"npm_version": "1.2.3",
886+
"registry_url": "https://registry.example.com",
887+
"agent_setup": ["npm install", "npm run setup"],
888+
},
889+
},
890+
name="test-agent",
891+
extra_headers={"X-Custom": "header"},
892+
)
893+
894+
@pytest.mark.asyncio
895+
async def test_create_from_npm_raises_when_source_provided(self, mock_async_client: AsyncMock) -> None:
896+
"""Test create_from_npm raises ValueError when source is provided in params."""
897+
client = AsyncAgentOps(mock_async_client)
898+
899+
with pytest.raises(ValueError, match="Cannot specify 'source' when using create_from_npm"):
900+
await client.create_from_npm(
901+
name="test-agent",
902+
package_name="@runloop/example-agent",
903+
source={"type": "git", "git": {"repository": "https://github.com/example/repo"}},
904+
)
905+
906+
@pytest.mark.asyncio
907+
async def test_create_from_pip(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
908+
"""Test create_from_pip factory method."""
909+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
910+
911+
client = AsyncAgentOps(mock_async_client)
912+
agent = await client.create_from_pip(
913+
name="test-agent",
914+
package_name="runloop-example-agent",
915+
)
916+
917+
assert isinstance(agent, AsyncAgent)
918+
assert agent.id == "agent_123"
919+
mock_async_client.agents.create.assert_awaited_once_with(
920+
source={
921+
"type": "pip",
922+
"pip": {
923+
"package_name": "runloop-example-agent",
924+
},
925+
},
926+
name="test-agent",
927+
)
928+
929+
@pytest.mark.asyncio
930+
async def test_create_from_pip_with_all_options(
931+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
932+
) -> None:
933+
"""Test create_from_pip factory method with all optional parameters."""
934+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
935+
936+
client = AsyncAgentOps(mock_async_client)
937+
agent = await client.create_from_pip(
938+
name="test-agent",
939+
package_name="runloop-example-agent",
940+
pip_version="1.2.3",
941+
registry_url="https://pypi.example.com",
942+
agent_setup=["pip install extra-deps"],
943+
)
944+
945+
assert isinstance(agent, AsyncAgent)
946+
assert agent.id == "agent_123"
947+
mock_async_client.agents.create.assert_awaited_once_with(
948+
source={
949+
"type": "pip",
950+
"pip": {
951+
"package_name": "runloop-example-agent",
952+
"pip_version": "1.2.3",
953+
"registry_url": "https://pypi.example.com",
954+
"agent_setup": ["pip install extra-deps"],
955+
},
956+
},
957+
name="test-agent",
958+
)
959+
960+
@pytest.mark.asyncio
961+
async def test_create_from_git(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
962+
"""Test create_from_git factory method."""
963+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
964+
965+
client = AsyncAgentOps(mock_async_client)
966+
agent = await client.create_from_git(
967+
name="test-agent",
968+
repository="https://github.com/example/agent-repo",
969+
)
970+
971+
assert isinstance(agent, AsyncAgent)
972+
assert agent.id == "agent_123"
973+
mock_async_client.agents.create.assert_awaited_once_with(
974+
source={
975+
"type": "git",
976+
"git": {
977+
"repository": "https://github.com/example/agent-repo",
978+
},
979+
},
980+
name="test-agent",
981+
)
982+
983+
@pytest.mark.asyncio
984+
async def test_create_from_git_with_all_options(
985+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
986+
) -> None:
987+
"""Test create_from_git factory method with all optional parameters."""
988+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
989+
990+
client = AsyncAgentOps(mock_async_client)
991+
agent = await client.create_from_git(
992+
name="test-agent",
993+
repository="https://github.com/example/agent-repo",
994+
ref="develop",
995+
agent_setup=["npm install", "npm run build"],
996+
)
997+
998+
assert isinstance(agent, AsyncAgent)
999+
assert agent.id == "agent_123"
1000+
mock_async_client.agents.create.assert_awaited_once_with(
1001+
source={
1002+
"type": "git",
1003+
"git": {
1004+
"repository": "https://github.com/example/agent-repo",
1005+
"ref": "develop",
1006+
"agent_setup": ["npm install", "npm run build"],
1007+
},
1008+
},
1009+
name="test-agent",
1010+
)
1011+
1012+
@pytest.mark.asyncio
1013+
async def test_create_from_object(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None:
1014+
"""Test create_from_object factory method."""
1015+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
1016+
1017+
client = AsyncAgentOps(mock_async_client)
1018+
agent = await client.create_from_object(
1019+
name="test-agent",
1020+
object_id="obj_123",
1021+
)
1022+
1023+
assert isinstance(agent, AsyncAgent)
1024+
assert agent.id == "agent_123"
1025+
mock_async_client.agents.create.assert_awaited_once_with(
1026+
source={
1027+
"type": "object",
1028+
"object": {
1029+
"object_id": "obj_123",
1030+
},
1031+
},
1032+
name="test-agent",
1033+
)
1034+
1035+
@pytest.mark.asyncio
1036+
async def test_create_from_object_with_agent_setup(
1037+
self, mock_async_client: AsyncMock, agent_view: MockAgentView
1038+
) -> None:
1039+
"""Test create_from_object factory method with agent_setup."""
1040+
mock_async_client.agents.create = AsyncMock(return_value=agent_view)
1041+
1042+
client = AsyncAgentOps(mock_async_client)
1043+
agent = await client.create_from_object(
1044+
name="test-agent",
1045+
object_id="obj_123",
1046+
agent_setup=["chmod +x setup.sh", "./setup.sh"],
1047+
)
1048+
1049+
assert isinstance(agent, AsyncAgent)
1050+
assert agent.id == "agent_123"
1051+
mock_async_client.agents.create.assert_awaited_once_with(
1052+
source={
1053+
"type": "object",
1054+
"object": {
1055+
"object_id": "obj_123",
1056+
"agent_setup": ["chmod +x setup.sh", "./setup.sh"],
1057+
},
1058+
},
1059+
name="test-agent",
1060+
)
1061+
8381062

8391063
class TestAsyncRunloopSDK:
8401064
"""Tests for AsyncRunloopSDK class."""

0 commit comments

Comments
 (0)