python-rundeck 0.1.0__tar.gz

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 (28) hide show
  1. python_rundeck-0.1.0/LICENSE +21 -0
  2. python_rundeck-0.1.0/PKG-INFO +454 -0
  3. python_rundeck-0.1.0/README.md +420 -0
  4. python_rundeck-0.1.0/pyproject.toml +70 -0
  5. python_rundeck-0.1.0/src/rundeck/__init__.py +39 -0
  6. python_rundeck-0.1.0/src/rundeck/base.py +231 -0
  7. python_rundeck-0.1.0/src/rundeck/client.py +341 -0
  8. python_rundeck-0.1.0/src/rundeck/config.py +166 -0
  9. python_rundeck-0.1.0/src/rundeck/const.py +45 -0
  10. python_rundeck-0.1.0/src/rundeck/exceptions.py +164 -0
  11. python_rundeck-0.1.0/src/rundeck/py.typed +1 -0
  12. python_rundeck-0.1.0/src/rundeck/v1/__init__.py +1 -0
  13. python_rundeck-0.1.0/src/rundeck/v1/objects/__init__.py +1 -0
  14. python_rundeck-0.1.0/src/rundeck/v1/objects/adhoc.py +93 -0
  15. python_rundeck-0.1.0/src/rundeck/v1/objects/config_management.py +51 -0
  16. python_rundeck-0.1.0/src/rundeck/v1/objects/executions.py +327 -0
  17. python_rundeck-0.1.0/src/rundeck/v1/objects/features.py +44 -0
  18. python_rundeck-0.1.0/src/rundeck/v1/objects/jobs.py +329 -0
  19. python_rundeck-0.1.0/src/rundeck/v1/objects/key_storage.py +79 -0
  20. python_rundeck-0.1.0/src/rundeck/v1/objects/metrics.py +37 -0
  21. python_rundeck-0.1.0/src/rundeck/v1/objects/plugins.py +38 -0
  22. python_rundeck-0.1.0/src/rundeck/v1/objects/projects.py +421 -0
  23. python_rundeck-0.1.0/src/rundeck/v1/objects/scheduler.py +50 -0
  24. python_rundeck-0.1.0/src/rundeck/v1/objects/scm.py +229 -0
  25. python_rundeck-0.1.0/src/rundeck/v1/objects/system.py +140 -0
  26. python_rundeck-0.1.0/src/rundeck/v1/objects/tokens.py +102 -0
  27. python_rundeck-0.1.0/src/rundeck/v1/objects/users.py +137 -0
  28. python_rundeck-0.1.0/src/rundeck/v1/objects/webhooks.py +104 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pascal Seckinger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,454 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-rundeck
