python-rundeck 0.3.0__tar.gz → 1.0.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.
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/PKG-INFO +41 -4
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/README.md +40 -3
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/pyproject.toml +1 -1
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/base.py +5 -3
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/executions.py +2 -3
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/jobs.py +24 -6
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/key_storage.py +33 -12
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/system.py +6 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/users.py +5 -4
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/LICENSE +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/__init__.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/client.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/config.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/const.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/exceptions.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/py.typed +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/__init__.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/__init__.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/adhoc.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/config_management.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/features.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/metrics.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/plugins.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/projects.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/scheduler.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/scm.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/tokens.py +0 -0
- {python_rundeck-0.3.0 → python_rundeck-1.0.0}/src/rundeck/v1/objects/webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-rundeck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python client for the Rundeck API (v14-v56).
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -51,6 +51,7 @@ Contents
|
|
|
51
51
|
- Quick start
|
|
52
52
|
- Configuration
|
|
53
53
|
- Available resources
|
|
54
|
+
- Return types policy
|
|
54
55
|
- Examples by resource
|
|
55
56
|
- Error handling
|
|
56
57
|
- Development and testing
|
|
@@ -80,7 +81,8 @@ for p in projects:
|
|
|
80
81
|
# Get a project and run a job
|
|
81
82
|
project = rd.projects.get("demo")
|
|
82
83
|
jobs = project.jobs.list()
|
|
83
|
-
|
|
84
|
+
# Use as_execution=True to receive an Execution object.
|
|
85
|
+
execu = jobs[0].run(as_execution=True)
|
|
84
86
|
print(execu.id)
|
|
85
87
|
```
|
|
86
88
|
|
|
@@ -122,6 +124,40 @@ Available resources
|
|
|
122
124
|
- `config_management` (`ConfigManagementManager`): global configuration `/config`.
|
|
123
125
|
- `scm` (via `project.scm` and `job.scm`): import/export plugins, setup, enable/disable, status, actions (commit/import/export...).
|
|
124
126
|
|
|
127
|
+
Return types policy
|
|
128
|
+
-------------------
|
|
129
|
+
- Resource managers use `list/get/create/update` to return objects (`RundeckObject`).
|
|
130
|
+
- Factory managers return the created resource object for actions that create it
|
|
131
|
+
(e.g., `project.adhoc` returns `Execution`).
|
|
132
|
+
- Utility managers return raw API responses (dict/str/bytes).
|
|
133
|
+
- `delete(...)` returns `None`.
|
|
134
|
+
- Object methods return raw API responses and update the object in place when needed.
|
|
135
|
+
- Explicit flags are used when an object return is requested (e.g., `as_execution=True`).
|
|
136
|
+
|
|
137
|
+
Resource managers:
|
|
138
|
+
- `projects`, `jobs`, `executions`, `tokens`, `users`, `plugins`, `features`, `key_storage`, `system.acl`, `project.webhooks`.
|
|
139
|
+
|
|
140
|
+
Factory managers:
|
|
141
|
+
- `adhoc` (via `project.adhoc`).
|
|
142
|
+
Returns `Execution` objects (execution factory).
|
|
143
|
+
|
|
144
|
+
Utility managers:
|
|
145
|
+
- `system`, `config_management`, `metrics`, `scheduler`, `webhooks` (event send).
|
|
146
|
+
- `scm` (via `project.scm` and `job.scm`), and project sub-managers like
|
|
147
|
+
`project.config`, `project.resources`, `project.sources`, `project.readme`,
|
|
148
|
+
`project.archive`, `project.acl`.
|
|
149
|
+
|
|
150
|
+
Note: `project.webhooks.create(...)` returns the raw API response (e.g., `{"msg": "Saved webhook"}`)
|
|
151
|
+
because the Rundeck API does not return the created webhook object.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
```python
|
|
155
|
+
job = rd.jobs.get("job-id") # -> Job (object)
|
|
156
|
+
resp = job.run() # -> dict (raw API response)
|
|
157
|
+
execu = job.run(as_execution=True) # -> Execution (object)
|
|
158
|
+
execution.refresh() # updates in place, returns None
|
|
159
|
+
```
|
|
160
|
+
|
|
125
161
|
Examples by resource (complete)
|
|
126
162
|
-------------------------------
|
|
127
163
|
Projects
|
|
@@ -240,7 +276,7 @@ project.jobs.import_jobs(content=open("jobs.json", "rb").read(), fileformat="jso
|
|
|
240
276
|
Note: import remains exposed as `import_jobs(...)` (the Python keyword prevents a direct call to `.import`). If you prefer the alias, use `getattr(rd.jobs, "import")(...)`.
|
|
241
277
|
|
|
242
278
|
# Run and get the execution
|
|
243
|
-
execution = job.run(argString="-option value")
|
|
279
|
+
execution = job.run(as_execution=True, argString="-option value")
|
|
244
280
|
|
|
245
281
|
# Bulk actions
|
|
246
282
|
rd.jobs.bulk.enable_execution(["id1", "id2"])
|
|
@@ -398,7 +434,8 @@ ks = rd.key_storage
|
|
|
398
434
|
# Create a secret (password)
|
|
399
435
|
ks.create("integration/secret1", content="s3cr3t", content_type="application/x-rundeck-data-password")
|
|
400
436
|
resources = ks.list() # root listing
|
|
401
|
-
meta = ks.
|
|
437
|
+
meta = ks.get("integration/secret1")
|
|
438
|
+
content = meta.content() # raw bytes
|
|
402
439
|
ks.delete("integration/secret1")
|
|
403
440
|
|
|
404
441
|
# AdHoc commands/scripts
|
|
@@ -13,6 +13,7 @@ Contents
|
|
|
13
13
|
- Quick start
|
|
14
14
|
- Configuration
|
|
15
15
|
- Available resources
|
|
16
|
+
- Return types policy
|
|
16
17
|
- Examples by resource
|
|
17
18
|
- Error handling
|
|
18
19
|
- Development and testing
|
|
@@ -42,7 +43,8 @@ for p in projects:
|
|
|
42
43
|
# Get a project and run a job
|
|
43
44
|
project = rd.projects.get("demo")
|
|
44
45
|
jobs = project.jobs.list()
|
|
45
|
-
|
|
46
|
+
# Use as_execution=True to receive an Execution object.
|
|
47
|
+
execu = jobs[0].run(as_execution=True)
|
|
46
48
|
print(execu.id)
|
|
47
49
|
```
|
|
48
50
|
|
|
@@ -84,6 +86,40 @@ Available resources
|
|
|
84
86
|
- `config_management` (`ConfigManagementManager`): global configuration `/config`.
|
|
85
87
|
- `scm` (via `project.scm` and `job.scm`): import/export plugins, setup, enable/disable, status, actions (commit/import/export...).
|
|
86
88
|
|
|
89
|
+
Return types policy
|
|
90
|
+
-------------------
|
|
91
|
+
- Resource managers use `list/get/create/update` to return objects (`RundeckObject`).
|
|
92
|
+
- Factory managers return the created resource object for actions that create it
|
|
93
|
+
(e.g., `project.adhoc` returns `Execution`).
|
|
94
|
+
- Utility managers return raw API responses (dict/str/bytes).
|
|
95
|
+
- `delete(...)` returns `None`.
|
|
96
|
+
- Object methods return raw API responses and update the object in place when needed.
|
|
97
|
+
- Explicit flags are used when an object return is requested (e.g., `as_execution=True`).
|
|
98
|
+
|
|
99
|
+
Resource managers:
|
|
100
|
+
- `projects`, `jobs`, `executions`, `tokens`, `users`, `plugins`, `features`, `key_storage`, `system.acl`, `project.webhooks`.
|
|
101
|
+
|
|
102
|
+
Factory managers:
|
|
103
|
+
- `adhoc` (via `project.adhoc`).
|
|
104
|
+
Returns `Execution` objects (execution factory).
|
|
105
|
+
|
|
106
|
+
Utility managers:
|
|
107
|
+
- `system`, `config_management`, `metrics`, `scheduler`, `webhooks` (event send).
|
|
108
|
+
- `scm` (via `project.scm` and `job.scm`), and project sub-managers like
|
|
109
|
+
`project.config`, `project.resources`, `project.sources`, `project.readme`,
|
|
110
|
+
`project.archive`, `project.acl`.
|
|
111
|
+
|
|
112
|
+
Note: `project.webhooks.create(...)` returns the raw API response (e.g., `{"msg": "Saved webhook"}`)
|
|
113
|
+
because the Rundeck API does not return the created webhook object.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
```python
|
|
117
|
+
job = rd.jobs.get("job-id") # -> Job (object)
|
|
118
|
+
resp = job.run() # -> dict (raw API response)
|
|
119
|
+
execu = job.run(as_execution=True) # -> Execution (object)
|
|
120
|
+
execution.refresh() # updates in place, returns None
|
|
121
|
+
```
|
|
122
|
+
|
|
87
123
|
Examples by resource (complete)
|
|
88
124
|
-------------------------------
|
|
89
125
|
Projects
|
|
@@ -202,7 +238,7 @@ project.jobs.import_jobs(content=open("jobs.json", "rb").read(), fileformat="jso
|
|
|
202
238
|
Note: import remains exposed as `import_jobs(...)` (the Python keyword prevents a direct call to `.import`). If you prefer the alias, use `getattr(rd.jobs, "import")(...)`.
|
|
203
239
|
|
|
204
240
|
# Run and get the execution
|
|
205
|
-
execution = job.run(argString="-option value")
|
|
241
|
+
execution = job.run(as_execution=True, argString="-option value")
|
|
206
242
|
|
|
207
243
|
# Bulk actions
|
|
208
244
|
rd.jobs.bulk.enable_execution(["id1", "id2"])
|
|
@@ -360,7 +396,8 @@ ks = rd.key_storage
|
|
|
360
396
|
# Create a secret (password)
|
|
361
397
|
ks.create("integration/secret1", content="s3cr3t", content_type="application/x-rundeck-data-password")
|
|
362
398
|
resources = ks.list() # root listing
|
|
363
|
-
meta = ks.
|
|
399
|
+
meta = ks.get("integration/secret1")
|
|
400
|
+
content = meta.content() # raw bytes
|
|
364
401
|
ks.delete("integration/secret1")
|
|
365
402
|
|
|
366
403
|
# AdHoc commands/scripts
|
|
@@ -50,14 +50,16 @@ class RundeckObject(Generic[ResourceT]):
|
|
|
50
50
|
def to_dict(self) -> dict[str, Any]:
|
|
51
51
|
return dict(self._attrs)
|
|
52
52
|
|
|
53
|
-
def refresh(self: ResourceT) ->
|
|
54
|
-
"""
|
|
53
|
+
def refresh(self: ResourceT) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Recharge la ressource en interrogeant son manager.
|
|
56
|
+
|
|
57
|
+
"""
|
|
55
58
|
ident = self._attrs.get(self._id_attr)
|
|
56
59
|
if ident is None:
|
|
57
60
|
raise AttributeError(f"{self.__class__.__name__} has no identifier")
|
|
58
61
|
fresh = self.manager.get(ident)
|
|
59
62
|
self._attrs = fresh._attrs
|
|
60
|
-
return self
|
|
61
63
|
|
|
62
64
|
def pformat(self) -> str:
|
|
63
65
|
"""Retourne une représentation jolie du dictionnaire d'attributs."""
|
|
@@ -111,16 +111,15 @@ class Execution(RundeckObject):
|
|
|
111
111
|
"""
|
|
112
112
|
return self._attrs.get("status") == "aborted"
|
|
113
113
|
|
|
114
|
-
def refresh(self) ->
|
|
114
|
+
def refresh(self) -> None:
|
|
115
115
|
"""
|
|
116
116
|
Actualise les données de l'exécution
|
|
117
117
|
|
|
118
118
|
Returns:
|
|
119
|
-
|
|
119
|
+
None.
|
|
120
120
|
"""
|
|
121
121
|
updated = self.manager.get(self.id)
|
|
122
122
|
self._attrs = updated._attrs
|
|
123
|
-
return self
|
|
124
123
|
|
|
125
124
|
|
|
126
125
|
class ExecutionManager(RundeckObjectManager):
|
|
@@ -22,22 +22,40 @@ class Job(RundeckObject):
|
|
|
22
22
|
return f"{base}/{suffix.lstrip('/')}"
|
|
23
23
|
return base
|
|
24
24
|
|
|
25
|
-
def run(
|
|
25
|
+
def run(
|
|
26
|
+
self, as_execution: bool = False, **kwargs: Any
|
|
27
|
+
) -> Execution | dict[str, Any]:
|
|
26
28
|
"""
|
|
27
|
-
Exécute le job et retourne
|
|
29
|
+
Exécute le job et retourne la réponse brute par défaut.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
as_execution: True pour envelopper la réponse en Execution.
|
|
28
33
|
"""
|
|
29
34
|
path = self._job_path("run")
|
|
30
35
|
result = self.rd.http_post(path, json=kwargs or None)
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
if as_execution:
|
|
37
|
+
# Wrap avec le manager d'exécutions existant.
|
|
38
|
+
return Execution(self.manager.rd.executions, result)
|
|
39
|
+
return result
|
|
33
40
|
|
|
34
|
-
def retry(
|
|
41
|
+
def retry(
|
|
42
|
+
self,
|
|
43
|
+
execution_id: str,
|
|
44
|
+
as_execution: bool = False,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> Execution | dict[str, Any]:
|
|
35
47
|
"""
|
|
36
48
|
Relance une exécution (retry).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
execution_id: ID de l'exécution à relancer.
|
|
52
|
+
as_execution: True pour envelopper la réponse en Execution.
|
|
37
53
|
"""
|
|
38
54
|
path = self._job_path(f"retry/{execution_id}")
|
|
39
55
|
result = self.rd.http_post(path, json=kwargs or None)
|
|
40
|
-
|
|
56
|
+
if as_execution:
|
|
57
|
+
return Execution(self.manager.rd.executions, result)
|
|
58
|
+
return result
|
|
41
59
|
|
|
42
60
|
def delete(self) -> None:
|
|
43
61
|
"""Supprime le job courant."""
|
|
@@ -15,6 +15,28 @@ class StorageKey(RundeckObject):
|
|
|
15
15
|
_id_attr = "path"
|
|
16
16
|
_repr_attr = "name"
|
|
17
17
|
|
|
18
|
+
def _relative_path(self) -> str:
|
|
19
|
+
path = self.path
|
|
20
|
+
# API returns "keys/..." but endpoints expect the relative path.
|
|
21
|
+
if isinstance(path, str) and path.startswith("keys/"):
|
|
22
|
+
return path[len("keys/") :]
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
def content(self, accept: str = "application/pgp-keys") -> bytes:
|
|
26
|
+
"""Récupère le contenu d'une clé (ex: clé publique)."""
|
|
27
|
+
path = self.manager._file_path(self._relative_path())
|
|
28
|
+
headers = {"Accept": accept} if accept else None
|
|
29
|
+
response = self.manager.rd.http_get(path, headers=headers, raw=True)
|
|
30
|
+
return response.content
|
|
31
|
+
|
|
32
|
+
def update(self, content: bytes | str, content_type: str) -> dict[str, Any]:
|
|
33
|
+
"""Met à jour le contenu de la clé et rafraîchit l'objet."""
|
|
34
|
+
updated = self.manager.update(
|
|
35
|
+
self._relative_path(), content=content, content_type=content_type
|
|
36
|
+
)
|
|
37
|
+
self._attrs = updated._attrs
|
|
38
|
+
return updated.to_dict()
|
|
39
|
+
|
|
18
40
|
|
|
19
41
|
class StorageKeyManager(RundeckObjectManager[StorageKey]):
|
|
20
42
|
"""Manager pour les clés/ressources du storage."""
|
|
@@ -43,7 +65,7 @@ class StorageKeyManager(RundeckObjectManager[StorageKey]):
|
|
|
43
65
|
resources = result.get("resources") or []
|
|
44
66
|
return self._wrap_list(resources)
|
|
45
67
|
|
|
46
|
-
def
|
|
68
|
+
def get(self, path: str) -> StorageKey:
|
|
47
69
|
"""Récupère les métadonnées d'une clé/fichier."""
|
|
48
70
|
target = self._file_path(path)
|
|
49
71
|
result = self.rd.http_get(target)
|
|
@@ -51,24 +73,23 @@ class StorageKeyManager(RundeckObjectManager[StorageKey]):
|
|
|
51
73
|
return self._wrap(result)
|
|
52
74
|
return self._wrap({"path": path, "raw": result})
|
|
53
75
|
|
|
54
|
-
def
|
|
55
|
-
"""Récupère le contenu d'une clé (ex: clé publique)."""
|
|
56
|
-
target = self._file_path(path)
|
|
57
|
-
headers = {"Accept": accept} if accept else None
|
|
58
|
-
response = self.rd.http_get(target, headers=headers, raw=True)
|
|
59
|
-
return response.content
|
|
60
|
-
|
|
61
|
-
def create(self, path: str, content: bytes | str, content_type: str) -> Any:
|
|
76
|
+
def create(self, path: str, content: bytes | str, content_type: str) -> StorageKey:
|
|
62
77
|
"""Crée une nouvelle clé/fichier à l'emplacement donné."""
|
|
63
78
|
target = self._file_path(path)
|
|
64
79
|
headers = {"Content-Type": content_type, "Accept": "application/json"}
|
|
65
|
-
|
|
80
|
+
result = self.rd.http_post(target, data=content, headers=headers)
|
|
81
|
+
if isinstance(result, dict):
|
|
82
|
+
return self._wrap(result)
|
|
83
|
+
return self._wrap({"path": path, "raw": result})
|
|
66
84
|
|
|
67
|
-
def update(self, path: str, content: bytes | str, content_type: str) ->
|
|
85
|
+
def update(self, path: str, content: bytes | str, content_type: str) -> StorageKey:
|
|
68
86
|
"""Met à jour une clé/fichier existant."""
|
|
69
87
|
target = self._file_path(path)
|
|
70
88
|
headers = {"Content-Type": content_type, "Accept": "application/json"}
|
|
71
|
-
|
|
89
|
+
result = self.rd.http_put(target, data=content, headers=headers)
|
|
90
|
+
if isinstance(result, dict):
|
|
91
|
+
return self._wrap(result)
|
|
92
|
+
return self._wrap({"path": path, "raw": result})
|
|
72
93
|
|
|
73
94
|
def delete(self, path: str) -> None:
|
|
74
95
|
"""Supprime une clé/fichier."""
|
|
@@ -34,6 +34,12 @@ class SystemACL(RundeckObject):
|
|
|
34
34
|
_id_attr = "name"
|
|
35
35
|
_repr_attr = "name"
|
|
36
36
|
|
|
37
|
+
def update(self, content: str) -> dict[str, Any]:
|
|
38
|
+
"""Met à jour la policy ACL et rafraîchit l'objet."""
|
|
39
|
+
updated = self.manager.update(self.name, content)
|
|
40
|
+
self._attrs = updated._attrs
|
|
41
|
+
return updated.to_dict()
|
|
42
|
+
|
|
37
43
|
|
|
38
44
|
class SystemACLManager(RundeckObjectManager[SystemACL]):
|
|
39
45
|
"""Sous-manager pour les endpoints ACL système."""
|
|
@@ -18,7 +18,7 @@ class User(RundeckObject):
|
|
|
18
18
|
firstName: str | None = None,
|
|
19
19
|
lastName: str | None = None,
|
|
20
20
|
email: str | None = None,
|
|
21
|
-
) ->
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
22
|
"""
|
|
23
23
|
Met à jour les informations de l'utilisateur
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ class User(RundeckObject):
|
|
|
28
28
|
email: Email
|
|
29
29
|
|
|
30
30
|
Returns:
|
|
31
|
-
|
|
31
|
+
Réponse brute de l'API.
|
|
32
32
|
"""
|
|
33
33
|
data: dict[str, Any] = {}
|
|
34
34
|
if firstName:
|
|
@@ -40,8 +40,9 @@ class User(RundeckObject):
|
|
|
40
40
|
|
|
41
41
|
path = self.manager._build_path(f"info/{self.login}")
|
|
42
42
|
result = self.manager.rd.http_post(path, json=data)
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
if isinstance(result, dict):
|
|
44
|
+
self._attrs.update(result)
|
|
45
|
+
return result
|
|
45
46
|
|
|
46
47
|
def roles(self) -> list[str]:
|
|
47
48
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|