codecraftai-restclient-runpod 1.2.0__py3-none-any.whl

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.
Files changed (30) hide show
  1. ccai/restclient/runpod/__init__.py +0 -0
  2. ccai/restclient/runpod/apigroups/__init__.py +0 -0
  3. ccai/restclient/runpod/apigroups/datacentersgroup.py +82 -0
  4. ccai/restclient/runpod/apigroups/gputypesgroup.py +80 -0
  5. ccai/restclient/runpod/apigroups/groupsupport.py +182 -0
  6. ccai/restclient/runpod/apigroups/networkvolumesgroup.py +156 -0
  7. ccai/restclient/runpod/apigroups/podsgroup.py +216 -0
  8. ccai/restclient/runpod/apigroups/podtemplatesgroup.py +148 -0
  9. ccai/restclient/runpod/apigroups/tagsgroup.py +74 -0
  10. ccai/restclient/runpod/aspectsrunpod.py +123 -0
  11. ccai/restclient/runpod/enums/__init__.py +0 -0
  12. ccai/restclient/runpod/enums/runpodcloudtype.py +30 -0
  13. ccai/restclient/runpod/exceptions/__init__.py +0 -0
  14. ccai/restclient/runpod/exceptions/runpoderrors.py +55 -0
  15. ccai/restclient/runpod/model/__init__.py +0 -0
  16. ccai/restclient/runpod/model/runpoddatacenter.py +85 -0
  17. ccai/restclient/runpod/model/runpodgputype.py +109 -0
  18. ccai/restclient/runpod/model/runpodnetworkvolume.py +91 -0
  19. ccai/restclient/runpod/model/runpodpod.py +240 -0
  20. ccai/restclient/runpod/model/runpodpodtemplate.py +103 -0
  21. ccai/restclient/runpod/py.typed +0 -0
  22. ccai/restclient/runpod/runpodclient.py +584 -0
  23. ccai/restclient/runpod/specs/__init__.py +0 -0
  24. ccai/restclient/runpod/specs/runpodnetworkvolumespec.py +56 -0
  25. ccai/restclient/runpod/specs/runpodpodlaunchspec.py +125 -0
  26. ccai/restclient/runpod/specs/runpodpodtemplatespec.py +65 -0
  27. codecraftai_restclient_runpod-1.2.0.dist-info/METADATA +86 -0
  28. codecraftai_restclient_runpod-1.2.0.dist-info/RECORD +30 -0
  29. codecraftai_restclient_runpod-1.2.0.dist-info/WHEEL +4 -0
  30. codecraftai_restclient_runpod-1.2.0.dist-info/licenses/LICENSE.md +204 -0
