dpcharmlibs-interfaces 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.
Files changed (101) hide show
  1. dpcharmlibs_interfaces-1.0.0/.gitignore +184 -0
  2. dpcharmlibs_interfaces-1.0.0/CHANGELOG.md +3 -0
  3. dpcharmlibs_interfaces-1.0.0/PKG-INFO +266 -0
  4. dpcharmlibs_interfaces-1.0.0/README.md +249 -0
  5. dpcharmlibs_interfaces-1.0.0/pyproject.toml +97 -0
  6. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/__init__.py +434 -0
  7. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/_version.py +15 -0
  8. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/diff.py +94 -0
  9. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/errors.py +34 -0
  10. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/events.py +333 -0
  11. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/handlers.py +1142 -0
  12. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/models.py +518 -0
  13. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/py.typed +0 -0
  14. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/repository.py +521 -0
  15. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/repository_interfaces.py +210 -0
  16. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/secrets.py +202 -0
  17. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/types.py +61 -0
  18. dpcharmlibs_interfaces-1.0.0/src/dpcharmlibs/interfaces/utils.py +67 -0
  19. dpcharmlibs_interfaces-1.0.0/tests/integration/__init__.py +13 -0
  20. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/actions.yaml +4 -0
  21. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/actions.yaml +78 -0
  22. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/charmcraft.yaml +25 -0
  23. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/config.yaml +5 -0
  24. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/library/README.md +1 -0
  25. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/library/pyproject.toml +1 -0
  26. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/metadata.yaml +40 -0
  27. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/pyproject.toml +18 -0
  28. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/src/charm.py +875 -0
  29. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/src/common.py +1 -0
  30. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/application-charm/src/etcd_requires.py +176 -0
  31. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/actions.yaml +1 -0
  32. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/charmcraft.yaml +12 -0
  33. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/lib/charms/data_platform_libs/v0/data_interfaces.py +5782 -0
  34. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/library/README.md +1 -0
  35. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/library/pyproject.toml +1 -0
  36. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/metadata.yaml +12 -0
  37. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/pyproject.toml +14 -0
  38. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/src/charm.py +65 -0
  39. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/backward-compatibility-charm/src/common.py +1 -0
  40. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/common.py +40 -0
  41. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/actions.yaml +175 -0
  42. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/charmcraft.yaml +24 -0
  43. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/library/README.md +1 -0
  44. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/library/pyproject.toml +1 -0
  45. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/metadata.yaml +34 -0
  46. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/pyproject.toml +16 -0
  47. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/src/charm.py +523 -0
  48. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/src/common.py +1 -0
  49. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/database-charm/src/status-schema.json +26 -0
  50. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/actions.yaml +128 -0
  51. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/charmcraft.yaml +23 -0
  52. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/library/README.md +1 -0
  53. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/library/pyproject.toml +1 -0
  54. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/metadata.yaml +34 -0
  55. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/pyproject.toml +18 -0
  56. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/src/charm.py +231 -0
  57. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/dummy-database-charm/src/common.py +1 -0
  58. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/actions.yaml +27 -0
  59. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/charmcraft.yaml +12 -0
  60. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/library/README.md +1 -0
  61. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/library/pyproject.toml +1 -0
  62. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/metadata.yaml +16 -0
  63. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/pyproject.toml +14 -0
  64. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/src/charm.py +231 -0
  65. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-charm/src/common.py +1 -0
  66. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/actions.yaml +15 -0
  67. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/charmcraft.yaml +12 -0
  68. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/library/README.md +1 -0
  69. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/library/pyproject.toml +1 -0
  70. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/metadata.yaml +16 -0
  71. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/pyproject.toml +14 -0
  72. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/src/charm.py +169 -0
  73. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/kafka-connect-charm/src/common.py +1 -0
  74. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/actions.yaml +6 -0
  75. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/charmcraft.yaml +12 -0
  76. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/library/README.md +1 -0
  77. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/library/pyproject.toml +1 -0
  78. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/metadata.yaml +16 -0
  79. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/pyproject.toml +14 -0
  80. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/src/charm.py +172 -0
  81. dpcharmlibs_interfaces-1.0.0/tests/integration/charms/opensearch-charm/src/common.py +1 -0
  82. dpcharmlibs_interfaces-1.0.0/tests/integration/conftest.py +254 -0
  83. dpcharmlibs_interfaces-1.0.0/tests/integration/helpers.py +300 -0
  84. dpcharmlibs_interfaces-1.0.0/tests/integration/jubilant_helpers.py +144 -0
  85. dpcharmlibs_interfaces-1.0.0/tests/integration/pack.sh +28 -0
  86. dpcharmlibs_interfaces-1.0.0/tests/integration/scripts/install_etcdctl.sh +11 -0
  87. dpcharmlibs_interfaces-1.0.0/tests/integration/test_backward_compatibility_charm.py +87 -0
  88. dpcharmlibs_interfaces-1.0.0/tests/integration/test_charm.py +1324 -0
  89. dpcharmlibs_interfaces-1.0.0/tests/integration/test_etcd_charm.py +217 -0
  90. dpcharmlibs_interfaces-1.0.0/tests/integration/test_kafka_charm.py +205 -0
  91. dpcharmlibs_interfaces-1.0.0/tests/integration/test_kafka_connect_charm.py +152 -0
  92. dpcharmlibs_interfaces-1.0.0/tests/integration/test_opensearch_charm.py +132 -0
  93. dpcharmlibs_interfaces-1.0.0/tests/integration/test_status.py +176 -0
  94. dpcharmlibs_interfaces-1.0.0/tests/unit/__init__.py +13 -0
  95. dpcharmlibs_interfaces-1.0.0/tests/unit/conftest.py +55 -0
  96. dpcharmlibs_interfaces-1.0.0/tests/unit/helpers.py +75 -0
  97. dpcharmlibs_interfaces-1.0.0/tests/unit/test_data_interfaces.py +1780 -0
  98. dpcharmlibs_interfaces-1.0.0/tests/unit/test_status.py +234 -0
  99. dpcharmlibs_interfaces-1.0.0/tests/unit/test_version.py +21 -0
  100. dpcharmlibs_interfaces-1.0.0/tests/unit/test_version_in_charm.py +38 -0
  101. dpcharmlibs_interfaces-1.0.0/uv.lock +1688 -0
