charmlibs-interfaces-istio-request-auth 0.0.1__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.
@@ -0,0 +1,184 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+ cos-tool*
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .coverage-*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+ .report/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ #pdm.lock
115
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
116
+ # in version control.
117
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
118
+ .pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
123
+ __pypackages__/
124
+
125
+ # Celery stuff
126
+ celerybeat-schedule
127
+ celerybeat.pid
128
+
129
+ # SageMath parsed files
130
+ *.sage.py
131
+
132
+ # Environments
133
+ .env
134
+ .venv
135
+ env/
136
+ venv/
137
+ ENV/
138
+ env.bak/
139
+ venv.bak/
140
+
141
+ # Spyder project settings
142
+ .spyderproject
143
+ .spyproject
144
+
145
+ # Rope project settings
146
+ .ropeproject
147
+
148
+ # mkdocs documentation
149
+ /site
150
+
151
+ # mypy
152
+ .mypy_cache/
153
+ .dmypy.json
154
+ dmypy.json
155
+
156
+ # Pyre type checker
157
+ .pyre/
158
+
159
+ # pytype static type analyzer
160
+ .pytype/
161
+
162
+ # Cython debug symbols
163
+ cython_debug/
164
+
165
+ # PyCharm
166
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
169
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
170
+ .idea/
171
+
172
+ # PyPI configuration file
173
+ .pypirc
174
+
175
+ # packed charms
176
+ .packed
177
+
178
+ # temporary directory for packing charms
179
+ .tmp
180
+
181
+ # uv.lock from example libraries as we don't commit these
182
+ .example/**/uv.lock
183
+ .tutorial/**/uv.lock
184
+ interfaces/.example/**/uv.lock
@@ -0,0 +1,11 @@
1
+ ## 0.0.1 - 22 April 2026
2
+
3
+ Initial release.
4
+
5
+ The initial `istio-request-auth` interface library provides the following features:
6
+
7
+ - Lets charms share their trusted JWT issuers and header-claim mappings as jwt_rules with the `istio-ingress-k8s` charms
8
+ - Lets charms specify multiple trusted issuers and their corresponding header-claim mappings as a list of jwt_rules
9
+ - Provides convienience data models for describing JWTRules
10
+ - Models the top level data bag as datamodel
11
+
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: charmlibs-interfaces-istio-request-auth
3
+ Version: 0.0.1
4
+ Summary: The charmlibs.interfaces.istio_request_auth package.
5
+ Project-URL: Documentation, https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth
6
+ Project-URL: Repository, https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth
7
+ Project-URL: Issues, https://github.com/canonical/charmlibs/issues
8
+ Project-URL: Changelog, https://github.com/canonical/charmlibs/blob/main/interfaces/istio-request-auth/CHANGELOG.md
9
+ Author: Service Mesh
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: ops
17
+ Requires-Dist: pydantic>=2
18
+ Description-Content-Type: text/markdown
19
+
20
+ # charmlibs.interfaces.istio_request_auth
21
+
22
+ The `istio-request-auth` interface library.
23
+
24
+ To install, add `charmlibs-interfaces-istio-request-auth` to your Python dependencies. Then in your Python code, import as:
25
+
26
+ ```py
27
+ from charmlibs.interfaces import istio_request_auth
28
+ ```
29
+
30
+ See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth) for more.
@@ -0,0 +1,11 @@
1
+ # charmlibs.interfaces.istio_request_auth
2
+
3
+ The `istio-request-auth` interface library.
4
+
5
+ To install, add `charmlibs-interfaces-istio-request-auth` to your Python dependencies. Then in your Python code, import as:
6
+
7
+ ```py
8
+ from charmlibs.interfaces import istio_request_auth
9
+ ```
10
+
11
+ See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth) for more.
@@ -0,0 +1,74 @@
1
+ [project]
2
+ name = "charmlibs-interfaces-istio-request-auth"
3
+ description = "The charmlibs.interfaces.istio_request_auth package."
4
+ readme = "README.md"
5
+ requires-python = ">=3.10"
6
+ authors = [
7
+ {name="Service Mesh"},
8
+ ]
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Intended Audience :: Developers",
13
+ "Operating System :: POSIX :: Linux",
14
+ "Development Status :: 5 - Production/Stable",
15
+ ]
16
+ dynamic = ["version"]
17
+ dependencies = [
18
+ "ops",
19
+ "pydantic>=2",
20
+ ]
21
+
22
+ [dependency-groups]
23
+ lint = [ # installed for `just lint interfaces/istio-request-auth` (unit, functional, and integration are also installed)
24
+ # "typing_extensions",
25
+ ]
26
+ unit = [ # installed for `just unit interfaces/istio-request-auth`
27
+ "ops[testing]",
28
+ ]
29
+ functional = [ # installed for `just functional interfaces/istio-request-auth`
30
+ ]
31
+ integration = [ # installed for `just integration interfaces/istio-request-auth`
32
+ "jubilant",
33
+ ]
34
+
35
+ [project.urls]
36
+ "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth"
37
+ "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth"
38
+ "Issues" = "https://github.com/canonical/charmlibs/issues"
39
+ "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio-request-auth/CHANGELOG.md"
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/charmlibs"]
47
+
48
+ [tool.hatch.version]
49
+ path = "src/charmlibs/interfaces/istio_request_auth/_version.py"
50
+
51
+ [tool.ruff]
52
+ extend = "../../pyproject.toml"
53
+ src = ["src", "tests/unit", "tests/functional", "tests/integration"] # correctly sort local imports in tests
54
+
55
+ [tool.ruff.lint.extend-per-file-ignores]
56
+ # add additional per-file-ignores here to avoid overriding repo-level config
57
+ "tests/**/*" = [
58
+ # "E501", # line too long
59
+ ]
60
+
61
+ [tool.pyright]
62
+ extends = "../../pyproject.toml"
63
+ include = ["src", "tests"]
64
+ pythonVersion = "3.10" # check no python > 3.10 features are used
65
+
66
+ [tool.charmlibs.functional]
67
+ ubuntu = [] # ubuntu versions to run functional tests with, e.g. "24.04" (defaults to just "latest")
68
+ pebble = [] # pebble versions to run functional tests with, e.g. "v1.0.0", "master" (defaults to no pebble versions)
69
+ sudo = false # whether to run functional tests with sudo (defaults to false)
70
+
71
+ [tool.charmlibs.integration]
72
+ # tags to run integration tests with (defaults to running once with no tag, i.e. tags = [''])
73
+ # Available in CI in tests/integration/pack.sh and integration tests as CHARMLIBS_TAG
74
+ tags = [] # Not used by the pack.sh and integration tests generated by the template
@@ -0,0 +1,105 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Istio request authentication interface library.
16
+
17
+ This library provides the provider and requirer sides of the ``istio-request-auth``
18
+ relation interface for configuring Istio
19
+ `RequestAuthentication <https://istio.io/latest/docs/reference/config/security/request_authentication/>`_
20
+ resources via relation data (JWT rules, JWKS endpoints, claim-to-header mapping).
21
+
22
+ What is this library for?
23
+ =========================
24
+
25
+ The `istio-ingress-k8s <https://github.com/canonical/istio-ingress-k8s-operator/>`_ charm
26
+ wraps a `Kubernetes Gateway API <https://gateway-api.sigs.k8s.io/>`_ of class ``istio``. It
27
+ can connect to an OAuth 2.0 provider like the ``oauth2-proxy`` charm via the ``forward-auth``
28
+ relation to forward requests via an authentication stack for user authentication.
29
+
30
+ Istio also natively supports validating a pre-generated JWT against an issuer using a
31
+ ``RequestAuthentication`` Kubernetes resource. This means a request containing a JWT in
32
+ the header can be natively authenticated by Istio instead of taking a detour via the
33
+ authentication stack, and the claims from the token are parsed and added as headers to
34
+ the downstream request. The `RequestAuthentication` resource is purely an Istio concept
35
+ offered by the CRDs native to Istio. Hence the interface is named with an `istio-` prefix.
36
+
37
+ For applications to take advantage of this feature, they need to tell Istio which issuers
38
+ they trust and which headers they want the claims in the token to be mapped to. To enable
39
+ this information exchange and add the appropriate ``RequestAuthentication`` resource, the
40
+ ``istio-request-auth`` interface library is introduced.
41
+
42
+ Security note
43
+ =============
44
+
45
+ If a requirer is connected but has not provided valid (non-empty) ``jwt_rules``, no
46
+ ``RequestAuthentication`` resource is created. This could allow unauthenticated
47
+ traffic. The provider exposes :meth:`~IstioRequestAuthProvider.get_connected_apps`
48
+ so the ingress charm can detect such applications and drop their ingress until valid
49
+ rules are provided.
50
+
51
+ Requirer usage::
52
+
53
+ from charmlibs.interfaces.istio_request_auth import (
54
+ ClaimToHeader,
55
+ JWTRule,
56
+ IstioRequestAuthRequirer,
57
+ )
58
+
59
+ class MyAppCharm(CharmBase):
60
+ def __init__(self, *args):
61
+ super().__init__(*args)
62
+ self.request_auth = IstioRequestAuthRequirer(self)
63
+
64
+ def _publish_rules(self):
65
+ self.request_auth.publish_data([
66
+ JWTRule(
67
+ issuer="https://accounts.example.com",
68
+ claim_to_headers=[
69
+ ClaimToHeader(header="x-user-email", claim="email"),
70
+ ],
71
+ ),
72
+ ])
73
+
74
+ Provider usage::
75
+
76
+ from charmlibs.interfaces.istio_request_auth import IstioRequestAuthProvider
77
+
78
+ class MyIngressCharm(CharmBase):
79
+ def __init__(self, *args):
80
+ super().__init__(*args)
81
+ self.request_auth = IstioRequestAuthProvider(self)
82
+
83
+ def _reconcile(self):
84
+ valid = self.request_auth.get_data()
85
+ connected = self.request_auth.get_connected_apps()
86
+ invalid_apps = connected - set(valid.keys())
87
+ # Drop ingress for invalid_apps, create RequestAuthentication for valid
88
+ """
89
+
90
+ from ._istio_request_auth import (
91
+ ClaimToHeader,
92
+ FromHeader,
93
+ IstioRequestAuthProvider,
94
+ IstioRequestAuthRequirer,
95
+ JWTRule,
96
+ )
97
+ from ._version import __version__ as __version__
98
+
99
+ __all__ = [
100
+ 'ClaimToHeader',
101
+ 'FromHeader',
102
+ 'IstioRequestAuthProvider',
103
+ 'IstioRequestAuthRequirer',
104
+ 'JWTRule',
105
+ ]
@@ -0,0 +1,221 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Istio request authentication interface implementation.
16
+
17
+ Migrated from charmed-service-mesh-helpers interfaces/request_auth.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import TYPE_CHECKING
24
+
25
+ from ops.framework import Object
26
+ from pydantic import BaseModel, ConfigDict, Field
27
+
28
+ if TYPE_CHECKING:
29
+ from ops import CharmBase
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class ClaimToHeader(BaseModel):
35
+ """Maps a JWT claim to a request header."""
36
+
37
+ model_config = ConfigDict(frozen=True)
38
+
39
+ header: str = Field(description='Target request header name')
40
+ claim: str = Field(description='JWT claim name to extract')
41
+
42
+
43
+ class FromHeader(BaseModel):
44
+ """Specifies a header location from which to extract a JWT."""
45
+
46
+ model_config = ConfigDict(frozen=True)
47
+
48
+ name: str = Field(description='Header name')
49
+ prefix: str | None = None
50
+
51
+
52
+ class JWTRule(BaseModel):
53
+ """A single JWT validation rule provided by the requiring app."""
54
+
55
+ model_config = ConfigDict(frozen=True)
56
+
57
+ # The following fields mirror the JWTRule entry in the RequestAuthentication CRD.
58
+ # For details check https://istio.io/latest/docs/reference/config/security/request_authentication/#JWTRule
59
+ issuer: str = Field(description='Issuer URL for token validation')
60
+ jwks_uri: str | None = None
61
+ audiences: list[str] | None = None
62
+ forward_original_token: bool | None = None
63
+ # claim_to_headers allows mapping a single claim to multiple headers or
64
+ # multiple claims to the same header (concatenated with comma; missing
65
+ # claims are skipped).
66
+ claim_to_headers: list[ClaimToHeader] | None = None
67
+ # from_headers allows defining multiple potential header sources.
68
+ # The first one with a valid token will be used.
69
+ from_headers: list[FromHeader] | None = None
70
+
71
+
72
+ class _RequestAuthData(BaseModel):
73
+ """Top-level databag model for the istio-request-auth relation.
74
+
75
+ Each field maps directly to a top-level key in the Juju application databag.
76
+ Use ``ops.Relation.load`` / ``ops.Relation.save`` to (de)serialise.
77
+
78
+ ``jwt_rules`` defaults to ``None``. When it is missing, ``None``, or an
79
+ empty list the provider side treats the relation as not-ready and skips
80
+ the application.
81
+ """
82
+
83
+ model_config = ConfigDict(frozen=True)
84
+
85
+ jwt_rules: list[JWTRule] | None = Field(
86
+ default=None,
87
+ description='List of JWT validation rules. Missing or empty means not ready.',
88
+ )
89
+
90
+
91
+ class IstioRequestAuthProvider(Object):
92
+ """Provider side of the istio-request-auth interface.
93
+
94
+ Used by the ingress charm to read JWT authentication rules from all related
95
+ applications.
96
+
97
+ Applications that are connected but have not provided valid (non-empty)
98
+ ``jwt_rules`` are excluded from :meth:`get_data` but included in
99
+ :meth:`get_connected_apps`. Consumers can compare the two sets to
100
+ identify applications that have not yet provided data::
101
+
102
+ valid = provider.get_data()
103
+ connected = provider.get_connected_apps()
104
+ apps_without_data = connected - set(valid.keys())
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ charm: CharmBase,
110
+ relation_name: str = 'istio-request-auth',
111
+ ):
112
+ """Initialize the IstioRequestAuthProvider.
113
+
114
+ Args:
115
+ charm: The charm that owns this provider.
116
+ relation_name: Name of the relation (default: "istio-request-auth").
117
+ """
118
+ super().__init__(charm, relation_name)
119
+ self._charm = charm
120
+ self._relation_name = relation_name
121
+
122
+ @property
123
+ def is_ready(self) -> bool:
124
+ """Check if any related application has provided valid request auth data.
125
+
126
+ Returns:
127
+ True if at least one requirer has published non-empty jwt_rules.
128
+ """
129
+ return bool(self.get_data())
130
+
131
+ def get_connected_apps(self) -> set[str]:
132
+ """Return the names of all applications connected over the relation.
133
+
134
+ This includes apps that have not yet provided valid data.
135
+ """
136
+ apps: set[str] = set()
137
+ for relation in self._charm.model.relations.get(self._relation_name, []):
138
+ if relation.app:
139
+ apps.add(relation.app.name)
140
+ return apps
141
+
142
+ def get_data(self) -> dict[str, list[JWTRule]]:
143
+ """Retrieve valid JWT rules from all related applications.
144
+
145
+ Uses ``ops.Relation.load`` to deserialise each application's databag
146
+ into :class:`RequestAuthData`. Only applications whose databag
147
+ contains a non-empty ``jwt_rules`` list are included.
148
+
149
+ Returns:
150
+ A dict mapping application name to its list of ``JWTRule`` objects.
151
+ """
152
+ result: dict[str, list[JWTRule]] = {}
153
+ relations = self._charm.model.relations.get(self._relation_name, [])
154
+
155
+ for relation in relations:
156
+ if not relation.app:
157
+ continue
158
+
159
+ app_name = relation.app.name
160
+
161
+ try:
162
+ data = relation.load(_RequestAuthData, relation.app)
163
+ except Exception as e:
164
+ logger.exception(
165
+ 'Failed to parse databag from application %s: %s',
166
+ app_name,
167
+ e,
168
+ )
169
+ continue
170
+
171
+ if not data.jwt_rules:
172
+ logger.warning(
173
+ 'Application %s has not provided jwt_rules',
174
+ app_name,
175
+ )
176
+ continue
177
+
178
+ result[app_name] = data.jwt_rules
179
+
180
+ return result
181
+
182
+
183
+ class IstioRequestAuthRequirer(Object):
184
+ """Requirer side of the istio-request-auth interface.
185
+
186
+ Used by downstream applications to publish their JWT authentication rules
187
+ to the ingress charm.
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ charm: CharmBase,
193
+ relation_name: str = 'istio-request-auth',
194
+ ):
195
+ """Initialize the IstioRequestAuthRequirer.
196
+
197
+ Args:
198
+ charm: The charm that owns this requirer.
199
+ relation_name: Name of the relation (default: "istio-request-auth").
200
+ """
201
+ super().__init__(charm, relation_name)
202
+ self._charm = charm
203
+ self._relation_name = relation_name
204
+
205
+ def publish_data(self, jwt_rules: list[JWTRule]) -> None:
206
+ """Publish JWT rules to the provider.
207
+
208
+ Uses ``ops.Relation.save`` to write a :class:`RequestAuthData` instance
209
+ to the application databag so ``jwt_rules`` appears as a top-level key.
210
+
211
+ Args:
212
+ jwt_rules: The JWT validation rules to publish.
213
+ """
214
+ if not self._charm.unit.is_leader():
215
+ logger.debug('Not leader, skipping request auth data publication')
216
+ return
217
+
218
+ data = _RequestAuthData(jwt_rules=jwt_rules)
219
+
220
+ for relation in self._charm.model.relations.get(self._relation_name, []):
221
+ relation.save(data, self._charm.app)
@@ -0,0 +1,15 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ __version__ = '0.0.1'
@@ -0,0 +1,15 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Fixtures for unit tests, typically mocking out parts of the external system."""