File without changes
File without changes
@@ -0,0 +1,82 @@
1
+ """
2
+ .. module:: datacentersgroup
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Module that contains the :class:`DataCentersGroup` api
5
+ group exposing the RunPod data-center catalog endpoint.
6
+
7
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
8
+ """
9
+
10
+ __author__ = "Myron Walker"
11
+ __copyright__ = "Copyright 2026, Code Craft AI LLC"
12
+ __credits__ = []
13
+
14
+
15
+ from typing import Any, Dict, List
16
+
17
+ from ccai.restclient.base.restapigroup import RestApiGroup
18
+
19
+ from ccai.restclient.runpod.aspectsrunpod import (
20
+ AspectsRunpod, DEFAULT_RUNPOD_ASPECTS
21
+ )
22
+ from ccai.restclient.runpod.apigroups.groupsupport import raise_for_status
23
+ from ccai.restclient.runpod.model.runpoddatacenter import RunpodDataCenter
24
+
25
+
26
+ class DataCentersGroup(RestApiGroup):
27
+ """
28
+ :class:`DataCentersGroup` exposes the RunPod data-center catalog
29
+ endpoint. The catalog is a small, non-paged collection.
30
+ """
31
+
32
+ BASE_LEAF = "/v1"
33
+
34
+ def list(self, *,
35
+ aspects: AspectsRunpod = DEFAULT_RUNPOD_ASPECTS) -> List[RunpodDataCenter]:
36
+ """
37
+ List every RunPod data center.
38
+
39
+ :param aspects: Behaviour control for the call.
40
+
41
+ :returns: A list of :class:`RunpodDataCenter` records.
42
+ """
43
+ client = self.client
44
+ leaf = self.build_leaf(parts=["datacenters"])
45
+ response = client.request(method="GET", leaf=leaf, aspects=aspects)
46
+ raise_for_status(response=response, action="list data centers")
47
+
48
+ payload = response.json()
49
+ raw_entries = self._extract_entries(payload=payload)
50
+
51
+ rtnval: List[RunpodDataCenter] = []
52
+ for entry in raw_entries:
53
+ rtnval.append(RunpodDataCenter.from_dict(entry))
54
+
55
+ return rtnval
56
+
57
+ def _extract_entries(self, *, payload: Any) -> List[Dict[str, Any]]:
58
+ """
59
+ Extract the data-center entry list from a response payload.
60
+
61
+ :param payload: The decoded response payload.
62
+
63
+ :returns: The list of raw data-center dictionaries.
64
+ """
65
+ if isinstance(payload, list):
66
+ rtnval: List[Dict[str, Any]] = list(payload)
67
+ return rtnval
68
+
69
+ if isinstance(payload, dict):
70
+ entries = payload.get("dataCenters")
71
+ if entries is None:
72
+ entries = payload.get("datacenters")
73
+ if entries is None:
74
+ entries = payload.get("items", [])
75
+ rtnval = list(entries)
76
+ return rtnval
77
+
78
+ rtnval = []
79
+ return rtnval
80
+
81
+
82
+ __all__ = ["DataCentersGroup"]
@@ -0,0 +1,80 @@
1
+ """
2
+ .. module:: gputypesgroup
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Module that contains the :class:`GpuTypesGroup` api group
5
+ exposing the RunPod GPU-type catalog endpoint.
6
+
7
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
8
+ """
9
+
10
+ __author__ = "Myron Walker"
11
+ __copyright__ = "Copyright 2026, Code Craft AI LLC"
12
+ __credits__ = []
13
+
14
+
15
+ from typing import Any, Dict, List
16
+
17
+ from ccai.restclient.base.restapigroup import RestApiGroup
18
+
19
+ from ccai.restclient.runpod.aspectsrunpod import (
20
+ AspectsRunpod, DEFAULT_RUNPOD_ASPECTS
21
+ )
22
+ from ccai.restclient.runpod.apigroups.groupsupport import raise_for_status
23
+ from ccai.restclient.runpod.model.runpodgputype import RunpodGpuType
24
+
25
+
26
+ class GpuTypesGroup(RestApiGroup):
27
+ """
28
+ :class:`GpuTypesGroup` exposes the RunPod GPU-type catalog endpoint.
29
+ The catalog is a small, non-paged collection.
30
+ """
31
+
32
+ BASE_LEAF = "/v1"
33
+
34
+ def list(self, *,
35
+ aspects: AspectsRunpod = DEFAULT_RUNPOD_ASPECTS) -> List[RunpodGpuType]:
36
+ """
37
+ List every GPU type RunPod offers.
38
+
39
+ :param aspects: Behaviour control for the call.
40
+
41
+ :returns: A list of :class:`RunpodGpuType` records.
42
+ """
43
+ client = self.client
44
+ leaf = self.build_leaf(parts=["gpuTypes"])
45
+ response = client.request(method="GET", leaf=leaf, aspects=aspects)
46
+ raise_for_status(response=response, action="list gpu types")
47
+
48
+ payload = response.json()
49
+ raw_entries = self._extract_entries(payload=payload)
50
+
51
+ rtnval: List[RunpodGpuType] = []
52
+ for entry in raw_entries:
53
+ rtnval.append(RunpodGpuType.from_dict(entry))
54
+
55
+ return rtnval
56
+
57
+ def _extract_entries(self, *, payload: Any) -> List[Dict[str, Any]]:
58
+ """
59
+ Extract the GPU-type entry list from a response payload.
60
+
61
+ :param payload: The decoded response payload.
62
+
63
+ :returns: The list of raw GPU-type dictionaries.
64
+ """
65
+ if isinstance(payload, list):
66
+ rtnval: List[Dict[str, Any]] = list(payload)
67
+ return rtnval
68
+
69
+ if isinstance(payload, dict):
70
+ entries = payload.get("gpuTypes")
71
+ if entries is None:
72
+ entries = payload.get("items", [])
73
+ rtnval = list(entries)
74
+ return rtnval
75
+
76
+ rtnval = []
77
+ return rtnval
78
+
79
+
80
+ __all__ = ["GpuTypesGroup"]
@@ -0,0 +1,182 @@
1
+ """
2
+ .. module:: groupsupport
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Module that contains shared helpers used by the RunPod
5
+ api-group classes — URL composition and HTTP-error
6
+ reclassification.
7
+
8
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
9
+ """
10
+
11
+ __author__ = "Myron Walker"
12
+ __copyright__ = "Copyright 2026, Code Craft AI LLC"
13
+ __credits__ = []
14
+
15
+
16
+ import os
17
+
18
+ from typing import Any, Callable, Dict, Iterator, List, Optional
19
+ from urllib.parse import quote
20
+
21
+ from requests import Response
22
+
23
+ from ccai.errors.exceptions import SemanticError
24
+
25
+ from ccai.restclient.runpod.exceptions.runpoderrors import (
26
+ RunpodAuthenticationError, RunpodError
27
+ )
28
+
29
+
30
+ def encode_segment(*, segment: str) -> str:
31
+ """
32
+ Percent-encode a single URL path segment.
33
+
34
+ :param segment: The path segment to encode. Must be a non-empty
35
+ string.
36
+
37
+ :returns: The percent-encoded segment.
38
+
39
+ :raises: :class:`SemanticError` when ``segment`` is ``None`` or
40
+ empty.
41
+ """
42
+ if segment is None:
43
+ raise SemanticError("A RunPod URL path segment must not be None.")
44
+ if not isinstance(segment, str):
45
+ raise SemanticError("A RunPod URL path segment must be a string.")
46
+ if len(segment) == 0:
47
+ raise SemanticError("A RunPod URL path segment must not be empty.")
48
+
49
+ rtnval = quote(segment, safe="")
50
+ return rtnval
51
+
52
+
53
+ def compose_leaf(*, base: str, parts: List[str]) -> str:
54
+ """
55
+ Compose a REST leaf path by joining ``base`` with per-segment
56
+ percent-encoded ``parts``.
57
+
58
+ :param base: The base leaf path (e.g. ``"/v1"``).
59
+ :param parts: The path segments to append. Each segment is
60
+ percent-encoded; empty / ``None`` segments are rejected.
61
+
62
+ :returns: The composed leaf path.
63
+
64
+ :raises: :class:`SemanticError` when ``parts`` is ``None`` or holds
65
+ an empty / ``None`` segment.
66
+ """
67
+ if parts is None:
68
+ raise SemanticError("compose_leaf requires a non-None parts list.")
69
+
70
+ encoded_parts: List[str] = []
71
+ for part in parts:
72
+ encoded_parts.append(encode_segment(segment=part))
73
+
74
+ trimmed_base = base
75
+ if trimmed_base.endswith("/"):
76
+ trimmed_base = trimmed_base[:-1]
77
+
78
+ joined = "/".join(encoded_parts)
79
+ rtnval = "{}/{}".format(trimmed_base, joined)
80
+ return rtnval
81
+
82
+
83
+ def raise_for_status(*, response: Response, action: str) -> None:
84
+ """
85
+ Reclassify a non-2xx RunPod response into a typed exception.
86
+
87
+ :param response: The :class:`Response` to inspect.
88
+ :param action: A short description of the attempted action.
89
+
90
+ :returns: ``None``.
91
+
92
+ :raises: :class:`RunpodAuthenticationError` on HTTP 401 / 403;
93
+ :class:`RunpodError` on every other non-2xx status.
94
+ """
95
+ status = response.status_code
96
+ if 200 <= status < 300:
97
+ return
98
+
99
+ err_msg_lines = ["RunPod API call failed: {}.".format(action)]
100
+ err_msg_lines.append("STATUS: {}".format(status))
101
+ err_msg_lines.append("RESPONSE:")
102
+ err_msg_lines.append(response.text)
103
+ err_msg = os.linesep.join(err_msg_lines)
104
+
105
+ if status == 401 or status == 403:
106
+ raise RunpodAuthenticationError(err_msg)
107
+
108
+ raise RunpodError(err_msg)
109
+
110
+
111
+ def safe_json(*, response: Response) -> Dict[str, Any]:
112
+ """
113
+ Parse a response body as a JSON object, degrading to an empty dict.
114
+
115
+ :param response: The :class:`Response` to parse.
116
+
117
+ :returns: The decoded object, or an empty dict.
118
+ """
119
+ try:
120
+ payload = response.json()
121
+ except ValueError:
122
+ rtnval: Dict[str, Any] = {}
123
+ return rtnval
124
+
125
+ if not isinstance(payload, dict):
126
+ rtnval = {}
127
+ return rtnval
128
+
129
+ rtnval = payload
130
+ return rtnval
131
+
132
+
133
+ def iter_cursor_pages(*, fetch_page: Callable[[Optional[str]], Dict[str, Any]],
134
+ items_field: str = "items",
135
+ cursor_field: str = "next") -> Iterator[Dict[str, Any]]:
136
+ """
137
+ Iterate a cursor-paginated RunPod endpoint, yielding each raw item
138
+ dictionary across every page.
139
+
140
+ :param fetch_page: Callable accepting the current cursor token
141
+ (``None`` on the first call) and returning a page dictionary.
142
+ :param items_field: Name of the field holding the page item list.
143
+ :param cursor_field: Name of the field holding the next-page cursor
144
+ token (``None`` / absent / empty-string indicates the final
145
+ page).
146
+
147
+ :returns: An iterator over the raw item dictionaries.
148
+
149
+ :raises: :class:`RunpodError` when the endpoint returns a cursor
150
+ token that has already been seen (cursor cycle).
151
+ """
152
+ cursor: Optional[str] = None
153
+ seen_cursors: List[str] = []
154
+ while True:
155
+ page = fetch_page(cursor)
156
+
157
+ items = page.get(items_field)
158
+ if items is None:
159
+ items = []
160
+ for item in items:
161
+ yield item
162
+
163
+ next_cursor = page.get(cursor_field)
164
+ if next_cursor is None:
165
+ break
166
+ if not isinstance(next_cursor, str):
167
+ break
168
+ if len(next_cursor) == 0:
169
+ break
170
+ if next_cursor in seen_cursors:
171
+ err_msg_lines = ["RunPod cursor pagination detected a cursor cycle."]
172
+ err_msg_lines.append("REPEATED CURSOR: {}".format(next_cursor))
173
+ err_msg = os.linesep.join(err_msg_lines)
174
+ raise RunpodError(err_msg)
175
+
176
+ seen_cursors.append(next_cursor)
177
+ cursor = next_cursor
178
+ return
179
+
180
+
181
+ __all__ = ["encode_segment", "compose_leaf", "raise_for_status", "safe_json",
182
+ "iter_cursor_pages"]
@@ -0,0 +1,156 @@
1
+ """
2
+ .. module:: networkvolumesgroup
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Module that contains the :class:`NetworkVolumesGroup` api
5
+ group exposing the RunPod network-volume endpoints.
6
+
7
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
8
+ """
9
+
10
+ __author__ = "Myron Walker"
11
+ __copyright__ = "Copyright 2026, Code Craft AI LLC"
12
+ __credits__ = []
13
+
14
+
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from ccai.errors.exceptions import SemanticError
18
+
19
+ from ccai.restclient.base.restapigroup import RestApiGroup
20
+
21
+ from ccai.restclient.runpod.aspectsrunpod import (
22
+ AspectsRunpod, DEFAULT_RUNPOD_ASPECTS
23
+ )
24
+ from ccai.restclient.runpod.apigroups.groupsupport import (
25
+ iter_cursor_pages, raise_for_status, safe_json
26
+ )
27
+ from ccai.restclient.runpod.model.runpodnetworkvolume import (
28
+ RunpodNetworkVolume
29
+ )
30
+ from ccai.restclient.runpod.specs.runpodnetworkvolumespec import (
31
+ RunpodNetworkVolumeSpec
32
+ )
33
+
34
+
35
+ class NetworkVolumesGroup(RestApiGroup):
36
+ """
37
+ :class:`NetworkVolumesGroup` exposes the RunPod network-volume
38
+ endpoints — idempotent create, cursor-paged list, and idempotent
39
+ delete.
40
+ """
41
+
42
+ BASE_LEAF = "/v1"
43
+
44
+ def create(self, *, spec: RunpodNetworkVolumeSpec,
45
+ aspects: AspectsRunpod = DEFAULT_RUNPOD_ASPECTS) -> RunpodNetworkVolume:
46
+ """
47
+ Create a network volume. An existing volume carrying the same
48
+ name in the same data center is returned unchanged (idempotent
49
+ create).
50
+
51
+ :param spec: The :class:`RunpodNetworkVolumeSpec` to realise.
52
+ :param aspects: Behaviour control for the call.
53
+
54
+ :returns: A :class:`RunpodNetworkVolume` record.
55
+
56
+ :raises: :class:`SemanticError` when ``spec`` is ``None``.
57
+ """
58
+ if spec is None:
59
+ raise SemanticError("NetworkVolumesGroup.create requires a spec.")
60
+
61
+ existing = self._find_by_name(name=spec.name,
62
+ data_center_id=spec.data_center_id,
63
+ aspects=aspects)
64
+ if existing is not None:
65
+ rtnval = existing
66
+ return rtnval
67
+
68
+ client = self.client
69
+ leaf = self.build_leaf(parts=["networkvolumes"])
70
+ response = client.request(method="POST", leaf=leaf,
71
+ json_body=spec.to_payload(),
72
+ aspects=aspects)
73
+ raise_for_status(response=response, action="create network volume")
74
+
75
+ rtnval = RunpodNetworkVolume.from_dict(safe_json(response=response))
76
+ return rtnval
77
+
78
+ def list(self, *,
79
+ aspects: AspectsRunpod = DEFAULT_RUNPOD_ASPECTS) -> List[RunpodNetworkVolume]:
80
+ """
81
+ List every network volume, following cursor pages transparently.
82
+
83
+ :param aspects: Behaviour control for the call.
84
+
85
+ :returns: A list of :class:`RunpodNetworkVolume` records.
86
+ """
87
+ client = self.client
88
+ leaf = self.build_leaf(parts=["networkvolumes"])
89
+
90
+ def fetch_page(cursor: Optional[str]) -> Dict[str, Any]:
91
+ params: Dict[str, str] = {"limit": str(aspects.page_size)}
92
+ if cursor is not None:
93
+ params["cursor"] = cursor
94
+ response = client.request(method="GET", leaf=leaf,
95
+ params=params, aspects=aspects)
96
+ raise_for_status(response=response, action="list network volumes")
97
+ page = safe_json(response=response)
98
+ return page
99
+
100
+ rtnval: List[RunpodNetworkVolume] = []
101
+ for entry in iter_cursor_pages(fetch_page=fetch_page):
102
+ rtnval.append(RunpodNetworkVolume.from_dict(entry))
103
+
104
+ return rtnval
105
+
106
+ def delete(self, *, volume_id: str,
107
+ aspects: AspectsRunpod = DEFAULT_RUNPOD_ASPECTS) -> None:
108
+ """
109
+ Delete a network volume. Deleting an unknown id is a no-op.
110
+
111
+ :param volume_id: The network-volume identifier to delete.
112
+ :param aspects: Behaviour control for the call.
113
+
114
+ :returns: ``None``.
115
+
116
+ :raises: :class:`SemanticError` when ``volume_id`` is empty.
117
+ """
118
+ if volume_id is None or len(volume_id) == 0:
119
+ raise SemanticError("'volume_id' must be a non-empty string.")
120
+
121
+ client = self.client
122
+ leaf = self.build_leaf(parts=["networkvolumes", volume_id])
123
+ response = client.request(method="DELETE", leaf=leaf, aspects=aspects)
124
+
125
+ status = response.status_code
126
+ if 200 <= status < 300:
127
+ return
128
+ if status == 404:
129
+ return
130
+
131
+ raise_for_status(response=response, action="delete network volume")
132
+ return
133
+
134
+ def _find_by_name(self, *, name: str, data_center_id: str,
135
+ aspects: AspectsRunpod) -> Optional[RunpodNetworkVolume]:
136
+ """
137
+ Find an existing network volume by name and data center.
138
+
139
+ :param name: The volume name to match.
140
+ :param data_center_id: The data center to match.
141
+ :param aspects: Behaviour control for the call.
142
+
143
+ :returns: The matching :class:`RunpodNetworkVolume`, or ``None``.
144
+ """
145
+ volumes = self.list(aspects=aspects)
146
+ for volume in volumes:
147
+ if volume.name == name:
148
+ if volume.data_center_id == data_center_id:
149
+ rtnval: Optional[RunpodNetworkVolume] = volume
150
+ return rtnval
151
+
152
+ rtnval = None
153
+ return rtnval
154
+
155
+
156
+ __all__ = ["NetworkVolumesGroup"]