@@ -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
+ # file for storing tests.
182
+ .collected
183
+
184
+ !**/charms/*/lib/
@@ -0,0 +1,3 @@
1
+ # 1.0.0 - 24 March 2026
2
+
3
+ * Initial release of the library
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: dpcharmlibs-interfaces
3
+ Version: 1.0.0
4
+ Summary: The dpcharmlibs.interfaces package.
5
+ Project-URL: Repository, https://github.com/canonical/data-platform-charmlibs
6
+ Project-URL: Issues, https://github.com/canonical/data-platform-charmlibs/issues
7
+ Author: The Data Platform team at Canonical Ltd.
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: ops<4,>=3
15
+ Requires-Dist: pydantic<3,>=2.11
16
+ Description-Content-Type: text/markdown
17
+
18
+ # dpcharmlibs.interfaces: Library to manage the relation for the data-platform products
19
+
20
+ The `interfaces` library.
21
+
22
+ To install, add `dpcharmlibs-interfaces` to your Python dependencies. Then in your Python code, import as:
23
+
24
+ ```py
25
+ from dpcharmlibs import interfaces
26
+ ```
27
+
28
+ ## Abstract
29
+
30
+ This V1 has been specified in
31
+ <https://docs.google.com/document/d/1lnuonWnoQb36RWYwfHOBwU0VClLbawpTISXIC_yNKYo>,
32
+ and should be backward compatible with v0 clients.
33
+
34
+ This library contains the Requires and Provides classes for handling the relation
35
+ between an application and multiple managed application supported by the data-team:
36
+ MySQL, Postgresql, MongoDB, Redis, Kafka, and Karapace.
37
+
38
+ ## Components
39
+
40
+ ### Models
41
+
42
+ This library exposes basic default models that can be used in most cases.
43
+ If you need more complex models, you can subclass them.
44
+
45
+ ```python
46
+ from dpcharmlibs.interfaces import RequirerCommonModel, ExtraSecretStr
47
+
48
+ class ExtendedCommonModel(RequirerCommonModel):
49
+ operator_password: ExtraSecretStr
50
+ ```
51
+
52
+ Secret groups are handled using annotated types.
53
+ If you wish to add extra secret groups, please follow the following model.
54
+ The string metadata represents the secret group name, and `OptionalSecretStr` is a TypeAlias for
55
+ `SecretStr | None`. Finally, `SecretStr` represents a field validating the URI pattern `secret:.*`
56
+
57
+ ```python
58
+ MyGroupSecretStr = Annotated[OptionalSecretStr, Field(exclude=True, default=None), "mygroup"]
59
+ ```
60
+
61
+ Fields not specified as OptionalSecretStr and extended with a group name in the metadata will NOT
62
+ get serialised.
63
+
64
+ ### Requirer Charm
65
+
66
+ This library is a uniform interface to a selection of common database
67
+ metadata, with added custom events that add convenience to database management,
68
+ and methods to consume the application related data.
69
+
70
+ ```python
71
+ from dpcharmlibs.interfaces import (
72
+ RequirerCommonModel,
73
+ RequirerDataContractV1,
74
+ ResourceCreatedEvent,
75
+ ResourceEntityCreatedEvent,
76
+ ResourceProviderModel,
77
+ ResourceRequirerEventHandler,
78
+ )
79
+
80
+ class ClientCharm(CharmBase):
81
+ # Database charm that accepts connections from application charms.
82
+ def __init__(self, *args) -> None:
83
+ super().__init__(*args)
84
+
85
+ requests = [
86
+ RequirerCommonModel(
87
+ resource="clientdb",
88
+ ),
89
+ RequirerCommonModel(
90
+ resource="clientbis",
91
+ ),
92
+ RequirerCommonModel(
93
+ entity_type="USER",
94
+ )
95
+ ]
96
+ self.database = ResourceRequirerEventHandler(
97
+ self,"database", requests, response_model=ResourceProviderModel
98
+ )
99
+ self.framework.observe(self.database.on.resource_created, self._on_resource_created)
100
+ self.framework.observe(self.database.on.resource_entity_created, self._on_entity_created)
101
+
102
+ def _on_resource_created(self, event: ResourceCreatedEvent) -> None:
103
+ # Event triggered when a new database is created.
104
+ relation_id = event.relation.id
105
+ response = event.response # This is the response model
106
+
107
+ username = event.response.username
108
+ password = event.response.password
109
+ ...
110
+
111
+ def _on_entity_created(self, event: ResourceCreatedEvent) -> None:
112
+ # Event triggered when a new entity is created.
113
+ ...
114
+ ```
115
+
116
+ Compared to V0, this library makes heavy use of pydantic models, and allows for
117
+ multiple requests, specified as a list.
118
+ On the Requirer side, each response will trigger one custom event for that response.
119
+ This way, it allows for more strategic events to be emitted according to the request.
120
+
121
+ As show above, the library provides some custom events to handle specific situations,
122
+ which are listed below:
123
+
124
+ - resource_created: event emitted when the requested database is created.
125
+ - resource_entity_created: event emitted when the requested entity is created.
126
+ - endpoints_changed: event emitted when the read/write endpoints of
127
+ the database have changed.
128
+ - read_only_endpoints_changed: event emitted when the read-only endpoints of
129
+ the database have changed. Event is not triggered if read/write endpoints
130
+ changed too.
131
+
132
+ If it is needed to connect multiple database clusters to the same relation endpoint
133
+ the application charm can implement the same code as if it would connect to only
134
+ one database cluster (like the above code example).
135
+
136
+ To differentiate multiple clusters connected to the same relation endpoint
137
+ the application charm can use the name of the remote application:
138
+
139
+ ```python
140
+ def _on_resource_created(self, event: ResourceCreatedEvent) -> None:
141
+ # Get the remote app name of the cluster that triggered this event
142
+ cluster = event.relation.app.name
143
+ ```
144
+
145
+ It is also possible to provide an alias for each different database cluster/relation.
146
+
147
+ So, it is possible to differentiate the clusters in two ways.
148
+
149
+ The first is to use the remote application name, with `event.relation.app.name`.
150
+
151
+ The second way is to use different event handlers to handle each cluster events.
152
+
153
+ The implementation would be something like the following code:
154
+
155
+ ```python
156
+ from dpcharmlibs.interfaces import (
157
+ RequirerCommonModel,
158
+ RequirerDataContractV1,
159
+ ResourceCreatedEvent,
160
+ ResourceEntityCreatedEvent,
161
+ ResourceProviderModel,
162
+ ResourceRequirerEventHandler,
163
+ )
164
+
165
+ class ApplicationCharm(CharmBase):
166
+ # Application charm that connects to database charms.
167
+
168
+ def __init__(self, *args):
169
+ super().__init__(*args)
170
+
171
+ requests = [
172
+ RequirerCommonModel(
173
+ resource="clientdb",
174
+ ),
175
+ RequirerCommonModel(
176
+ resource="clientbis",
177
+ ),
178
+ ]
179
+ # Define the cluster aliases and one handler for each cluster database
180
+ # created event.
181
+ self.database = ResourceRequirerEventHandler(
182
+ self,
183
+ relation_name="database"
184
+ relations_aliases = ["cluster1", "cluster2"],
185
+ requests=
186
+ )
187
+ self.framework.observe(
188
+ self.database.on.cluster1_resource_created, self._on_cluster1_resource_created
189
+ )
190
+ self.framework.observe(
191
+ self.database.on.cluster2_resource_created, self._on_cluster2_resource_created
192
+ )
193
+
194
+ def _on_cluster1_resource_created(self, event: ResourceCreatedEvent) -> None:
195
+ # Handle the created database on the cluster named cluster1
196
+
197
+ # Create configuration file for app
198
+ config_file = self._render_app_config_file(
199
+ event.response.username,
200
+ event.response.password,
201
+ event.response.endpoints,
202
+ )
203
+ ...
204
+
205
+ def _on_cluster2_resource_created(self, event: ResourceCreatedEvent) -> None:
206
+ # Handle the created database on the cluster named cluster2
207
+
208
+ # Create configuration file for app
209
+ config_file = self._render_app_config_file(
210
+ event.response.username,
211
+ event.response.password,
212
+ event.response.endpoints,
213
+ )
214
+ ...
215
+ ```
216
+
217
+ ### Provider Charm
218
+
219
+ Following an example of using the ResourceRequestedEvent, in the context of the
220
+ database charm code:
221
+
222
+ ```python
223
+ from dpcharmlibs.interfaces import (
224
+ ResourceProviderEventHandler,
225
+ ResourceProviderModel,
226
+ ResourceRequestedEvent,
227
+ RequirerCommonModel,
228
+ )
229
+
230
+ class SampleCharm(CharmBase):
231
+
232
+ def __init__(self, *args):
233
+ super().__init__(*args)
234
+ # Charm events defined in the database provides charm library.
235
+ self.provided_database = ResourceProviderEventHandler(
236
+ self, "database", RequirerCommonModel,
237
+ )
238
+ self.framework.observe(self.provided_database.on.resource_requested,
239
+ self._on_resource_requested)
240
+ # Database generic helper
241
+ self.database = DatabaseHelper()
242
+
243
+ def _on_resource_requested(self, event: ResourceRequestedEvent) -> None:
244
+ # Handle the event triggered by a new database requested in the relation
245
+ # Retrieve the database name using the charm library.
246
+ db_name = event.request.resource
247
+ # generate a new user credential
248
+ username = self.database.generate_user(event.request.request_id)
249
+ password = self.database.generate_password(event.request.request_id)
250
+ # set the credentials for the relation
251
+ response = ResourceProviderModel(
252
+ salt=event.request.salt,
253
+ request_id=event.request.request_id,
254
+ resource=db_name,
255
+ username=username,
256
+ password=password,
257
+ ...
258
+ )
259
+ self.provided_database.set_response(event.relation.id, response)
260
+ ```
261
+
262
+ As shown above, the library provides a custom event (resource_requested) to handle
263
+ the situation when an application charm requests a new database to be created.
264
+ It's preferred to subscribe to this event instead of relation changed event to avoid
265
+ creating a new database when other information other than a database name is
266
+ exchanged in the relation databag.
@@ -0,0 +1,249 @@
1
+ # dpcharmlibs.interfaces: Library to manage the relation for the data-platform products
2
+
3
+ The `interfaces` library.
4
+
5
+ To install, add `dpcharmlibs-interfaces` to your Python dependencies. Then in your Python code, import as:
6
+
7
+ ```py
8
+ from dpcharmlibs import interfaces
9
+ ```
10
+
11
+ ## Abstract
12
+
13
+ This V1 has been specified in
14
+ <https://docs.google.com/document/d/1lnuonWnoQb36RWYwfHOBwU0VClLbawpTISXIC_yNKYo>,
15
+ and should be backward compatible with v0 clients.
16
+
17
+ This library contains the Requires and Provides classes for handling the relation
18
+ between an application and multiple managed application supported by the data-team:
19
+ MySQL, Postgresql, MongoDB, Redis, Kafka, and Karapace.
20
+
21
+ ## Components
22
+
23
+ ### Models
24
+
25
+ This library exposes basic default models that can be used in most cases.
26
+ If you need more complex models, you can subclass them.
27
+
28
+ ```python
29
+ from dpcharmlibs.interfaces import RequirerCommonModel, ExtraSecretStr
30
+
31
+ class ExtendedCommonModel(RequirerCommonModel):
32
+ operator_password: ExtraSecretStr
33
+ ```
34
+
35
+ Secret groups are handled using annotated types.
36
+ If you wish to add extra secret groups, please follow the following model.
37
+ The string metadata represents the secret group name, and `OptionalSecretStr` is a TypeAlias for
38
+ `SecretStr | None`. Finally, `SecretStr` represents a field validating the URI pattern `secret:.*`
39
+
40
+ ```python
41
+ MyGroupSecretStr = Annotated[OptionalSecretStr, Field(exclude=True, default=None), "mygroup"]
42
+ ```
43
+
44
+ Fields not specified as OptionalSecretStr and extended with a group name in the metadata will NOT
45
+ get serialised.
46
+
47
+ ### Requirer Charm
48
+
49
+ This library is a uniform interface to a selection of common database
50
+ metadata, with added custom events that add convenience to database management,
51
+ and methods to consume the application related data.
52
+
53
+ ```python
54
+ from dpcharmlibs.interfaces import (
55
+ RequirerCommonModel,
56
+ RequirerDataContractV1,
57
+ ResourceCreatedEvent,
58
+ ResourceEntityCreatedEvent,
59
+ ResourceProviderModel,
60
+ ResourceRequirerEventHandler,
61
+ )
62
+
63
+ class ClientCharm(CharmBase):
64
+ # Database charm that accepts connections from application charms.
65
+ def __init__(self, *args) -> None:
66
+ super().__init__(*args)
67
+
68
+ requests = [
69
+ RequirerCommonModel(
70
+ resource="clientdb",
71
+ ),
72
+ RequirerCommonModel(
73
+ resource="clientbis",
74
+ ),
75
+ RequirerCommonModel(
76
+ entity_type="USER",
77
+ )
78
+ ]
79
+ self.database = ResourceRequirerEventHandler(
80
+ self,"database", requests, response_model=ResourceProviderModel
81
+ )
82
+ self.framework.observe(self.database.on.resource_created, self._on_resource_created)
83
+ self.framework.observe(self.database.on.resource_entity_created, self._on_entity_created)
84
+
85
+ def _on_resource_created(self, event: ResourceCreatedEvent) -> None:
86
+ # Event triggered when a new database is created.
87
+ relation_id = event.relation.id
88
+ response = event.response # This is the response model
89
+
90
+ username = event.response.username
91
+ password = event.response.password
92
+ ...
93
+
94
+ def _on_entity_created(self, event: ResourceCreatedEvent) -> None:
95
+ # Event triggered when a new entity is created.
96
+ ...
97
+ ```
98
+
99
+ Compared to V0, this library makes heavy use of pydantic models, and allows for
100
+ multiple requests, specified as a list.
101
+ On the Requirer side, each response will trigger one custom event for that response.
102
+ This way, it allows for more strategic events to be emitted according to the request.
103
+
104
+ As show above, the library provides some custom events to handle specific situations,
105
+ which are listed below:
106
+
107
+ - resource_created: event emitted when the requested database is created.
108
+ - resource_entity_created: event emitted when the requested entity is created.
109
+ - endpoints_changed: event emitted when the read/write endpoints of
110
+ the database have changed.
111
+ - read_only_endpoints_changed: event emitted when the read-only endpoints of
112
+ the database have changed. Event is not triggered if read/write endpoints
113
+ changed too.
114
+
115
+ If it is needed to connect multiple database clusters to the same relation endpoint
116
+ the application charm can implement the same code as if it would connect to only
117
+ one database cluster (like the above code example).
118
+
119
+ To differentiate multiple clusters connected to the same relation endpoint
120
+ the application charm can use the name of the remote application:
121
+
122
+ ```python
123
+ def _on_resource_created(self, event: ResourceCreatedEvent) -> None:
124
+ # Get the remote app name of the cluster that triggered this event
125
+ cluster = event.relation.app.name
126
+ ```
127
+
128
+ It is also possible to provide an alias for each different database cluster/relation.
129
+
130
+ So, it is possible to differentiate the clusters in two ways.
131
+
132
+ The first is to use the remote application name, with `event.relation.app.name`.
133
+
134
+ The second way is to use different event handlers to handle each cluster events.
135
+
136
+ The implementation would be something like the following code:
137
+
138
+ ```python
139
+ from dpcharmlibs.interfaces import (
140
+ RequirerCommonModel,
141
+ RequirerDataContractV1,
142
+ ResourceCreatedEvent,
143
+ ResourceEntityCreatedEvent,
144
+ ResourceProviderModel,
145
+ ResourceRequirerEventHandler,
146
+ )
147
+
148
+ class ApplicationCharm(CharmBase):
149
+ # Application charm that connects to database charms.
150
+
151
+ def __init__(self, *args):
152
+ super().__init__(*args)
153
+
154
+ requests = [
155
+ RequirerCommonModel(
156
+ resource="clientdb",
157
+ ),
158
+ RequirerCommonModel(
159
+ resource="clientbis",
160
+ ),
161
+ ]
162
+ # Define the cluster aliases and one handler for each cluster database
163
+ # created event.
164
+ self.database = ResourceRequirerEventHandler(
165
+ self,
166
+ relation_name="database"
167
+ relations_aliases = ["cluster1", "cluster2"],
168
+ requests=
169
+ )
170
+ self.framework.observe(
171
+ self.database.on.cluster1_resource_created, self._on_cluster1_resource_created
172
+ )
173
+ self.framework.observe(
174
+ self.database.on.cluster2_resource_created, self._on_cluster2_resource_created
175
+ )
176
+
177
+ def _on_cluster1_resource_created(self, event: ResourceCreatedEvent) -> None:
178
+ # Handle the created database on the cluster named cluster1
179
+
180
+ # Create configuration file for app
181
+ config_file = self._render_app_config_file(
182
+ event.response.username,
183
+ event.response.password,
184
+ event.response.endpoints,
185
+ )
186
+ ...
187
+
188
+ def _on_cluster2_resource_created(self, event: ResourceCreatedEvent) -> None:
189
+ # Handle the created database on the cluster named cluster2
190
+
191
+ # Create configuration file for app
192
+ config_file = self._render_app_config_file(
193
+ event.response.username,
194
+ event.response.password,
195
+ event.response.endpoints,
196
+ )
197
+ ...
198
+ ```
199
+
200
+ ### Provider Charm
201
+
202
+ Following an example of using the ResourceRequestedEvent, in the context of the
203
+ database charm code:
204
+
205
+ ```python
206
+ from dpcharmlibs.interfaces import (
207
+ ResourceProviderEventHandler,
208
+ ResourceProviderModel,
209
+ ResourceRequestedEvent,
210
+ RequirerCommonModel,
211
+ )
212
+
213
+ class SampleCharm(CharmBase):
214
+
215
+ def __init__(self, *args):
216
+ super().__init__(*args)
217
+ # Charm events defined in the database provides charm library.
218
+ self.provided_database = ResourceProviderEventHandler(
219
+ self, "database", RequirerCommonModel,
220
+ )
221
+ self.framework.observe(self.provided_database.on.resource_requested,
222
+ self._on_resource_requested)
223
+ # Database generic helper
224
+ self.database = DatabaseHelper()
225
+
226
+ def _on_resource_requested(self, event: ResourceRequestedEvent) -> None:
227
+ # Handle the event triggered by a new database requested in the relation
228
+ # Retrieve the database name using the charm library.
229
+ db_name = event.request.resource
230
+ # generate a new user credential
231
+ username = self.database.generate_user(event.request.request_id)
232
+ password = self.database.generate_password(event.request.request_id)
233
+ # set the credentials for the relation
234
+ response = ResourceProviderModel(
235
+ salt=event.request.salt,
236
+ request_id=event.request.request_id,
237
+ resource=db_name,
238
+ username=username,
239
+ password=password,
240
+ ...
241
+ )
242
+ self.provided_database.set_response(event.relation.id, response)
243
+ ```
244
+
245
+ As shown above, the library provides a custom event (resource_requested) to handle
246
+ the situation when an application charm requests a new database to be created.
247
+ It's preferred to subscribe to this event instead of relation changed event to avoid
248
+ creating a new database when other information other than a database name is
249
+ exchanged in the relation databag.