jubilant 0.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.
- jubilant-0.0.0/PKG-INFO +12 -0
- jubilant-0.0.0/README.md +5 -0
- jubilant-0.0.0/jubilant/__init__.py +143 -0
- jubilant-0.0.0/jubilant/_types.py +43 -0
- jubilant-0.0.0/jubilant.egg-info/PKG-INFO +12 -0
- jubilant-0.0.0/jubilant.egg-info/SOURCES.txt +8 -0
- jubilant-0.0.0/jubilant.egg-info/dependency_links.txt +1 -0
- jubilant-0.0.0/jubilant.egg-info/top_level.txt +1 -0
- jubilant-0.0.0/pyproject.toml +116 -0
- jubilant-0.0.0/setup.cfg +4 -0
jubilant-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: jubilant
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Juju CLI wrapper for charm integration testing
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# Jubilant, the joyful library for integration-testing Juju charms
|
|
9
|
+
|
|
10
|
+
Jubilant is a Python library that wraps the [Juju](https://juju.is/) CLI for use in charm integration tests. It provides methods that map 1:1 to Juju CLI commands, but with a type-annotated, Pythonic interface.
|
|
11
|
+
|
|
12
|
+
**NOTE:** Jubilant is in very early stages of development. This is pre-alpha code. Our intention is to release a 1.0.0 version early to mid 2025.
|
jubilant-0.0.0/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Jubilant, the joyful library for integration-testing Juju charms
|
|
2
|
+
|
|
3
|
+
Jubilant is a Python library that wraps the [Juju](https://juju.is/) CLI for use in charm integration tests. It provides methods that map 1:1 to Juju CLI commands, but with a type-annotated, Pythonic interface.
|
|
4
|
+
|
|
5
|
+
**NOTE:** Jubilant is in very early stages of development. This is pre-alpha code. Our intention is to release a 1.0.0 version early to mid 2025.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Jubilant is a Pythonic wrapper around the Juju CLI for integration testing."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from ._types import Status
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'Juju',
|
|
12
|
+
'Status',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
__version__ = '0.0.1'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Juju:
|
|
19
|
+
"""TODO."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, model: str | None = None):
|
|
22
|
+
self.model = model
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
args = []
|
|
26
|
+
if self.model is not None:
|
|
27
|
+
args.append(f'model={self.model!r}')
|
|
28
|
+
return f'Juju({", ".join(args)})'
|
|
29
|
+
|
|
30
|
+
def cli(self, *args: str) -> str:
|
|
31
|
+
"""TODO."""
|
|
32
|
+
# TODO: good error handling and include stderr in exception
|
|
33
|
+
process = subprocess.run(
|
|
34
|
+
['juju', *args], check=True, capture_output=True, encoding='UTF-8'
|
|
35
|
+
)
|
|
36
|
+
return process.stdout
|
|
37
|
+
|
|
38
|
+
def add_model(
|
|
39
|
+
self,
|
|
40
|
+
model_name: str, # TODO: should this use self.model if set, and set it if not?
|
|
41
|
+
*,
|
|
42
|
+
controller: str | None = None,
|
|
43
|
+
config: dict[str, typing.Any] | None = None, # TODO: is Any correct here?
|
|
44
|
+
) -> None:
|
|
45
|
+
"""TODO."""
|
|
46
|
+
args = ['add-model', model_name]
|
|
47
|
+
|
|
48
|
+
if controller is not None:
|
|
49
|
+
args.extend(['--controller', controller])
|
|
50
|
+
if config is not None:
|
|
51
|
+
for k, v in config.items():
|
|
52
|
+
args.extend(['--config', f'{k}={v}'])
|
|
53
|
+
|
|
54
|
+
self.cli(*args)
|
|
55
|
+
|
|
56
|
+
def destroy_model(
|
|
57
|
+
self,
|
|
58
|
+
model_name: str,
|
|
59
|
+
*,
|
|
60
|
+
force=False,
|
|
61
|
+
):
|
|
62
|
+
"""TODO."""
|
|
63
|
+
args = ['destroy-model', model_name, '--no-prompt']
|
|
64
|
+
if force:
|
|
65
|
+
args.append('--force')
|
|
66
|
+
self.cli(*args)
|
|
67
|
+
|
|
68
|
+
def deploy(
|
|
69
|
+
self,
|
|
70
|
+
charm_name: str,
|
|
71
|
+
application_name: str | None = None,
|
|
72
|
+
*,
|
|
73
|
+
model: str | None = None,
|
|
74
|
+
config: dict[str, typing.Any] | None = None, # TODO: is Any correct here?
|
|
75
|
+
num_units: int = 1,
|
|
76
|
+
resources: dict[str, str] | None = None,
|
|
77
|
+
trust: bool = False,
|
|
78
|
+
# TODO: include all the arguments we think people we use
|
|
79
|
+
) -> None:
|
|
80
|
+
"""TODO."""
|
|
81
|
+
args = ['deploy', charm_name]
|
|
82
|
+
if application_name is not None:
|
|
83
|
+
args.append(application_name)
|
|
84
|
+
|
|
85
|
+
if model is None:
|
|
86
|
+
model = self.model
|
|
87
|
+
if model is not None:
|
|
88
|
+
args.extend(['--model', model])
|
|
89
|
+
if config is not None:
|
|
90
|
+
for k, v in config.items():
|
|
91
|
+
args.extend(['--config', f'{k}={v}'])
|
|
92
|
+
if num_units != 1:
|
|
93
|
+
args.extend(['--num-units', str(num_units)])
|
|
94
|
+
if resources is not None:
|
|
95
|
+
for k, v in resources.items():
|
|
96
|
+
args.extend(['--resource', f'{k}={v}'])
|
|
97
|
+
if trust:
|
|
98
|
+
args.append('--trust')
|
|
99
|
+
|
|
100
|
+
self.cli(*args)
|
|
101
|
+
|
|
102
|
+
def status(
|
|
103
|
+
self,
|
|
104
|
+
*,
|
|
105
|
+
model: str | None = None,
|
|
106
|
+
) -> Status:
|
|
107
|
+
"""TODO."""
|
|
108
|
+
args = ['status', '--format', 'json']
|
|
109
|
+
|
|
110
|
+
if model is None:
|
|
111
|
+
model = self.model
|
|
112
|
+
if model is not None:
|
|
113
|
+
args.extend(['--model', model])
|
|
114
|
+
|
|
115
|
+
stdout = self.cli(*args)
|
|
116
|
+
result = json.loads(stdout)
|
|
117
|
+
return Status.from_dict(result)
|
|
118
|
+
|
|
119
|
+
def wait_status(
|
|
120
|
+
self,
|
|
121
|
+
ready_func: typing.Callable[[Status], bool],
|
|
122
|
+
*,
|
|
123
|
+
model: str | None = None,
|
|
124
|
+
timeout: float = 10 * 60,
|
|
125
|
+
delay: float = 1,
|
|
126
|
+
successes: int = 3,
|
|
127
|
+
) -> Status:
|
|
128
|
+
"""TODO."""
|
|
129
|
+
start = time.time()
|
|
130
|
+
success_count = 0
|
|
131
|
+
this_status = None
|
|
132
|
+
while time.time() - start < timeout:
|
|
133
|
+
this_status = self.status(model=model)
|
|
134
|
+
# logger.info('wait_status: %s', this_status) # TODO: ensure better debugging
|
|
135
|
+
print('TODO wait_status', this_status)
|
|
136
|
+
if ready_func(this_status):
|
|
137
|
+
success_count += 1
|
|
138
|
+
if success_count >= successes:
|
|
139
|
+
return this_status
|
|
140
|
+
else:
|
|
141
|
+
success_count = 0
|
|
142
|
+
time.sleep(delay)
|
|
143
|
+
raise Exception(f'timed out after {timeout}, last status:\n{this_status}')
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclasses.dataclass
|
|
5
|
+
class StatusInfoContents:
|
|
6
|
+
current: str | None = None
|
|
7
|
+
message: str | None = None
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_dict(cls, d):
|
|
11
|
+
self = cls()
|
|
12
|
+
self.current = d.get('current')
|
|
13
|
+
self.message = d.get('message')
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass
|
|
18
|
+
class ApplicationStatus:
|
|
19
|
+
application_status: StatusInfoContents = dataclasses.field(default_factory=StatusInfoContents)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, d):
|
|
23
|
+
self = cls()
|
|
24
|
+
self.application_status = StatusInfoContents.from_dict(d.get('application-status') or {})
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass
|
|
29
|
+
class Status:
|
|
30
|
+
# TODO: Ideally we can generate the list of fields from the Go source in Juju:
|
|
31
|
+
# cmd/juju/status/formatted.go
|
|
32
|
+
applications: dict[str, ApplicationStatus] = dataclasses.field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, d):
|
|
36
|
+
self = cls()
|
|
37
|
+
applications = d.get('applications') or {}
|
|
38
|
+
self.applications = {
|
|
39
|
+
name: ApplicationStatus.from_dict(status) for name, status in applications.items()
|
|
40
|
+
}
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
# TODO: helper methods
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: jubilant
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Juju CLI wrapper for charm integration testing
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# Jubilant, the joyful library for integration-testing Juju charms
|
|
9
|
+
|
|
10
|
+
Jubilant is a Python library that wraps the [Juju](https://juju.is/) CLI for use in charm integration tests. It provides methods that map 1:1 to Juju CLI commands, but with a type-annotated, Pythonic interface.
|
|
11
|
+
|
|
12
|
+
**NOTE:** Jubilant is in very early stages of development. This is pre-alpha code. Our intention is to release a 1.0.0 version early to mid 2025.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jubilant
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jubilant"
|
|
3
|
+
description = "Juju CLI wrapper for charm integration testing"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
dynamic = ["version"]
|
|
7
|
+
|
|
8
|
+
[dependency-groups]
|
|
9
|
+
dev = [
|
|
10
|
+
"pyright==1.1.390",
|
|
11
|
+
"pytest==8.3.4",
|
|
12
|
+
"ruff==0.8.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Linting tools configuration
|
|
16
|
+
[tool.ruff]
|
|
17
|
+
line-length = 99
|
|
18
|
+
target-version = "py312"
|
|
19
|
+
|
|
20
|
+
# Ruff formatter configuration
|
|
21
|
+
[tool.ruff.format]
|
|
22
|
+
quote-style = "single"
|
|
23
|
+
|
|
24
|
+
[tool.ruff.lint]
|
|
25
|
+
select = [
|
|
26
|
+
# Pyflakes
|
|
27
|
+
"F",
|
|
28
|
+
# Pycodestyle
|
|
29
|
+
"E",
|
|
30
|
+
"W",
|
|
31
|
+
# isort
|
|
32
|
+
"I001",
|
|
33
|
+
# pep8-naming
|
|
34
|
+
"N",
|
|
35
|
+
# flake8-builtins
|
|
36
|
+
"A",
|
|
37
|
+
# pyupgrade
|
|
38
|
+
"UP",
|
|
39
|
+
# flake8-2020
|
|
40
|
+
"YTT",
|
|
41
|
+
# flake8-bandit
|
|
42
|
+
"S",
|
|
43
|
+
# flake8-bugbear
|
|
44
|
+
"B",
|
|
45
|
+
# flake8-simplify
|
|
46
|
+
"SIM",
|
|
47
|
+
# Ruff specific
|
|
48
|
+
"RUF",
|
|
49
|
+
# Perflint
|
|
50
|
+
"PERF",
|
|
51
|
+
# pyflakes-docstrings
|
|
52
|
+
"D",
|
|
53
|
+
]
|
|
54
|
+
ignore = [
|
|
55
|
+
# Use of `assert` detected
|
|
56
|
+
"S101",
|
|
57
|
+
# Do not `assert False`
|
|
58
|
+
"B011",
|
|
59
|
+
# `pickle`, `cPickle`, `dill`, and `shelve` modules are possibly insecure
|
|
60
|
+
"S403",
|
|
61
|
+
# `subprocess` module is possibly insecure
|
|
62
|
+
"S404",
|
|
63
|
+
|
|
64
|
+
# No explicit `stacklevel` keyword argument found
|
|
65
|
+
"B028",
|
|
66
|
+
|
|
67
|
+
# Return condition directly, prefer readability.
|
|
68
|
+
"SIM103",
|
|
69
|
+
# Use contextlib.suppress() instead of try/except: pass
|
|
70
|
+
"SIM105",
|
|
71
|
+
# Use a single `with` statement with multiple contexts instead of nested `with` statements
|
|
72
|
+
"SIM117",
|
|
73
|
+
|
|
74
|
+
# Missing docstring in magic method
|
|
75
|
+
"D105",
|
|
76
|
+
# Missing docstring in `__init__`
|
|
77
|
+
"D107",
|
|
78
|
+
|
|
79
|
+
# Manual dict comprehension.
|
|
80
|
+
"PERF403",
|
|
81
|
+
|
|
82
|
+
# Convert {} from `TypedDict` functional to class syntax
|
|
83
|
+
# Note that since we have some `TypedDict`s that cannot use the class
|
|
84
|
+
# syntax, we're currently choosing to be consistent in syntax even though
|
|
85
|
+
# some can be moved to the class syntax.
|
|
86
|
+
"UP013",
|
|
87
|
+
|
|
88
|
+
## Likely worth doing, but later.
|
|
89
|
+
|
|
90
|
+
# `subprocess` call: check for execution of untrusted input
|
|
91
|
+
"S603",
|
|
92
|
+
# Starting a process with a partial executable path
|
|
93
|
+
"S607",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint.pydocstyle]
|
|
97
|
+
convention = "google"
|
|
98
|
+
|
|
99
|
+
[tool.ruff.lint.flake8-builtins]
|
|
100
|
+
builtins-ignorelist = ["id", "min", "map", "range", "type", "input", "format"]
|
|
101
|
+
|
|
102
|
+
[tool.ruff.lint.per-file-ignores]
|
|
103
|
+
"test/*" = [
|
|
104
|
+
# All documentation linting.
|
|
105
|
+
"D",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
[tool.pyright]
|
|
109
|
+
include = ["jubilant/*.py"]
|
|
110
|
+
pythonVersion = "3.12"
|
|
111
|
+
pythonPlatform = "All"
|
|
112
|
+
|
|
113
|
+
[tool.pytest.ini_options]
|
|
114
|
+
pythonpath = [
|
|
115
|
+
"."
|
|
116
|
+
]
|
jubilant-0.0.0/setup.cfg
ADDED