3
+ Version: 0.1.0
4
+ Summary: Python client for the Rundeck API (v14-v56).
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: rundeck,api,client
8
+ Author: Pascal Seckinger
9
+ Author-email: pascal.seckinger@protonmail.com
10
+ Requires-Python: >=3.11
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Classifier: Typing :: Typed
22
+ Provides-Extra: dev
23
+ Requires-Dist: bandit (>=1.7.9,<2.0.0) ; extra == "dev"
24
+ Requires-Dist: black (>=25.11.0,<26.0.0) ; extra == "dev"
25
+ Requires-Dist: flake8 (>=7.0.0,<8.0.0) ; extra == "dev"
26
+ Requires-Dist: isort (>=7.0.0,<8.0.0) ; extra == "dev"
27
+ Requires-Dist: pytest (>=9.0.2,<10.0.0) ; extra == "dev"
28
+ Requires-Dist: pytest-cov (>=5.0.0,<6.0.0) ; extra == "dev"
29
+ Requires-Dist: requests (>=2.32.5,<3.0.0)
30
+ Requires-Dist: ruff (>=0.7.0,<1.0.0) ; extra == "dev"
31
+ Requires-Dist: safety (>=3.2.0,<4.0.0) ; extra == "dev"
32
+ Description-Content-Type: text/markdown
33
+
34
+ python-rundeck
35
+ ===============
36
+
37
+ Client Python pour l’API Rundeck (v14–v56), inspiré de l’architecture de python-gitlab. Fournit des managers typés pour les ressources clés (projects, jobs, executions, tokens, users, système, configuration) et SCM (import/export).
38
+
39
+ Sommaire
40
+ --------
41
+ - Installation
42
+ - Démarrage rapide
43
+ - Configuration
44
+ - Ressources disponibles
45
+ - Exemples par ressource
46
+ - Gestion des erreurs
47
+ - Développement et tests
48
+
49
+ Installation
50
+ ------------
51
+ Prerequis : Python 3.11+. Le projet utilise Poetry.
52
+
53
+ ```bash
54
+ poetry install
55
+ # ou en editable avec pip
56
+ pip install -e .
57
+ ```
58
+
59
+ Démarrage rapide
60
+ ----------------
61
+ ```python
62
+ from rundeck.client import Rundeck
63
+
64
+ rd = Rundeck(url="https://rundeck.example.com", token="MY_TOKEN", api_version=56)
65
+ # Auth par mot de passe (session cookie) si pas de token
66
+ rd = Rundeck(url="https://rundeck.example.com", username="admin", password="admin", api_version=56)
67
+
68
+ # Lister les projets
69
+ projects = rd.projects.list()
70
+ for p in projects:
71
+ print(p.name)
72
+
73
+ # Récupérer un projet et lancer un job
74
+ project = rd.projects.get("demo")
75
+ jobs = project.jobs.list()
76
+ execu = jobs[0].run()
77
+ print(execu.id)
78
+ ```
79
+
80
+ Configuration
81
+ -------------
82
+ La configuration suit un modèle en cascade (args > env > fichiers > défauts) via `RundeckConfig`.
83
+
84
+ Paramètres principaux :
85
+ - `url` : URL Rundeck (ex. `https://rundeck.example.com`)
86
+ - `token` : Token API (en-tête `X-Rundeck-Auth-Token`)
87
+ - `username` / `password` : Authentification par session (j_security_check) si aucun token n'est fourni.
88
+ - `api_version` : Version d’API (ex. `56`)
89
+ - `timeout` : Timeout des requêtes (float, secondes)
90
+ - `ssl_verify` : Vérification TLS (bool ou chemin CA)
91
+
92
+ Fichiers de config : si besoin, passez `config_files` ou `Rundeck.from_config(config_section=...)`.
93
+ Variables d'env utiles (config client) :
94
+ - `RUNDECK_URL` : URL de base
95
+ - `RUNDECK_TOKEN` : token API (en-tête `X-Rundeck-Auth-Token`)
96
+ - `RUNDECK_USERNAME` / `RUNDECK_PASSWORD` : auth session (si pas de token)
97
+ - `RUNDECK_API_VERSION` : version d'API (ex: 56)
98
+ - `RUNDECK_TIMEOUT` : timeout des requêtes (secondes)
99
+ - `RUNDECK_SSL_VERIFY` : vérification TLS (bool ou chemin CA)
100
+ - `RUNDECK_USER_AGENT` : User-Agent HTTP
101
+
102
+ Ressources disponibles
103
+ ----------------------
104
+ - `projects` (`ProjectManager`) : CRUD projets, export/import de jobs, config projet, archive (export/import).
105
+ - `jobs` (`JobManager`) : liste, get, suppressions, actions bulk, exécution.
106
+ - `executions` (`ExecutionManager`) : liste/filtre, running, get/delete, query avancée.
107
+ - `tokens` (`TokenManager`) : liste, get, create, delete.
108
+ - `users` (`UserManager`) : opérations utilisateurs (selon implémentation courante).
109
+ - `metrics` (`MetricsManager`) : endpoints `/metrics` (list/data/healthcheck/ping).
110
+ - `plugins` (`PluginManager`) : liste des plugins installés (`/plugin/list`).
111
+ - `webhooks` (`WebhookEventManager` + `ProjectWebhookManager`) : gestion des webhooks projet et envoi via token.
112
+ - `key_storage` (`StorageKeyManager`) : gestion du stockage des clés `/storage/keys`.
113
+ - `adhoc` (`AdhocManager`, via `project.adhoc`) : exécution de commandes/scripts AdHoc.
114
+ - `system` (`SystemManager`) : info système, exécutions enable/disable, logstorage, ACL.
115
+ - `config_management` (`ConfigManagementManager`) : configuration globale `/config`.
116
+ - `scm` (via `project.scm` et `job.scm`) : plugins import/export, setup, enable/disable, statut, actions (commit/import/export...).
117
+
118
+ Exemples par ressource (complets)
119
+ ---------------------------------
120
+ Projets
121
+ ```python
122
+ # CRUD projet
123
+ p = rd.projects.create("demo")
124
+ p = rd.projects.get("demo")
125
+ rd.projects.delete("demo")
126
+ projects = rd.projects.list()
127
+
128
+ # Export / import de jobs d'un projet (via le manager de jobs)
129
+ project.jobs.export(format="json", idlist="id1,id2", groupPath="group/sub")
130
+ project.jobs.import_jobs(content=open("jobs.json").read(), format="json", dupeOption="update", uuidOption="remove")
131
+
132
+ # Export / import du projet (archive ZIP)
133
+ archive = project.archive
134
+ resp = archive.export(export_all=False, export_webhooks=True) # Response brute (zip)
135
+ token_info = archive.export_async(exportAll=True)
136
+ status = archive.export_status(token_info.get("token", ""))
137
+ zip_resp = archive.export_download(token_info.get("token", ""))
138
+
139
+ # Import d'archive (synchrone ou async)
140
+ archive.import_archive(
141
+ content=open("project-export.zip", "rb").read(),
142
+ jobUuidOption="preserve",
143
+ importExecutions=True,
144
+ importConfig=True,
145
+ )
146
+ archive.import_archive(content=open("project-export.zip", "rb").read(), async_import=True)
147
+ archive.import_status()
148
+
149
+ # Readme / MOTD du projet
150
+ project.readme.get_readme() # texte par défaut
151
+ project.readme.get_readme(accept="application/json")
152
+ project.readme.update_readme("Nouveau contenu", content_type="text/plain")
153
+ project.readme.delete_readme()
154
+ project.readme.get_motd()
155
+ project.readme.update_motd("Message du jour", content_type="text/plain")
156
+ project.readme.delete_motd()
157
+
158
+ # Config projet (clé/valeur)
159
+ conf = p.config.get()
160
+ p.config.keys.get("project.label")
161
+ p.config.keys.set("project.label", "Demo")
162
+ p.config.keys.update({"project.description": "Sample"})
163
+ p.config.replace({"project.label": "Demo", "project.description": "Sample"})
164
+ p.config.keys.delete("project.label")
165
+
166
+ # SCM import/export (sur un projet)
167
+ scm_import = project.scm.import_ # ou getattr(project.scm, "import")
168
+ scm_export = project.scm.export
169
+
170
+ # Découverte des plugins
171
+ scm_import.plugins.list()
172
+ scm_export.plugins.list()
173
+
174
+ # Champs d'entrée pour un plugin et setup (plugin_type explicite)
175
+ fields = scm_import.plugins.input_fields("git-import")
176
+ scm_import.config.setup("git-import", {"url": "ssh://git@example.com/repo.git", "dir": "/tmp/repo"})
177
+
178
+ # Activer/désactiver un plugin
179
+ scm_import.config.enable("git-import")
180
+ scm_export.config.disable("custom-export")
181
+
182
+ # Statut/config SCM
183
+ import_status = scm_import.actions.status()
184
+ export_conf = scm_export.config.get()
185
+
186
+ # Actions SCM côté projet (ex: commit/pull/push selon plugin)
187
+ action_fields = scm_export.actions.input_fields("commit")
188
+ scm_export.actions.perform(
189
+ "commit",
190
+ input_values={"message": "Sync jobs"},
191
+ jobs=["job-1"],
192
+ items=["path/job-1.yaml"],
193
+ deleted=["obsolete/path.yaml"],
194
+ )
195
+ ```
196
+
197
+ Jobs
198
+ ```python
199
+ # Lister les jobs d’un projet
200
+ jobs = rd.jobs.list(project="demo", groupPath="ops")
201
+
202
+ # Depuis un projet parenté
203
+ project = rd.projects.get("demo")
204
+ jobs = project.jobs.list()
205
+
206
+ # Accès direct à un job
207
+ job = rd.jobs.get("job-id")
208
+ job.delete()
209
+ job.definition(format="yaml")
210
+ job.retry("exec-id", argString="-opt val")
211
+ job.enable_execution()
212
+ job.disable_execution()
213
+ job.enable_schedule()
214
+ job.disable_schedule()
215
+ info = job.info()
216
+ meta = job.meta(meta="name,description")
217
+ tags = job.tags()
218
+ workflow = job.workflow()
219
+ forecast = job.forecast(time="2024-05-01T10:00:00Z", max=5)
220
+
221
+ # Exporter/importer des jobs via le manager (paramètre project ou parent)
222
+ rd.jobs.export(project="demo", format="xml", idlist="id1,id2", groupPath="group")
223
+ rd.jobs.import_jobs(
224
+ project="demo",
225
+ content=open("jobs.xml", "rb").read(),
226
+ fileformat="xml",
227
+ dupeOption="update",
228
+ )
229
+ # Ou via un projet parenté
230
+ project.jobs.export(format="json")
231
+ project.jobs.import_jobs(content=open("jobs.json", "rb").read(), fileformat="json")
232
+
233
+ Note: l'import reste exposé sous le nom `import_jobs(...)` (le mot-clé Python empêche un appel direct à `.import`). Si vous préférez l'alias, utilisez `getattr(rd.jobs, "import")(...)`.
234
+
235
+ # Exécuter et récupérer l'exécution
236
+ execution = job.run(argString="-option value")
237
+
238
+ # Actions bulk
239
+ rd.jobs.bulk.enable_execution(["id1", "id2"])
240
+ rd.jobs.bulk.disable_execution(["id1", "id2"])
241
+ rd.jobs.bulk.delete(["id1", "id2"])
242
+ rd.jobs.bulk.enable_schedule(["id1", "id2"])
243
+ rd.jobs.bulk.disable_schedule(["id1", "id2"])
244
+
245
+ # Upload de fichiers d'option et fichiers uploadés
246
+ job.upload_option_file("csvfile", open("data.csv", "rb").read(), file_name="data.csv")
247
+ job.list_uploaded_files(max=20)
248
+ rd.jobs.get_uploaded_file_info("file-id")
249
+
250
+ # SCM import/export sur un job
251
+ job_scm_export = job.scm.export
252
+ job_scm_import = job.scm.import_ # ou getattr(job.scm, "import")
253
+
254
+ job_scm_export.status()
255
+ job_scm_export.diff()
256
+ job_scm_export.perform("commit", input_values={"message": "Sync job"})
257
+
258
+ job_scm_import.status()
259
+ job_scm_import.input_fields("pull")
260
+ job_scm_import.perform("pull", input_values={"message": "Update from repo"})
261
+
262
+ # Ressources d'un projet
263
+ resources = project.resources.list(format="json", groupPath="ops")
264
+ node = project.resources.get("node1")
265
+ sources = project.sources.list()
266
+ source_details = project.sources.get(1)
267
+ project.sources.list_resources(1, accept="application/json")
268
+ project.sources.update_resources(1, content="{}", content_type="application/json")
269
+ project.acl.list()
270
+ project.acl.get("policy.aclpolicy")
271
+ project.acl.create("policy.aclpolicy", content="...yaml...")
272
+ project.acl.update("policy.aclpolicy", content="...yaml...")
273
+ project.acl.delete("policy.aclpolicy")
274
+ ```
275
+
276
+ Exécutions
277
+ ```python
278
+ # Liste simple ou paginée
279
+ execs = rd.executions.list(project="demo", status="running", max=50, offset=0)
280
+ running = rd.executions.running(project="demo") # ou "*" pour tous
281
+
282
+ # Détails / suppression
283
+ e = rd.executions.get("123")
284
+ rd.executions.delete("123")
285
+ rd.executions.bulk_delete(["123", "124"])
286
+
287
+ # Requête avancée (query)
288
+ advanced = rd.executions.query(
289
+ project="demo",
290
+ statusFilter="failed",
291
+ userFilter="alice",
292
+ jobIdListFilter=["id1", "id2"],
293
+ groupPath="ops",
294
+ max=100,
295
+ )
296
+
297
+ # Méthodes sur Execution
298
+ e.abort(asUser="admin")
299
+ output = e.get_output(offset=0, maxlines=100)
300
+ state = e.get_state()
301
+ is_running = e.is_running()
302
+ e.refresh() # recharge les données
303
+ ```
304
+
305
+ Tokens
306
+ ```python
307
+ tokens = rd.tokens.list()
308
+ user_tokens = rd.tokens.list(user="alice")
309
+ t = rd.tokens.get("tok-1")
310
+ new_token = rd.tokens.create(user="alice", roles=["admin"], duration="90d", name="cli")
311
+ rd.tokens.delete(new_token.id)
312
+ ```
313
+
314
+ Utilisateurs
315
+ ```python
316
+ users = rd.users.list()
317
+ me = rd.users.get_current()
318
+ u = rd.users.get("bob")
319
+ roles = rd.users.current_roles()
320
+
321
+ # Mise à jour via manager ou objet
322
+ u = rd.users.update("bob", firstName="Bob", lastName="Builder", email="bob@example.com")
323
+ u.roles() # via l'objet
324
+ u.update(email="new@example.com") # met à jour et rafraîchit l'objet
325
+ ```
326
+
327
+ Système
328
+ ```python
329
+ system = rd.system
330
+ info = system.info()
331
+
332
+ # Log storage
333
+ system.logstorage.info()
334
+ system.logstorage.incomplete(max=50, offset=0)
335
+ system.logstorage.incomplete_resume()
336
+
337
+ # Mode exécution (sous-manager)
338
+ system.executions.enable()
339
+ system.executions.disable()
340
+ system.executions.status()
341
+
342
+ # ACL
343
+ system.acl.list()
344
+ system.acl.get("policy.aclpolicy")
345
+ system.acl.create("policy.aclpolicy", content="...yaml...")
346
+ system.acl.update("policy.aclpolicy", content="...yaml...")
347
+ system.acl.delete("policy.aclpolicy")
348
+
349
+ # Scheduler takeover (cluster)
350
+ rd.scheduler.takeover(all_servers=True)
351
+ rd.scheduler.takeover(server_uuid="uuid-123", project="demo", job_id="job-1")
352
+ ```
353
+
354
+ Métriques
355
+ ```python
356
+ metrics = rd.metrics
357
+ metrics.list()
358
+ metrics.data()
359
+ metrics.healthcheck()
360
+ metrics.ping()
361
+
362
+ # Plugins installés
363
+ plugins = rd.plugins.list()
364
+ for plugin in plugins:
365
+ print(plugin.name, plugin.service)
366
+
367
+ # Détail d'un plugin
368
+ first = plugins[0]
369
+ detail = rd.plugins.detail(first.service, first.name)
370
+
371
+ # Webhooks projet et envoi
372
+ project = rd.projects.get("demo")
373
+ wh = project.webhooks
374
+ wh.create(
375
+ project=project.id,
376
+ name="hook1",
377
+ user="admin",
378
+ roles="admin",
379
+ eventPlugin="log-webhook-event",
380
+ config={},
381
+ enabled=True,
382
+ )
383
+ hooks = wh.list()
384
+ first_hook = hooks[0]
385
+ wh.update(first_hook.id, name="hook1-updated")
386
+ rd.webhooks.send(first_hook.authToken, json={"hello": "world"})
387
+ wh.delete(first_hook.id)
388
+
389
+ # Key storage (/storage/keys)
390
+ ks = rd.key_storage
391
+ # Créer un secret (password)
392
+ ks.create("integration/secret1", content="s3cr3t", content_type="application/x-rundeck-data-password")
393
+ resources = ks.list() # liste racine
394
+ meta = ks.get_metadata("integration/secret1")
395
+ ks.delete("integration/secret1")
396
+
397
+ # AdHoc commands/scripts
398
+ project = rd.projects.get("demo")
399
+ exec1 = project.adhoc.run_command("echo 'hello world'")
400
+ exec2 = project.adhoc.run_script("echo 'from script'")
401
+ # exec2.id pour suivre l'exécution
402
+ # Mode stub (sans refresh immédiat) puis refresh manuel
403
+ exec_stub = project.adhoc.run_command("echo stub", refresh=False)
404
+ exec_stub.refresh() # charge l'objet complet
405
+ # Script via multipart (upload)
406
+ exec3 = project.adhoc.run_script(
407
+ script_file=("hello.sh", "echo multipart", "text/plain"),
408
+ refresh=False,
409
+ )
410
+
411
+ # Features système
412
+ features = rd.features.list()
413
+ if features:
414
+ first = features[0]
415
+ status = rd.features.get(first.name)
416
+ print(first.name, status.enabled)
417
+ ```
418
+
419
+ Configuration globale `/config`
420
+ ```python
421
+ cfg = rd.config_management
422
+ all_configs = cfg.list()
423
+ cfg.save([{"key": "ui.banner", "value": "Hello"}])
424
+ cfg.delete("ui.banner", strata="default")
425
+ cfg.refresh()
426
+ cfg.restart()
427
+ ```
428
+
429
+ Pagination
430
+ ----------
431
+ Les managers héritent de `RundeckObjectManager.iter(...)` (offset/max). Exemple :
432
+ ```python
433
+ for job in rd.jobs.iter(project="demo", page_size=100):
434
+ print(job.id)
435
+ ```
436
+
437
+ Gestion des erreurs
438
+ -------------------
439
+ Les erreurs HTTP passent par `raise_for_status` et lèvent des exceptions dédiées (ex. `RundeckAuthenticationError`, `RundeckNotFoundError`, `RundeckValidationError`, `RundeckConflictError`, `RundeckServerError`). Gérez-les avec un bloc `try/except` autour des appels client.
440
+
441
+ Développement et tests
442
+ ----------------------
443
+ - Formatage/Lint : `black src/rundeck tests/`, `ruff check src/rundeck tests/`
444
+ - Typage : `mypy src/rundeck/`
445
+ - Tests : `pytest`
446
+ - Tests d'intégration locaux : `scripts/run-integration.sh` (démarre docker compose à la racine, exporte par défaut `RUNDECK_URL=http://localhost:4440`, `RUNDECK_TOKEN=adminToken`, `RUNDECK_API_VERSION=56`, attend le healthcheck, lance `poetry run pytest -m integration`). Vous pouvez surcharger les variables d'environnement avant l'appel. Ajoutez `KEEP_STACK=1` pour ne pas arrêter l'instance après les tests. Script de teardown dédié : `scripts/stop-integration.sh`.
447
+
448
+ Structure du code
449
+ -----------------
450
+ - `src/rundeck/base.py` : objets/manager génériques, helpers `_build_path`, pagination, CRUD.
451
+ - `src/rundeck/client.py` : client HTTP, méthodes `http_get/post/put/delete/list`.
452
+ - `src/rundeck/v1/objects/` : managers et objets métiers (projects, jobs, executions, system, tokens, users, config_management).
453
+ - Schéma managers/objets : on applique le pattern manager → objet partout où l’API expose une ressource identifiable (jobs, projects, users, tokens, executions, etc.). Pour les endpoints purement globaux ou utilitaires sans ressource (ex. `/system` et ses sous-domaines, `/config`, `/metrics`), on utilise des sous-managers dédiés plutôt que de forcer un objet artificiel.
454
+