ops 2.19.0__tar.gz → 2.20.0.dev0__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.
- {ops-2.19.0/ops.egg-info → ops-2.20.0.dev0}/PKG-INFO +1 -1
- {ops-2.19.0 → ops-2.20.0.dev0}/STYLE.md +22 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/_main.py +41 -41
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/harness.py +54 -27
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/charm.py +15 -3
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/framework.py +3 -3
- {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/jujucontext.py +1 -1
- {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/log.py +1 -1
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/main.py +2 -3
- {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/model.py +17 -17
- {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/pebble.py +5 -5
- {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/testing.py +4 -11
- {ops-2.19.0/test/charms/test_main/lib → ops-2.20.0.dev0}/ops/version.py +1 -1
- {ops-2.19.0 → ops-2.20.0.dev0/ops.egg-info}/PKG-INFO +1 -1
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_main.py +41 -41
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/harness.py +54 -27
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/charm.py +15 -3
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/framework.py +3 -3
- {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/jujucontext.py +1 -1
- {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/log.py +1 -1
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/main.py +2 -3
- {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/model.py +17 -17
- {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/pebble.py +5 -5
- {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/testing.py +4 -11
- {ops-2.19.0 → ops-2.20.0.dev0/test/charms/test_main/lib}/ops/version.py +1 -1
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_charm.py +16 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_testing.py +33 -8
- {ops-2.19.0 → ops-2.20.0.dev0}/CHANGES.md +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/CODE_OF_CONDUCT.md +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/HACKING.md +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/LICENSE.txt +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/MANIFEST.in +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/README.md +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/SECURITY.md +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/timeconv.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/_private/yaml.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/jujuversion.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/lib/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/py.typed +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops/storage.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/SOURCES.txt +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/dependency_links.txt +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/requires.txt +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/ops.egg-info/top_level.txt +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/pyproject.toml +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/setup.cfg +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/benchmark/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-ids +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-ids.bat +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-list +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/bin/relation-list.bat +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/actions.yaml +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/config.yaml +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/timeconv.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/_private/yaml.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/jujuversion.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/lib/__init__.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/py.typed +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/lib/ops/storage.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/metadata.yaml +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_main/src/charm.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_smoke/README.md +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_smoke/metadata.yaml +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/charms/test_smoke/src/charm.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/fake_pebble.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/pebble_cli.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/smoke/test_smoke.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_framework.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_helpers.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_infra.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_jujucontext.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_jujuversion.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_lib.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_log.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_main.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_main_invocation.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_main_type_hint.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_model.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_pebble.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_real_pebble.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_storage.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_timeconv.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/test/test_yaml.py +0 -0
- {ops-2.19.0 → ops-2.20.0.dev0}/tox.ini +0 -0
|
@@ -52,6 +52,28 @@ counts: Optional[Tuple[str, int]]
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
### Use relative imports inside a package
|
|
56
|
+
|
|
57
|
+
When writing code inside a package (a directory containing an `__init__.py` file), use relative imports with a `.` instead of absolute imports.
|
|
58
|
+
For example, within the `ops` package:
|
|
59
|
+
|
|
60
|
+
**Don't:**
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from ops import charm
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Do:**
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from . import charm
|
|
70
|
+
|
|
71
|
+
# Or, if you need to avoid adding the public name "charm" to the namespace:
|
|
72
|
+
|
|
73
|
+
from . import charm as _charm
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
|
|
55
77
|
### Avoid nested comprehensions and generator expressions
|
|
56
78
|
|
|
57
79
|
"Flat is better than nested."
|
|
@@ -23,13 +23,13 @@ import warnings
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast
|
|
25
25
|
|
|
26
|
-
import
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
import
|
|
30
|
-
from
|
|
31
|
-
from
|
|
32
|
-
from
|
|
26
|
+
from . import charm as _charm
|
|
27
|
+
from . import framework as _framework
|
|
28
|
+
from . import model as _model
|
|
29
|
+
from . import storage as _storage
|
|
30
|
+
from . import version as _version
|
|
31
|
+
from .jujucontext import _JujuContext
|
|
32
|
+
from .log import setup_root_logging
|
|
33
33
|
|
|
34
34
|
CHARM_STATE_FILE = '.unit-state.db'
|
|
35
35
|
|
|
@@ -49,8 +49,8 @@ def _exe_path(path: Path) -> Optional[Path]:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def _create_event_link(
|
|
52
|
-
charm: '
|
|
53
|
-
bound_event: '
|
|
52
|
+
charm: '_charm.CharmBase',
|
|
53
|
+
bound_event: '_framework.BoundEvent',
|
|
54
54
|
link_to: Union[str, Path],
|
|
55
55
|
):
|
|
56
56
|
"""Create a symlink for a particular event.
|
|
@@ -63,10 +63,10 @@ def _create_event_link(
|
|
|
63
63
|
# type guard
|
|
64
64
|
assert bound_event.event_kind, f'unbound BoundEvent {bound_event}'
|
|
65
65
|
|
|
66
|
-
if issubclass(bound_event.event_type,
|
|
66
|
+
if issubclass(bound_event.event_type, _charm.HookEvent):
|
|
67
67
|
event_dir = charm.framework.charm_dir / 'hooks'
|
|
68
68
|
event_path = event_dir / bound_event.event_kind.replace('_', '-')
|
|
69
|
-
elif issubclass(bound_event.event_type,
|
|
69
|
+
elif issubclass(bound_event.event_type, _charm.ActionEvent):
|
|
70
70
|
if not bound_event.event_kind.endswith('_action'):
|
|
71
71
|
raise RuntimeError(f'action event name {bound_event.event_kind} needs _action suffix')
|
|
72
72
|
event_dir = charm.framework.charm_dir / 'actions'
|
|
@@ -89,7 +89,7 @@ def _create_event_link(
|
|
|
89
89
|
event_path.symlink_to(target_path)
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def _setup_event_links(charm_dir: Path, charm: '
|
|
92
|
+
def _setup_event_links(charm_dir: Path, charm: '_charm.CharmBase', juju_context: _JujuContext):
|
|
93
93
|
"""Set up links for supported events that originate from Juju.
|
|
94
94
|
|
|
95
95
|
Whether a charm can handle an event or not can be determined by
|
|
@@ -108,42 +108,42 @@ def _setup_event_links(charm_dir: Path, charm: 'ops.charm.CharmBase', juju_conte
|
|
|
108
108
|
link_to = os.path.realpath(juju_context.dispatch_path or sys.argv[0])
|
|
109
109
|
for bound_event in charm.on.events().values():
|
|
110
110
|
# Only events that originate from Juju need symlinks.
|
|
111
|
-
if issubclass(bound_event.event_type, (
|
|
111
|
+
if issubclass(bound_event.event_type, (_charm.HookEvent, _charm.ActionEvent)):
|
|
112
112
|
_create_event_link(charm, bound_event, link_to)
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
def _get_event_args(
|
|
116
|
-
charm: '
|
|
117
|
-
bound_event: '
|
|
116
|
+
charm: '_charm.CharmBase',
|
|
117
|
+
bound_event: '_framework.BoundEvent',
|
|
118
118
|
juju_context: _JujuContext,
|
|
119
119
|
) -> Tuple[List[Any], Dict[str, Any]]:
|
|
120
120
|
event_type = bound_event.event_type
|
|
121
121
|
model = charm.framework.model
|
|
122
122
|
|
|
123
123
|
relation = None
|
|
124
|
-
if issubclass(event_type,
|
|
124
|
+
if issubclass(event_type, _charm.WorkloadEvent):
|
|
125
125
|
workload_name = juju_context.workload_name
|
|
126
126
|
assert workload_name is not None
|
|
127
127
|
container = model.unit.get_container(workload_name)
|
|
128
128
|
args: List[Any] = [container]
|
|
129
|
-
if issubclass(event_type,
|
|
129
|
+
if issubclass(event_type, _charm.PebbleNoticeEvent):
|
|
130
130
|
notice_id = juju_context.notice_id
|
|
131
131
|
notice_type = juju_context.notice_type
|
|
132
132
|
notice_key = juju_context.notice_key
|
|
133
133
|
args.extend([notice_id, notice_type, notice_key])
|
|
134
|
-
elif issubclass(event_type,
|
|
134
|
+
elif issubclass(event_type, _charm.PebbleCheckEvent):
|
|
135
135
|
check_name = juju_context.pebble_check_name
|
|
136
136
|
args.append(check_name)
|
|
137
137
|
return args, {}
|
|
138
|
-
elif issubclass(event_type,
|
|
138
|
+
elif issubclass(event_type, _charm.SecretEvent):
|
|
139
139
|
args: List[Any] = [
|
|
140
140
|
juju_context.secret_id,
|
|
141
141
|
juju_context.secret_label,
|
|
142
142
|
]
|
|
143
|
-
if issubclass(event_type, (
|
|
143
|
+
if issubclass(event_type, (_charm.SecretRemoveEvent, _charm.SecretExpiredEvent)):
|
|
144
144
|
args.append(juju_context.secret_revision)
|
|
145
145
|
return args, {}
|
|
146
|
-
elif issubclass(event_type,
|
|
146
|
+
elif issubclass(event_type, _charm.StorageEvent):
|
|
147
147
|
# Before JUJU_STORAGE_ID exists, take the event name as
|
|
148
148
|
# <storage_name>_storage_<attached|detached> and replace it with <storage_name>
|
|
149
149
|
storage_name = juju_context.storage_name or '-'.join(
|
|
@@ -157,17 +157,17 @@ def _get_event_args(
|
|
|
157
157
|
else:
|
|
158
158
|
# If there's more than one value, pick the right one. We'll realize the key on lookup
|
|
159
159
|
storage = next((s for s in storages if s.index == index), None)
|
|
160
|
-
storage = cast(Union[
|
|
160
|
+
storage = cast(Union[_storage.JujuStorage, _storage.SQLiteStorage], storage)
|
|
161
161
|
storage.location = storage_location # type: ignore
|
|
162
162
|
return [storage], {}
|
|
163
|
-
elif issubclass(event_type,
|
|
163
|
+
elif issubclass(event_type, _charm.ActionEvent):
|
|
164
164
|
args: List[Any] = [juju_context.action_uuid]
|
|
165
165
|
return args, {}
|
|
166
|
-
elif issubclass(event_type,
|
|
166
|
+
elif issubclass(event_type, _charm.RelationEvent):
|
|
167
167
|
relation_name = juju_context.relation_name
|
|
168
168
|
assert relation_name is not None
|
|
169
169
|
relation_id = juju_context.relation_id
|
|
170
|
-
relation: Optional[
|
|
170
|
+
relation: Optional[_model.Relation] = model.get_relation(relation_name, relation_id)
|
|
171
171
|
|
|
172
172
|
remote_app_name = juju_context.remote_app_name
|
|
173
173
|
remote_unit_name = juju_context.remote_unit_name
|
|
@@ -223,7 +223,7 @@ class _Dispatcher:
|
|
|
223
223
|
else:
|
|
224
224
|
self._init_legacy()
|
|
225
225
|
|
|
226
|
-
def ensure_event_links(self, charm: '
|
|
226
|
+
def ensure_event_links(self, charm: '_charm.CharmBase'):
|
|
227
227
|
"""Make sure necessary symlinks are present on disk."""
|
|
228
228
|
if self.is_dispatch_aware:
|
|
229
229
|
# links aren't needed
|
|
@@ -324,7 +324,7 @@ class _Dispatcher:
|
|
|
324
324
|
|
|
325
325
|
|
|
326
326
|
def _should_use_controller_storage(
|
|
327
|
-
db_path: Path, meta: CharmMeta, juju_context: _JujuContext
|
|
327
|
+
db_path: Path, meta: _charm.CharmMeta, juju_context: _JujuContext
|
|
328
328
|
) -> bool:
|
|
329
329
|
"""Figure out whether we want to use controller storage or not."""
|
|
330
330
|
# if local state has been used previously, carry on using that
|
|
@@ -375,8 +375,8 @@ class _Manager:
|
|
|
375
375
|
|
|
376
376
|
def __init__(
|
|
377
377
|
self,
|
|
378
|
-
charm_class: Type['
|
|
379
|
-
model_backend: Optional[
|
|
378
|
+
charm_class: Type['_charm.CharmBase'],
|
|
379
|
+
model_backend: Optional[_model._ModelBackend] = None,
|
|
380
380
|
use_juju_for_storage: Optional[bool] = None,
|
|
381
381
|
charm_state_path: str = CHARM_STATE_FILE,
|
|
382
382
|
juju_context: Optional[_JujuContext] = None,
|
|
@@ -387,7 +387,7 @@ class _Manager:
|
|
|
387
387
|
self._charm_state_path = charm_state_path
|
|
388
388
|
self._charm_class = charm_class
|
|
389
389
|
if model_backend is None:
|
|
390
|
-
model_backend =
|
|
390
|
+
model_backend = _model._ModelBackend(juju_context=self._juju_context)
|
|
391
391
|
self._model_backend = model_backend
|
|
392
392
|
|
|
393
393
|
# Do this as early as possible to be sure to catch the most logs.
|
|
@@ -405,9 +405,9 @@ class _Manager:
|
|
|
405
405
|
self.charm = self._make_charm(self.framework, self.dispatcher)
|
|
406
406
|
|
|
407
407
|
def _load_charm_meta(self):
|
|
408
|
-
return CharmMeta.from_charm_root(self._charm_root)
|
|
408
|
+
return _charm.CharmMeta.from_charm_root(self._charm_root)
|
|
409
409
|
|
|
410
|
-
def _make_charm(self, framework: '
|
|
410
|
+
def _make_charm(self, framework: '_framework.Framework', dispatcher: _Dispatcher):
|
|
411
411
|
charm = self._charm_class(framework)
|
|
412
412
|
dispatcher.ensure_event_links(charm)
|
|
413
413
|
return charm
|
|
@@ -421,13 +421,13 @@ class _Manager:
|
|
|
421
421
|
self._model_backend, debug=self._juju_context.debug, exc_stderr=handling_action
|
|
422
422
|
)
|
|
423
423
|
|
|
424
|
-
logger.debug('ops %s up and running.',
|
|
424
|
+
logger.debug('ops %s up and running.', _version.version)
|
|
425
425
|
|
|
426
426
|
def _make_storage(self, dispatcher: _Dispatcher):
|
|
427
427
|
charm_state_path = self._charm_root / self._charm_state_path
|
|
428
428
|
|
|
429
429
|
use_juju_for_storage = self._use_juju_for_storage
|
|
430
|
-
if use_juju_for_storage and not
|
|
430
|
+
if use_juju_for_storage and not _storage.juju_backend_available():
|
|
431
431
|
# raise an exception; the charm is broken and needs fixing.
|
|
432
432
|
msg = 'charm set use_juju_for_storage=True, but Juju version {} does not support it'
|
|
433
433
|
raise RuntimeError(msg.format(self._juju_context.version))
|
|
@@ -457,9 +457,9 @@ class _Manager:
|
|
|
457
457
|
raise _Abort(0)
|
|
458
458
|
|
|
459
459
|
if self._use_juju_for_storage:
|
|
460
|
-
store =
|
|
460
|
+
store = _storage.JujuStorage()
|
|
461
461
|
else:
|
|
462
|
-
store =
|
|
462
|
+
store = _storage.SQLiteStorage(charm_state_path)
|
|
463
463
|
return store
|
|
464
464
|
|
|
465
465
|
def _make_framework(self, dispatcher: _Dispatcher):
|
|
@@ -471,11 +471,11 @@ class _Manager:
|
|
|
471
471
|
else:
|
|
472
472
|
broken_relation_id = None
|
|
473
473
|
|
|
474
|
-
model =
|
|
474
|
+
model = _model.Model(
|
|
475
475
|
self._charm_meta, self._model_backend, broken_relation_id=broken_relation_id
|
|
476
476
|
)
|
|
477
477
|
store = self._make_storage(dispatcher)
|
|
478
|
-
framework =
|
|
478
|
+
framework = _framework.Framework(
|
|
479
479
|
store,
|
|
480
480
|
self._charm_root,
|
|
481
481
|
self._charm_meta,
|
|
@@ -501,9 +501,9 @@ class _Manager:
|
|
|
501
501
|
# Emit the Juju event.
|
|
502
502
|
self._emit_charm_event(self.dispatcher.event_name)
|
|
503
503
|
# Emit collect-status events.
|
|
504
|
-
|
|
504
|
+
_charm._evaluate_status(self.charm)
|
|
505
505
|
|
|
506
|
-
def _get_event_to_emit(self, event_name: str) -> Optional[
|
|
506
|
+
def _get_event_to_emit(self, event_name: str) -> Optional[_framework.BoundEvent]:
|
|
507
507
|
try:
|
|
508
508
|
return getattr(self.charm.on, event_name)
|
|
509
509
|
except AttributeError:
|
|
@@ -547,7 +547,7 @@ class _Manager:
|
|
|
547
547
|
self.framework.close()
|
|
548
548
|
|
|
549
549
|
|
|
550
|
-
def main(charm_class: Type[
|
|
550
|
+
def main(charm_class: Type[_charm.CharmBase], use_juju_for_storage: Optional[bool] = None):
|
|
551
551
|
"""Set up the charm and dispatch the observed event.
|
|
552
552
|
|
|
553
553
|
See `ops.main() <#ops-main-entry-point>`_ for details.
|
|
@@ -58,16 +58,16 @@ from typing import (
|
|
|
58
58
|
cast,
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
from
|
|
62
|
-
from
|
|
63
|
-
from
|
|
64
|
-
from
|
|
65
|
-
from
|
|
66
|
-
from
|
|
61
|
+
from .. import charm, framework, model, pebble, storage
|
|
62
|
+
from ..charm import CharmBase, CharmMeta, RelationRole
|
|
63
|
+
from ..jujucontext import _JujuContext
|
|
64
|
+
from ..model import Container, RelationNotFoundError, StatusName, _NetworkDict
|
|
65
|
+
from ..pebble import ExecProcess
|
|
66
|
+
from . import yaml
|
|
67
67
|
|
|
68
68
|
if typing.TYPE_CHECKING:
|
|
69
69
|
try:
|
|
70
|
-
from
|
|
70
|
+
from ..testing import State # type: ignore
|
|
71
71
|
except ImportError:
|
|
72
72
|
# This is used in the ActionFailed type annotations: it will never be
|
|
73
73
|
# used in this case, because it's only relevant when ops.testing has
|
|
@@ -3213,75 +3213,102 @@ class _TestingPebbleClient:
|
|
|
3213
3213
|
self._new_perform_check(info)
|
|
3214
3214
|
return self.autostart_services(timeout, delay)
|
|
3215
3215
|
|
|
3216
|
+
def _new_service_change(self, kind: pebble.ChangeKind, services: List[str]) -> pebble.Change:
|
|
3217
|
+
now = datetime.datetime.now()
|
|
3218
|
+
if kind == pebble.ChangeKind.START:
|
|
3219
|
+
action = 'Start'
|
|
3220
|
+
elif kind == pebble.ChangeKind.STOP:
|
|
3221
|
+
action = 'Stop'
|
|
3222
|
+
elif kind == pebble.ChangeKind.RESTART:
|
|
3223
|
+
action = 'Restart'
|
|
3224
|
+
else:
|
|
3225
|
+
raise ValueError(f'unknown kind {kind}')
|
|
3226
|
+
summary = f'{action} service {services[0]}'
|
|
3227
|
+
if len(services) > 1:
|
|
3228
|
+
summary += f' and {len(services) - 1} more'
|
|
3229
|
+
change = pebble.Change(
|
|
3230
|
+
pebble.ChangeID(str(uuid.uuid4())),
|
|
3231
|
+
kind.value,
|
|
3232
|
+
summary=summary,
|
|
3233
|
+
status=pebble.ChangeStatus.DONE.value,
|
|
3234
|
+
tasks=[],
|
|
3235
|
+
ready=True,
|
|
3236
|
+
err=None,
|
|
3237
|
+
spawn_time=now,
|
|
3238
|
+
ready_time=now,
|
|
3239
|
+
)
|
|
3240
|
+
self._changes[change.id] = change
|
|
3241
|
+
return change
|
|
3242
|
+
|
|
3216
3243
|
def start_services(
|
|
3217
3244
|
self,
|
|
3218
3245
|
services: List[str],
|
|
3219
3246
|
timeout: float = 30.0,
|
|
3220
3247
|
delay: float = 0.1,
|
|
3221
|
-
):
|
|
3248
|
+
) -> pebble.ChangeID:
|
|
3222
3249
|
# A common mistake is to pass just the name of a service, rather than a list of services,
|
|
3223
3250
|
# so trap that so it is caught quickly.
|
|
3224
3251
|
if isinstance(services, str):
|
|
3225
3252
|
raise TypeError(f'start_services should take a list of names, not just "{services}"')
|
|
3226
|
-
|
|
3253
|
+
if not services:
|
|
3254
|
+
raise self._api_error(400, 'must specify services for start action')
|
|
3227
3255
|
self._check_connection()
|
|
3228
3256
|
|
|
3229
|
-
# Note: jam 2021-04-20 We don't implement ChangeID, but the default caller of this is
|
|
3230
|
-
# Container.start() which currently ignores the return value
|
|
3231
3257
|
known_services = self._render_services()
|
|
3232
3258
|
# Names appear to be validated before any are activated, so do two passes
|
|
3233
3259
|
for name in services:
|
|
3234
3260
|
if name not in known_services:
|
|
3235
|
-
|
|
3236
|
-
raise RuntimeError(f'400 Bad Request: service "{name}" does not exist')
|
|
3261
|
+
raise self._api_error(400, f'cannot start services: service {name} does not exist')
|
|
3237
3262
|
for name in services:
|
|
3238
3263
|
self._service_status[name] = pebble.ServiceStatus.ACTIVE
|
|
3264
|
+
return self._new_service_change(pebble.ChangeKind.START, services).id
|
|
3239
3265
|
|
|
3240
3266
|
def stop_services(
|
|
3241
3267
|
self,
|
|
3242
3268
|
services: List[str],
|
|
3243
3269
|
timeout: float = 30.0,
|
|
3244
3270
|
delay: float = 0.1,
|
|
3245
|
-
):
|
|
3246
|
-
#
|
|
3271
|
+
) -> pebble.ChangeID:
|
|
3272
|
+
# Handle a common mistake of passing just a name rather than a list of names.
|
|
3247
3273
|
if isinstance(services, str):
|
|
3248
3274
|
raise TypeError(f'stop_services should take a list of names, not just "{services}"')
|
|
3275
|
+
if not services:
|
|
3276
|
+
raise self._api_error(400, 'must specify services for start action')
|
|
3249
3277
|
|
|
3250
3278
|
self._check_connection()
|
|
3251
3279
|
|
|
3252
|
-
# Note: jam 2021-04-20 We don't implement ChangeID, but the default caller of this is
|
|
3253
|
-
# Container.stop() which currently ignores the return value
|
|
3254
3280
|
known_services = self._render_services()
|
|
3255
3281
|
for name in services:
|
|
3256
3282
|
if name not in known_services:
|
|
3257
|
-
|
|
3258
|
-
# 400 Bad Request: service "bal" does not exist
|
|
3259
|
-
raise RuntimeError(f'400 Bad Request: service "{name}" does not exist')
|
|
3283
|
+
raise self._api_error(400, f'cannot stop services: service {name} does not exist')
|
|
3260
3284
|
for name in services:
|
|
3261
3285
|
self._service_status[name] = pebble.ServiceStatus.INACTIVE
|
|
3286
|
+
return self._new_service_change(pebble.ChangeKind.STOP, services).id
|
|
3262
3287
|
|
|
3263
3288
|
def restart_services(
|
|
3264
3289
|
self,
|
|
3265
3290
|
services: List[str],
|
|
3266
3291
|
timeout: float = 30.0,
|
|
3267
3292
|
delay: float = 0.1,
|
|
3268
|
-
):
|
|
3269
|
-
#
|
|
3293
|
+
) -> pebble.ChangeID:
|
|
3294
|
+
# Handle a common mistake of passing just a name rather than a list of names.
|
|
3270
3295
|
if isinstance(services, str):
|
|
3271
3296
|
raise TypeError(f'restart_services should take a list of names, not just "{services}"')
|
|
3297
|
+
if not services:
|
|
3298
|
+
raise self._api_error(400, 'must specify services for start action')
|
|
3272
3299
|
|
|
3273
3300
|
self._check_connection()
|
|
3274
3301
|
|
|
3275
|
-
# Note: jam 2021-04-20 We don't implement ChangeID, but the default caller of this is
|
|
3276
|
-
# Container.restart() which currently ignores the return value
|
|
3277
3302
|
known_services = self._render_services()
|
|
3278
3303
|
for name in services:
|
|
3279
3304
|
if name not in known_services:
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3305
|
+
raise self._api_error(
|
|
3306
|
+
400,
|
|
3307
|
+
f'cannot restart services: service {name} does not exist',
|
|
3308
|
+
)
|
|
3283
3309
|
for name in services:
|
|
3284
3310
|
self._service_status[name] = pebble.ServiceStatus.ACTIVE
|
|
3311
|
+
return self._new_service_change(pebble.ChangeKind.RESTART, services).id
|
|
3285
3312
|
|
|
3286
3313
|
def wait_change(
|
|
3287
3314
|
self,
|
|
@@ -36,9 +36,9 @@ from typing import (
|
|
|
36
36
|
cast,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
from
|
|
40
|
-
from
|
|
41
|
-
from
|
|
39
|
+
from . import model
|
|
40
|
+
from ._private import yaml
|
|
41
|
+
from .framework import (
|
|
42
42
|
EventBase,
|
|
43
43
|
EventSource,
|
|
44
44
|
Framework,
|
|
@@ -1451,6 +1451,17 @@ class CharmMeta:
|
|
|
1451
1451
|
assumes: 'JujuAssumes'
|
|
1452
1452
|
"""Juju features this charm requires."""
|
|
1453
1453
|
|
|
1454
|
+
charm_user: Literal['root', 'sudoer', 'non-root']
|
|
1455
|
+
"""Type of user used to run the charm hook code.
|
|
1456
|
+
|
|
1457
|
+
The value of ``root`` ensures the charm runs as root. The value of
|
|
1458
|
+
``sudoer`` runs the charm as a user other than root with access to sudo to
|
|
1459
|
+
elevate its privileges. ``non-root`` ensures the charm does not run as root
|
|
1460
|
+
and also does not have ``sudo`` privileges.
|
|
1461
|
+
|
|
1462
|
+
.. jujuadded 3.6.0
|
|
1463
|
+
"""
|
|
1464
|
+
|
|
1454
1465
|
containers: Dict[str, 'ContainerMeta']
|
|
1455
1466
|
"""Container metadata for each defined container."""
|
|
1456
1467
|
|
|
@@ -1524,6 +1535,7 @@ class CharmMeta:
|
|
|
1524
1535
|
# Note that metadata v2 does not define min-juju-version ('assumes'
|
|
1525
1536
|
# should be used instead).
|
|
1526
1537
|
self.min_juju_version = raw_.get('min-juju-version')
|
|
1538
|
+
self.charm_user = raw_.get('charm-user', 'root')
|
|
1527
1539
|
self.requires = {
|
|
1528
1540
|
name: RelationMeta(RelationRole.requires, name, rel)
|
|
1529
1541
|
for name, rel in raw_.get('requires', {}).items()
|
|
@@ -47,9 +47,9 @@ from typing import (
|
|
|
47
47
|
Union,
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
from
|
|
51
|
-
from
|
|
52
|
-
from
|
|
50
|
+
from . import charm
|
|
51
|
+
from .model import Model, _ModelBackend
|
|
52
|
+
from .storage import JujuStorage, NoSnapshotError, SQLiteStorage
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
class Serializable(typing.Protocol):
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
from typing import Optional, Type
|
|
18
18
|
|
|
19
|
-
import ops.charm
|
|
20
|
-
|
|
21
19
|
from . import _main
|
|
20
|
+
from . import charm as _charm
|
|
22
21
|
|
|
23
22
|
# Re-export specific set of symbols that Scenario 6 imports from ops.main
|
|
24
23
|
from ._main import ( # noqa: F401
|
|
@@ -29,7 +28,7 @@ from ._main import ( # noqa: F401
|
|
|
29
28
|
)
|
|
30
29
|
|
|
31
30
|
|
|
32
|
-
def main(charm_class: Type[
|
|
31
|
+
def main(charm_class: Type[_charm.CharmBase], use_juju_for_storage: Optional[bool] = None):
|
|
33
32
|
"""Legacy entrypoint to set up the charm and dispatch the observed event.
|
|
34
33
|
|
|
35
34
|
.. deprecated:: 2.16.0
|
|
@@ -57,13 +57,13 @@ from typing import (
|
|
|
57
57
|
get_args,
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
import
|
|
61
|
-
|
|
62
|
-
from
|
|
63
|
-
from
|
|
64
|
-
from
|
|
60
|
+
from . import charm as _charm
|
|
61
|
+
from . import pebble
|
|
62
|
+
from ._private import timeconv, yaml
|
|
63
|
+
from .jujucontext import _JujuContext
|
|
64
|
+
from .jujuversion import JujuVersion
|
|
65
65
|
|
|
66
|
-
# JujuVersion is not used in
|
|
66
|
+
# JujuVersion is not used in this file, but there are charms that are importing JujuVersion
|
|
67
67
|
# from ops.model, so we keep it here.
|
|
68
68
|
_ = JujuVersion
|
|
69
69
|
|
|
@@ -82,7 +82,7 @@ _SETTABLE_STATUS_NAMES: Tuple[_SettableStatusName, ...] = get_args(_SettableStat
|
|
|
82
82
|
# mapping from relation name to a list of relation objects
|
|
83
83
|
_RelationMapping_Raw = Dict[str, Optional[List['Relation']]]
|
|
84
84
|
# mapping from container name to container metadata
|
|
85
|
-
_ContainerMeta_Raw = Dict[str, '
|
|
85
|
+
_ContainerMeta_Raw = Dict[str, '_charm.ContainerMeta']
|
|
86
86
|
|
|
87
87
|
# relation data is a string key: string value mapping so far as the
|
|
88
88
|
# controller is concerned
|
|
@@ -124,14 +124,14 @@ class Model:
|
|
|
124
124
|
|
|
125
125
|
def __init__(
|
|
126
126
|
self,
|
|
127
|
-
meta: '
|
|
127
|
+
meta: '_charm.CharmMeta',
|
|
128
128
|
backend: '_ModelBackend',
|
|
129
129
|
broken_relation_id: Optional[int] = None,
|
|
130
130
|
):
|
|
131
131
|
self._cache = _ModelCache(meta, backend)
|
|
132
132
|
self._backend = backend
|
|
133
133
|
self._unit = self.get_unit(self._backend.unit_name)
|
|
134
|
-
relations: Dict[str,
|
|
134
|
+
relations: Dict[str, _charm.RelationMeta] = meta.relations
|
|
135
135
|
self._relations = RelationMapping(
|
|
136
136
|
relations, self.unit, self._backend, self._cache, broken_relation_id=broken_relation_id
|
|
137
137
|
)
|
|
@@ -219,7 +219,7 @@ class Model:
|
|
|
219
219
|
return self._backend.model_uuid
|
|
220
220
|
|
|
221
221
|
@property
|
|
222
|
-
def juju_version(self) ->
|
|
222
|
+
def juju_version(self) -> JujuVersion:
|
|
223
223
|
"""Return the version of Juju that is running the model."""
|
|
224
224
|
return self._backend._juju_context.version
|
|
225
225
|
|
|
@@ -343,7 +343,7 @@ if typing.TYPE_CHECKING:
|
|
|
343
343
|
|
|
344
344
|
|
|
345
345
|
class _ModelCache:
|
|
346
|
-
def __init__(self, meta: '
|
|
346
|
+
def __init__(self, meta: '_charm.CharmMeta', backend: '_ModelBackend'):
|
|
347
347
|
self._meta = meta
|
|
348
348
|
self._backend = backend
|
|
349
349
|
self._secret_set_cache: collections.defaultdict[str, Dict[str, Any]] = (
|
|
@@ -383,7 +383,7 @@ class Application:
|
|
|
383
383
|
"""
|
|
384
384
|
|
|
385
385
|
def __init__(
|
|
386
|
-
self, name: str, meta: '
|
|
386
|
+
self, name: str, meta: '_charm.CharmMeta', backend: '_ModelBackend', cache: _ModelCache
|
|
387
387
|
):
|
|
388
388
|
self.name = name
|
|
389
389
|
self._backend = backend
|
|
@@ -559,7 +559,7 @@ class Unit:
|
|
|
559
559
|
def __init__(
|
|
560
560
|
self,
|
|
561
561
|
name: str,
|
|
562
|
-
meta: '
|
|
562
|
+
meta: '_charm.CharmMeta',
|
|
563
563
|
backend: '_ModelBackend',
|
|
564
564
|
cache: '_ModelCache',
|
|
565
565
|
):
|
|
@@ -908,7 +908,7 @@ class RelationMapping(Mapping[str, List['Relation']]):
|
|
|
908
908
|
|
|
909
909
|
def __init__(
|
|
910
910
|
self,
|
|
911
|
-
relations_meta: Dict[str, '
|
|
911
|
+
relations_meta: Dict[str, '_charm.RelationMeta'],
|
|
912
912
|
our_unit: 'Unit',
|
|
913
913
|
backend: '_ModelBackend',
|
|
914
914
|
cache: '_ModelCache',
|
|
@@ -1670,7 +1670,7 @@ class Relation:
|
|
|
1670
1670
|
|
|
1671
1671
|
This class should not be instantiated directly, instead use :meth:`Model.get_relation`,
|
|
1672
1672
|
:attr:`Model.relations`, or :attr:`ops.RelationEvent.relation`. This is principally used by
|
|
1673
|
-
:class:`ops.RelationMeta` to represent the relationships between charms.
|
|
1673
|
+
:class:`ops.charm.RelationMeta` to represent the relationships between charms.
|
|
1674
1674
|
"""
|
|
1675
1675
|
|
|
1676
1676
|
name: str
|
|
@@ -3962,7 +3962,7 @@ class LazyNotice:
|
|
|
3962
3962
|
self.type = type
|
|
3963
3963
|
self.key = key
|
|
3964
3964
|
|
|
3965
|
-
self._notice: Optional[
|
|
3965
|
+
self._notice: Optional[pebble.Notice] = None
|
|
3966
3966
|
|
|
3967
3967
|
def __repr__(self):
|
|
3968
3968
|
type_repr = self.type if isinstance(self.type, pebble.NoticeType) else repr(self.type)
|
|
@@ -3999,7 +3999,7 @@ class LazyCheckInfo:
|
|
|
3999
3999
|
def __init__(self, container: Container, name: str):
|
|
4000
4000
|
self._container = container
|
|
4001
4001
|
self.name = name
|
|
4002
|
-
self._info: Optional[
|
|
4002
|
+
self._info: Optional[pebble.CheckInfo] = None
|
|
4003
4003
|
|
|
4004
4004
|
def __repr__(self):
|
|
4005
4005
|
return f'LazyCheckInfo(name={self.name!r})'
|