python-rundeck 0.1.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.
@@ -0,0 +1,327 @@
1
+ """
2
+ Gestion des exécutions Rundeck
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from rundeck.base import RundeckObject, RundeckObjectManager
8
+
9
+
10
+ class Execution(RundeckObject):
11
+ """Représente une exécution Rundeck"""
12
+
13
+ _id_attr = "id"
14
+
15
+ def abort(self, asUser: str | None = None) -> dict[str, Any]:
16
+ """
17
+ Annule l'exécution
18
+
19
+ Args:
20
+ asUser: Annuler en tant qu'utilisateur
21
+
22
+ Returns:
23
+ Statut de l'annulation
24
+ """
25
+ path = f"/execution/{self.id}/abort"
26
+
27
+ data = {}
28
+ if asUser:
29
+ data["asUser"] = asUser
30
+
31
+ return self.manager.rd.http_get(path, params=data)
32
+
33
+ def get_output(
34
+ self,
35
+ offset: int = 0,
36
+ lastlines: int | None = None,
37
+ lastmod: int | None = None,
38
+ maxlines: int | None = None,
39
+ ) -> dict[str, Any]:
40
+ """
41
+ Récupère la sortie de l'exécution
42
+
43
+ Args:
44
+ offset: Offset dans la sortie
45
+ lastlines: Nombre de dernières lignes
46
+ lastmod: Timestamp de dernière modification
47
+ maxlines: Nombre maximum de lignes
48
+
49
+ Returns:
50
+ Sortie de l'exécution
51
+ """
52
+ path = f"/execution/{self.id}/output"
53
+
54
+ params: dict[str, Any] = {"offset": offset}
55
+ if lastlines:
56
+ params["lastlines"] = lastlines
57
+ if lastmod:
58
+ params["lastmod"] = lastmod
59
+ if maxlines:
60
+ params["maxlines"] = maxlines
61
+
62
+ return self.manager.rd.http_get(path, params=params)
63
+
64
+ def get_state(self) -> dict[str, Any]:
65
+ """
66
+ Récupère l'état de l'exécution
67
+
68
+ Returns:
69
+ État de l'exécution
70
+ """
71
+ path = f"/execution/{self.id}/state"
72
+ return self.manager.rd.http_get(path)
73
+
74
+ def delete(self) -> None:
75
+ """Supprime l'exécution"""
76
+ self.manager.delete(self.id)
77
+
78
+ def is_running(self) -> bool:
79
+ """
80
+ Vérifie si l'exécution est en cours
81
+
82
+ Returns:
83
+ True si en cours, False sinon
84
+ """
85
+ return self._attrs.get("status") == "running"
86
+
87
+ def is_succeeded(self) -> bool:
88
+ """
89
+ Vérifie si l'exécution a réussi
90
+
91
+ Returns:
92
+ True si réussie, False sinon
93
+ """
94
+ return self._attrs.get("status") == "succeeded"
95
+
96
+ def is_failed(self) -> bool:
97
+ """
98
+ Vérifie si l'exécution a échoué
99
+
100
+ Returns:
101
+ True si échouée, False sinon
102
+ """
103
+ return self._attrs.get("status") in ("failed", "failed-with-retry")
104
+
105
+ def is_aborted(self) -> bool:
106
+ """
107
+ Vérifie si l'exécution a été annulée
108
+
109
+ Returns:
110
+ True si annulée, False sinon
111
+ """
112
+ return self._attrs.get("status") == "aborted"
113
+
114
+ def refresh(self) -> "Execution":
115
+ """
116
+ Actualise les données de l'exécution
117
+
118
+ Returns:
119
+ Exécution actualisée
120
+ """
121
+ updated = self.manager.get(self.id)
122
+ self._attrs = updated._attrs
123
+ return self
124
+
125
+
126
+ class ExecutionManager(RundeckObjectManager):
127
+ """Manager pour les exécutions"""
128
+
129
+ _path = "/executions"
130
+ _obj_cls = Execution
131
+
132
+ def list(
133
+ self,
134
+ project: str | None = None,
135
+ status: str | None = None,
136
+ max: int | None = None,
137
+ offset: int | None = None,
138
+ **kwargs: Any,
139
+ ) -> "list[Execution]":
140
+ """
141
+ Liste les exécutions
142
+
143
+ Args:
144
+ project: Filtre par projet
145
+ status: Filtre par statut
146
+ max: Nombre maximum de résultats
147
+ offset: Offset de pagination
148
+ **kwargs: Autres paramètres de filtrage
149
+
150
+ Returns:
151
+ Liste d'exécutions
152
+ """
153
+ project_name = project or (
154
+ self.parent.id if getattr(self, "parent", None) else None
155
+ )
156
+ if project_name:
157
+ path = f"/project/{project_name}/executions"
158
+ else:
159
+ path = self._build_path()
160
+
161
+ params: dict[str, Any] = {}
162
+ if status:
163
+ params["status"] = status
164
+ if max:
165
+ params["max"] = max
166
+ if offset:
167
+ params["offset"] = offset
168
+ params.update(kwargs)
169
+
170
+ return self._list(path=path, params=params or None)
171
+
172
+ def running(self, project: str | None = None) -> "list[Execution]":
173
+ """
174
+ Liste les exécutions en cours
175
+
176
+ Args:
177
+ project: Filtre par projet (ou '*' pour tous)
178
+
179
+ Returns:
180
+ Liste d'exécutions en cours
181
+ """
182
+ project_name = project or (
183
+ self.parent.id if getattr(self, "parent", None) else None
184
+ )
185
+ if project_name:
186
+ path = f"/project/{project_name}/executions/running"
187
+ else:
188
+ path = "/project/*/executions/running"
189
+
190
+ return self._list(path=path)
191
+
192
+ def get(self, id: str) -> Execution:
193
+ """
194
+ Récupère une exécution par son ID
195
+
196
+ Args:
197
+ id: ID de l'exécution
198
+
199
+ Returns:
200
+ Exécution
201
+ """
202
+ path = f"/execution/{id}"
203
+ return self._get(id, path=path)
204
+
205
+ def delete(self, id: str) -> None:
206
+ """
207
+ Supprime une exécution
208
+
209
+ Args:
210
+ id: ID de l'exécution
211
+ """
212
+ path = f"/execution/{id}"
213
+ self._delete(id, path=path)
214
+
215
+ def bulk_delete(self, ids: "list[str]") -> dict[str, Any]:
216
+ """
217
+ Supprime plusieurs exécutions
218
+
219
+ Args:
220
+ ids: Liste d'IDs d'exécutions
221
+
222
+ Returns:
223
+ Résultat de la suppression
224
+ """
225
+ path = self._build_path("delete")
226
+ data = {"ids": ids}
227
+ return self.rd.http_post(path, json=data)
228
+
229
+ def query(
230
+ self,
231
+ project: str,
232
+ statusFilter: str | None = None,
233
+ abortedbyFilter: str | None = None,
234
+ userFilter: str | None = None,
235
+ recentFilter: str | None = None,
236
+ olderFilter: str | None = None,
237
+ begin: str | None = None,
238
+ end: str | None = None,
239
+ adhoc: bool | None = None,
240
+ jobIdListFilter: "list[str] | None" = None,
241
+ excludeJobIdListFilter: "list[str] | None" = None,
242
+ jobListFilter: "list[str] | None" = None,
243
+ excludeJobListFilter: "list[str] | None" = None,
244
+ groupPath: str | None = None,
245
+ groupPathExact: str | None = None,
246
+ excludeGroupPath: str | None = None,
247
+ excludeGroupPathExact: str | None = None,
248
+ jobExactFilter: str | None = None,
249
+ excludeJobExactFilter: str | None = None,
250
+ max: int | None = None,
251
+ offset: int | None = None,
252
+ ) -> "list[Execution]":
253
+ """
254
+ Requête avancée sur les exécutions
255
+
256
+ Args:
257
+ project: Nom du projet
258
+ statusFilter: Filtre par statut
259
+ abortedbyFilter: Filtre par utilisateur ayant annulé
260
+ userFilter: Filtre par utilisateur
261
+ recentFilter: Filtre par période récente
262
+ olderFilter: Filtre par période ancienne
263
+ begin: Date de début
264
+ end: Date de fin
265
+ adhoc: Exécutions adhoc uniquement
266
+ jobIdListFilter: Liste d'IDs de jobs
267
+ excludeJobIdListFilter: Exclure les IDs de jobs
268
+ jobListFilter: Liste de noms de jobs
269
+ excludeJobListFilter: Exclure les noms de jobs
270
+ groupPath: Chemin de groupe
271
+ groupPathExact: Chemin de groupe exact
272
+ excludeGroupPath: Exclure le chemin de groupe
273
+ excludeGroupPathExact: Exclure le chemin de groupe exact
274
+ jobExactFilter: Nom exact du job
275
+ excludeJobExactFilter: Exclure le nom exact du job
276
+ max: Nombre maximum de résultats
277
+ offset: Offset de pagination
278
+
279
+ Returns:
280
+ Liste d'exécutions
281
+ """
282
+ path = f"/project/{project}/executions"
283
+
284
+ params: dict[str, Any] = {}
285
+ if statusFilter:
286
+ params["statusFilter"] = statusFilter
287
+ if abortedbyFilter:
288
+ params["abortedbyFilter"] = abortedbyFilter
289
+ if userFilter:
290
+ params["userFilter"] = userFilter
291
+ if recentFilter:
292
+ params["recentFilter"] = recentFilter
293
+ if olderFilter:
294
+ params["olderFilter"] = olderFilter
295
+ if begin:
296
+ params["begin"] = begin
297
+ if end:
298
+ params["end"] = end
299
+ if adhoc is not None:
300
+ params["adhoc"] = "true" if adhoc else "false"
301
+ if jobIdListFilter:
302
+ params["jobIdListFilter"] = ",".join(jobIdListFilter)
303
+ if excludeJobIdListFilter:
304
+ params["excludeJobIdListFilter"] = ",".join(excludeJobIdListFilter)
305
+ if jobListFilter:
306
+ params["jobListFilter"] = ",".join(jobListFilter)
307
+ if excludeJobListFilter:
308
+ params["excludeJobListFilter"] = ",".join(excludeJobListFilter)
309
+ if groupPath:
310
+ params["groupPath"] = groupPath
311
+ if groupPathExact:
312
+ params["groupPathExact"] = groupPathExact
313
+ if excludeGroupPath:
314
+ params["excludeGroupPath"] = excludeGroupPath
315
+ if excludeGroupPathExact:
316
+ params["excludeGroupPathExact"] = excludeGroupPathExact
317
+ if jobExactFilter:
318
+ params["jobExactFilter"] = jobExactFilter
319
+ if excludeJobExactFilter:
320
+ params["excludeJobExactFilter"] = excludeJobExactFilter
321
+ if max:
322
+ params["max"] = max
323
+ if offset:
324
+ params["offset"] = offset
325
+
326
+ result = self.rd.http_list(path, params=params)
327
+ return [self._obj_cls(self, item) for item in result]
@@ -0,0 +1,44 @@
1
+ """
2
+ Gestion des system features (/feature).
3
+ """
4
+
5
+ from rundeck.base import RundeckObject, RundeckObjectManager
6
+
7
+
8
+ class Feature(RundeckObject):
9
+ """Représente un feature flag système."""
10
+
11
+ _id_attr = "name"
12
+ _repr_attr = "name"
13
+
14
+
15
+ class FeatureManager(RundeckObjectManager[Feature]):
16
+ """Manager pour interroger les features système."""
17
+
18
+ _path = "/feature"
19
+ _obj_cls = Feature
20
+
21
+ def list(self) -> list[Feature]:
22
+ """Liste le statut de tous les features système."""
23
+ base = self._build_path()
24
+ path = f"{base.rstrip('/')}/"
25
+ result = self.rd.http_get(path)
26
+ items = []
27
+ if isinstance(result, dict):
28
+ for name, value in result.items():
29
+ if isinstance(value, dict):
30
+ items.append({"name": name, **(value or {})})
31
+ else:
32
+ items.append({"name": name, "enabled": bool(value)})
33
+ return self._wrap_list(items)
34
+
35
+ def get(self, name: str) -> Feature:
36
+ """Récupère le statut d'un feature spécifique."""
37
+ path = self._build_path(name)
38
+ data = self.rd.http_get(path)
39
+ if isinstance(data, dict):
40
+ data.setdefault("name", name)
41
+ return self._wrap(data)
42
+
43
+
44
+ __all__ = ["Feature", "FeatureManager"]
@@ -0,0 +1,329 @@
1
+ """
2
+ Gestion des jobs Rundeck
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from rundeck.base import RundeckObject, RundeckObjectManager
8
+ from rundeck.v1.objects.executions import Execution
9
+ from rundeck.v1.objects.scm import JobSCMManager
10
+
11
+
12
+ class Job(RundeckObject):
13
+ """Représente un job Rundeck."""
14
+
15
+ _id_attr = "id"
16
+ _repr_attr = "name"
17
+
18
+ def _job_path(self, suffix: str | None = None) -> str:
19
+ """Construit le chemin de base /job/{id} avec suffixe optionnel."""
20
+ base = f"/job/{self.id}"
21
+ if suffix:
22
+ return f"{base}/{suffix.lstrip('/')}"
23
+ return base
24
+
25
+ def run(self, **kwargs: Any) -> Execution:
26
+ """
27
+ Exécute le job et retourne l'exécution.
28
+ """
29
+ path = self._job_path("run")
30
+ result = self.rd.http_post(path, json=kwargs or None)
31
+ # Wrap avec le manager d'exécutions existant.
32
+ return Execution(self.manager.rd.executions, result)
33
+
34
+ def retry(self, execution_id: str, **kwargs: Any) -> Execution:
35
+ """
36
+ Relance une exécution (retry).
37
+ """
38
+ path = self._job_path(f"retry/{execution_id}")
39
+ result = self.rd.http_post(path, json=kwargs or None)
40
+ return Execution(self.manager.rd.executions, result)
41
+
42
+ def delete(self) -> None:
43
+ """Supprime le job courant."""
44
+ self.manager.delete(self.id)
45
+
46
+ def definition(self, format: str = "json") -> Any:
47
+ """Récupère la définition du job dans le format demandé."""
48
+ path = self._job_path()
49
+ return self.rd.http_get(path, params={"format": format})
50
+
51
+ def enable_execution(self) -> Any:
52
+ """Active l'exécution pour ce job."""
53
+ path = self._job_path("execution/enable")
54
+ return self.rd.http_post(path)
55
+
56
+ def disable_execution(self) -> Any:
57
+ """Désactive l'exécution pour ce job."""
58
+ path = self._job_path("execution/disable")
59
+ return self.rd.http_post(path)
60
+
61
+ def enable_schedule(self) -> Any:
62
+ """Active la planification pour ce job."""
63
+ path = self._job_path("schedule/enable")
64
+ return self.rd.http_post(path)
65
+
66
+ def disable_schedule(self) -> Any:
67
+ """Désactive la planification pour ce job."""
68
+ path = self._job_path("schedule/disable")
69
+ return self.rd.http_post(path)
70
+
71
+ def info(self) -> dict[str, Any]:
72
+ """Récupère les métadonnées du job."""
73
+ path = self._job_path("info")
74
+ return self.rd.http_get(path)
75
+
76
+ def workflow(self) -> dict[str, Any]:
77
+ """Récupère le workflow du job."""
78
+ path = self._job_path("workflow")
79
+ return self.rd.http_get(path)
80
+
81
+ def meta(self, meta: str | None = None) -> list[dict[str, Any]]:
82
+ """
83
+ Récupère les métadonnées UI du job (endpoint /job/{id}/meta).
84
+
85
+ Args:
86
+ meta: Liste de métadonnées à inclure (ex: "name,description")
87
+ ou "*" pour tout.
88
+ """
89
+ params = {"meta": meta} if meta else None
90
+ path = self._job_path("meta")
91
+ return self.rd.http_get(path, params=params)
92
+
93
+ def tags(self) -> list[str]:
94
+ """Retourne les tags du job (commercial, endpoint /job/{id}/tags)."""
95
+ path = self._job_path("tags")
96
+ return self.rd.http_get(path)
97
+
98
+ def upload_option_file(
99
+ self,
100
+ option_name: str,
101
+ content: bytes | str,
102
+ file_name: str | None = None,
103
+ ) -> dict[str, Any]:
104
+ """
105
+ Upload d'un fichier pour une option (single file).
106
+ """
107
+ path = self._job_path("input/file")
108
+ params = {"optionName": option_name}
109
+ if file_name:
110
+ params["fileName"] = file_name
111
+ response = self.rd.http_post(
112
+ path,
113
+ params=params,
114
+ data=content,
115
+ headers={
116
+ "Content-Type": "application/octet-stream",
117
+ "Accept": "application/json",
118
+ },
119
+ raw=True,
120
+ )
121
+ return response.json()
122
+
123
+ def upload_option_files(
124
+ self, files: dict[str, tuple[str, bytes | Any]]
125
+ ) -> dict[str, Any]:
126
+ """
127
+ Upload multi-fichiers pour des options (multipart/form-data).
128
+
129
+ Args:
130
+ files: mapping option_name -> (file_name, content)
131
+ """
132
+ multipart: dict[str, tuple[str, bytes | Any]] = {
133
+ f"option.{name}": (fname, data) for name, (fname, data) in files.items()
134
+ }
135
+ path = self._job_path("input/file")
136
+ response = self.rd.http_post(
137
+ path,
138
+ files=multipart,
139
+ headers={"Accept": "application/json"},
140
+ raw=True,
141
+ )
142
+ return response.json()
143
+
144
+ def list_uploaded_files(
145
+ self,
146
+ max: int | None = None,
147
+ offset: int | None = None,
148
+ file_state: str | None = None,
149
+ ) -> dict[str, Any]:
150
+ """
151
+ Liste les fichiers uploadés pour ce job.
152
+ """
153
+ params: dict[str, Any] = {}
154
+ if max is not None:
155
+ params["max"] = max
156
+ if offset is not None:
157
+ params["offset"] = offset
158
+ if file_state:
159
+ params["fileState"] = file_state
160
+ path = f"/job/{self.id}/input/files"
161
+ return self.rd.http_get(path, params=params or None)
162
+
163
+ def forecast(
164
+ self, time: str | None = None, max: int | None = None
165
+ ) -> dict[str, Any]:
166
+ """Prévision du planning du job (scheduler/forecast)."""
167
+ params: dict[str, Any] = {}
168
+ if time:
169
+ params["time"] = time
170
+ if max:
171
+ params["max"] = max
172
+ path = f"/scheduler/{self._job_path('forecast').lstrip('/')}"
173
+ return self.rd.http_get(path, params=params)
174
+
175
+ @property
176
+ def scm(self) -> JobSCMManager:
177
+ """Accès aux opérations SCM pour ce job."""
178
+ return JobSCMManager(self.rd, parent=self)
179
+
180
+
181
+ class JobBulkManager(RundeckObjectManager[Job]):
182
+ """Opérations bulk sur les jobs (/jobs/*)."""
183
+
184
+ _path = "/jobs"
185
+ _obj_cls = Job
186
+
187
+ def enable_execution(self, ids: "list[str]") -> Any:
188
+ """Active l'exécution pour plusieurs jobs."""
189
+ data = {"ids": ids}
190
+ path = self._build_path("execution/enable")
191
+ return self.rd.http_put(path, json=data)
192
+
193
+ def disable_execution(self, ids: "list[str]") -> Any:
194
+ """Désactive l'exécution pour plusieurs jobs."""
195
+ data = {"ids": ids}
196
+ path = self._build_path("execution/disable")
197
+ return self.rd.http_put(path, json=data)
198
+
199
+ def enable_schedule(self, ids: "list[str]") -> Any:
200
+ """Active la planification pour plusieurs jobs."""
201
+ data = {"ids": ids}
202
+ path = self._build_path("schedule/enable")
203
+ return self.rd.http_post(path, json=data)
204
+
205
+ def disable_schedule(self, ids: "list[str]") -> Any:
206
+ """Désactive la planification pour plusieurs jobs."""
207
+ data = {"ids": ids}
208
+ path = self._build_path("schedule/disable")
209
+ return self.rd.http_post(path, json=data)
210
+
211
+ def delete(self, ids: "list[str]") -> Any:
212
+ """Supprime plusieurs jobs en une requête."""
213
+ data = {"ids": ids}
214
+ path = self._build_path("delete")
215
+ return self.rd.http_delete(path, json=data)
216
+
217
+
218
+ class JobManager(RundeckObjectManager[Job]):
219
+ """Manager pour les jobs."""
220
+
221
+ _path = "/project/{parent}/jobs"
222
+ _obj_cls = Job
223
+
224
+ def __init__(self, rd: Any, parent: RundeckObject | None = None) -> None:
225
+ super().__init__(rd, parent)
226
+ self.bulk = JobBulkManager(rd)
227
+
228
+ def __getattr__(self, name: str) -> Any:
229
+ # Alias pour permettre rd.jobs.import(...) malgré le mot-clé Python.
230
+ if name == "import":
231
+ return self.import_jobs
232
+ return super().__getattribute__(name)
233
+
234
+ def list(
235
+ self,
236
+ project: str | None = None,
237
+ **filters: Any,
238
+ ) -> "list[Job]":
239
+ """Liste les jobs d'un projet."""
240
+ project_name = project or (self.parent.id if self.parent else None)
241
+ # Avec un parent Project, on réutilise le manager parenté ; sinon on exige
242
+ # le nom du projet.
243
+ if not project_name:
244
+ raise ValueError("Le paramètre project est requis")
245
+
246
+ # Si le manager est parenté (project.jobs), _build_path() résout le
247
+ # placeholder {parent}.
248
+ # Sinon, on formate le chemin collection à partir du nom de projet fourni.
249
+ path = (
250
+ self._build_path()
251
+ if self.parent and not project
252
+ else self._path.format(parent=project_name)
253
+ )
254
+ return self._list(path=path, params=filters)
255
+
256
+ def get(self, id: str) -> "Job":
257
+ """Récupère un job par identifiant."""
258
+ path = f"/job/{id}"
259
+ return self._get(id, path=path)
260
+
261
+ def export(
262
+ self,
263
+ project: str | None = None,
264
+ format: str = "json",
265
+ idlist: str | None = None,
266
+ groupPath: str | None = None,
267
+ jobFilter: str | None = None,
268
+ ) -> Any:
269
+ """Exporte les jobs d'un projet (json/xml/yaml)."""
270
+ project_name = project or (self.parent.id if self.parent else None)
271
+ if not project_name:
272
+ raise ValueError("Le paramètre project est requis")
273
+
274
+ params: dict[str, Any] = {"format": format} if format else {}
275
+ if idlist:
276
+ params["idlist"] = idlist
277
+ if groupPath:
278
+ params["groupPath"] = groupPath
279
+ if jobFilter:
280
+ params["jobFilter"] = jobFilter
281
+
282
+ path = (
283
+ self._build_path("export")
284
+ if self.parent and not project
285
+ else f"/project/{project_name}/jobs/export"
286
+ )
287
+ return self.rd.http_get(path, params=params or None)
288
+
289
+ def import_jobs(
290
+ self,
291
+ project: str | None = None,
292
+ content: Any = None,
293
+ fileformat: str = "json",
294
+ dupeOption: str | None = None,
295
+ uuidOption: str | None = None,
296
+ content_type: str = "application/octet-stream",
297
+ ) -> Any:
298
+ """Importe des jobs (json/xml/yaml) dans un projet."""
299
+ project_name = project or (self.parent.id if self.parent else None)
300
+ if not project_name:
301
+ raise ValueError("Le paramètre project est requis")
302
+
303
+ params: dict[str, Any] = {"fileformat": fileformat} if fileformat else {}
304
+ if dupeOption:
305
+ params["dupeOption"] = dupeOption
306
+ if uuidOption:
307
+ params["uuidOption"] = uuidOption
308
+
309
+ path = (
310
+ self._build_path("import")
311
+ if self.parent and not project
312
+ else f"/project/{project_name}/jobs/import"
313
+ )
314
+ return self.rd.http_post(
315
+ path,
316
+ params=params or None,
317
+ data=content,
318
+ headers={"Content-Type": content_type},
319
+ )
320
+
321
+ def delete(self, id: str) -> None:
322
+ """Supprime un job par identifiant."""
323
+ path = f"/job/{id}"
324
+ self._delete(id, path=path)
325
+
326
+ def get_uploaded_file_info(self, file_id: str) -> dict[str, Any]:
327
+ """Récupère les métadonnées d'un fichier uploadé (par ID)."""
328
+ path = f"/jobs/file/{file_id}"
329
+ return self.rd.http_get(path)