postgresql-charms-single-kernel 16.1.12__tar.gz → 16.2.2__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 (19) hide show
  1. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/PKG-INFO +3 -1
  2. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/pyproject.toml +6 -3
  3. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/config/literals.py +2 -0
  4. postgresql_charms_single_kernel-16.2.2/single_kernel_postgresql/utils/__init__.py +197 -0
  5. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/utils/postgresql.py +2 -2
  6. postgresql_charms_single_kernel-16.1.12/single_kernel_postgresql/utils/__init__.py +0 -3
  7. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/LICENSE +0 -0
  8. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/README.md +0 -0
  9. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/__init__.py +0 -0
  10. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/abstract_charm.py +0 -0
  11. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/charmcraft.yaml +0 -0
  12. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/compat/__init__.py +0 -0
  13. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/compat/postgresql.py +0 -0
  14. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/compat/pyproject.toml +0 -0
  15. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/config/__init__.py +0 -0
  16. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/config/pyproject.toml +0 -0
  17. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/events/__init__.py +0 -0
  18. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/events/tls_transfer.py +0 -0
  19. {postgresql_charms_single_kernel-16.1.12 → postgresql_charms_single_kernel-16.2.2}/single_kernel_postgresql/utils/filesystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: postgresql-charms-single-kernel
3
- Version: 16.1.12
3
+ Version: 16.2.2
4
4
  Summary: Shared and reusable code for PostgreSQL-related charms
5
5
  Author: Canonical Data Platform
6
6
  Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
@@ -212,7 +212,9 @@ Classifier: Operating System :: POSIX :: Linux
212
212
  Requires-Dist: ops>=2.0.0
213
213
  Requires-Dist: psycopg2>=2.9.10
214
214
  Requires-Dist: tenacity>=9.0.0
215
+ Requires-Dist: httpx ; python_full_version >= '3.12' and extra == 'postgresql'
215
216
  Requires-Python: >=3.8, <4.0
217
+ Provides-Extra: postgresql
216
218
  Description-Content-Type: text/markdown
217
219
 
218
220
  # postgresql-single-kernel-library
@@ -4,7 +4,7 @@
4
4
  [project]
5
5
  name = "postgresql-charms-single-kernel"
6
6
  description = "Shared and reusable code for PostgreSQL-related charms"
7
- version = "16.1.12"
7
+ version = "16.2.2"
8
8
  readme = "README.md"
9
9
  license = {file = "LICENSE"}
10
10
  authors = [
@@ -22,6 +22,9 @@ dependencies = [
22
22
  "tenacity>=9.0.0",
23
23
  ]
24
24
 
25
+ [project.optional-dependencies]
26
+ postgresql = ["httpx; python_version >= '3.12'"]
27
+
25
28
  [build-system]
26
29
  requires = ["uv_build>=0.11.0,<0.12.0"]
27
30
  build-backend = "uv_build"
@@ -32,11 +35,11 @@ module-root = ""
32
35
 
33
36
  [dependency-groups]
34
37
  format = [
35
- "ruff==0.15.13"
38
+ "ruff==0.15.14"
36
39
  ]
37
40
  lint = [
38
41
  "codespell==2.4.2",
39
- "ty==0.0.37"
42
+ "ty==0.0.39"
40
43
  ]
41
44
  unit = [
42
45
  "coverage[toml]==7.14.0",
@@ -22,6 +22,8 @@ SNAP_USER = "_daemon_"
22
22
  USER = "operator"
23
23
  SYSTEM_USERS = [MONITORING_USER, REPLICATION_USER, REWIND_USER, USER]
24
24
 
25
+ API_REQUEST_TIMEOUT = 5
26
+
25
27
 
26
28
  class Substrates(str, Enum):
27
29
  """Possible substrates."""
@@ -0,0 +1,197 @@
1
+ # Copyright 2022 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ """A collection of utility functions that are used in the charm."""
5
+
6
+ import os
7
+ import pwd
8
+ import re
9
+ import secrets
10
+ import string
11
+ from asyncio import as_completed, create_task, run, wait
12
+ from contextlib import suppress
13
+ from ssl import CERT_NONE, create_default_context
14
+ from typing import Any
15
+
16
+ from httpx import AsyncClient, BasicAuth, HTTPError
17
+
18
+ from ..config.literals import API_REQUEST_TIMEOUT, Substrates
19
+
20
+
21
+ def new_password() -> str:
22
+ """Generate a random password string.
23
+
24
+ Returns:
25
+ A random password string.
26
+ """
27
+ choices = string.ascii_letters + string.digits
28
+ password = "".join([secrets.choice(choices) for _ in range(16)])
29
+ return password
30
+
31
+
32
+ def split_mem(mem_str) -> tuple:
33
+ """Split a memory string into a number and a unit.
34
+
35
+ Args:
36
+ mem_str: a string representing a memory value, e.g. "1Gi"
37
+ """
38
+ pattern = r"^(\d+)(\w+)$"
39
+ parts = re.match(pattern, mem_str)
40
+ if parts:
41
+ return parts.groups()
42
+ return None, "No unit found"
43
+
44
+
45
+ def any_memory_to_bytes(mem_str) -> int:
46
+ """Convert a memory string to bytes.
47
+
48
+ Args:
49
+ mem_str: a string representing a memory value, e.g. "1Gi"
50
+ """
51
+ units = {
52
+ "KI": 1024,
53
+ "K": 10**3,
54
+ "MI": 1048576,
55
+ "M": 10**6,
56
+ "GI": 1073741824,
57
+ "G": 10**9,
58
+ "TI": 1099511627776,
59
+ "T": 10**12,
60
+ }
61
+ try:
62
+ num = int(mem_str)
63
+ return num
64
+ except ValueError as e:
65
+ memory, unit = split_mem(mem_str)
66
+ unit = unit.upper()
67
+ if unit not in units:
68
+ raise ValueError(f"Invalid memory definition in '{mem_str}'") from e
69
+
70
+ num = int(memory)
71
+ return int(num * units[unit])
72
+
73
+
74
+ def any_cpu_to_cores(cpu_str) -> int:
75
+ """Convert a CPU string to cores.
76
+
77
+ Args:
78
+ cpu_str: a string representing a CPU value, as integer or millis
79
+ """
80
+ if cpu_str.endswith("m"):
81
+ # convert millis to cores, undercommited
82
+ return int(cpu_str[:-1]) // 1000
83
+ return int(cpu_str)
84
+
85
+
86
+ def label2name(label: str) -> str:
87
+ """Convert a unit label (with `-`) to a unit name (with `/`).
88
+
89
+ Args:
90
+ label: The label to convert.
91
+
92
+ Returns:
93
+ The converted name.
94
+ """
95
+ return "/".join(label.rsplit("-", 1))
96
+
97
+
98
+ def render_file(
99
+ substrate: Substrates, path: str, content: str, mode: int, change_owner: bool = True
100
+ ) -> None:
101
+ """Write a content rendered from a template to a file.
102
+
103
+ Args:
104
+ substrate: Charm substrate.
105
+ path: the path to the file.
106
+ content: the data to be written to the file.
107
+ mode: access permission mask applied to the
108
+ file using chmod (e.g. 0o640).
109
+ change_owner: whether to change the file owner
110
+ to the _daemon_ user.
111
+ """
112
+ # TODO: keep this method to use it also for generating replication configuration files and
113
+ # move it to an utils / helpers file.
114
+ # Write the content to the file.
115
+ with open(path, "w+") as file:
116
+ file.write(content)
117
+ # Ensure correct permissions are set on the file.
118
+ os.chmod(path, mode)
119
+ if change_owner:
120
+ _change_owner(substrate, path)
121
+
122
+
123
+ def create_directory(substrate: Substrates, path: str, mode: int) -> None:
124
+ """Creates a directory.
125
+
126
+ Args:
127
+ substrate: Charm substrate.
128
+ path: the path of the directory that should be created.
129
+ mode: access permission mask applied to the
130
+ directory using chmod (e.g. 0o640).
131
+ """
132
+ os.makedirs(path, mode=mode, exist_ok=True)
133
+ # Ensure correct permissions are set on the directory.
134
+ os.chmod(path, mode)
135
+ _change_owner(substrate, path)
136
+
137
+
138
+ def _change_owner(substrate: Substrates, path: str) -> None:
139
+ """Change the ownership of a file or a directory to the postgres user.
140
+
141
+ Args:
142
+ substrate: Charm substrate.
143
+ path: path to a file or directory.
144
+ """
145
+ try:
146
+ # Get the uid/gid for the _daemon_ user.
147
+ user_database = (
148
+ pwd.getpwnam("_daemon_") if substrate == Substrates.VM else pwd.getpwnam("postgres")
149
+ )
150
+ # Set the correct ownership for the file or directory.
151
+ os.chown(path, uid=user_database.pw_uid, gid=user_database.pw_gid)
152
+ except KeyError:
153
+ # Ignore non existing user error when it wasn't created yet.
154
+ pass
155
+
156
+
157
+ async def _httpx_get_request(
158
+ url: str, cafile: str, auth: BasicAuth | None = None, verify: bool = True
159
+ ) -> dict[str, Any] | None:
160
+ ssl_ctx = create_default_context()
161
+ if verify:
162
+ with suppress(FileNotFoundError):
163
+ ssl_ctx.load_verify_locations(cafile=cafile)
164
+ else:
165
+ ssl_ctx.check_hostname = False
166
+ ssl_ctx.verify_mode = CERT_NONE
167
+ async with AsyncClient(auth=auth, timeout=API_REQUEST_TIMEOUT, verify=ssl_ctx) as client:
168
+ try:
169
+ return (await client.get(url)).raise_for_status().json()
170
+ except (HTTPError, ValueError):
171
+ return None
172
+
173
+
174
+ async def _async_get_request(
175
+ uri: str, endpoints: list[str], cafile: str, auth: BasicAuth | None, verify: bool = True
176
+ ) -> dict[str, Any] | None:
177
+ tasks = [
178
+ create_task(_httpx_get_request(f"https://{ip}:8008{uri}", cafile, auth, verify))
179
+ for ip in endpoints
180
+ ]
181
+ for task in as_completed(tasks):
182
+ if result := await task:
183
+ for task in tasks:
184
+ task.cancel()
185
+ await wait(tasks)
186
+ return result
187
+
188
+
189
+ def parallel_patroni_get_request(
190
+ uri: str,
191
+ endpoints: list[str],
192
+ cafile: str,
193
+ auth: BasicAuth | None = None,
194
+ verify: bool = True,
195
+ ) -> dict[str, Any] | None:
196
+ """Call all possible patroni endpoints in parallel."""
197
+ return run(_async_get_request(uri, endpoints, cafile, auth, verify))
@@ -1381,14 +1381,14 @@ $$ LANGUAGE plpgsql security definer;""" # noqa: S608
1381
1381
  parameters[parameter] = value
1382
1382
  shared_buffers_max_value_in_mb = int(available_memory * 0.4 / 10**6)
1383
1383
  shared_buffers_max_value = int(shared_buffers_max_value_in_mb * 10**3 / 8)
1384
- if parameters.get("shared_buffers", 0) > shared_buffers_max_value:
1384
+ if int(parameters.get("shared_buffers", 0)) > shared_buffers_max_value:
1385
1385
  raise Exception(
1386
1386
  f"Shared buffers config option should be at most 40% of the available memory, which is {shared_buffers_max_value_in_mb}MB"
1387
1387
  )
1388
1388
  if profile == "production":
1389
1389
  if "shared_buffers" in parameters:
1390
1390
  # Convert to bytes to use in the calculation.
1391
- shared_buffers = parameters["shared_buffers"] * 8 * 10**3
1391
+ shared_buffers = int(parameters["shared_buffers"]) * 8 * 10**3
1392
1392
  else:
1393
1393
  # Use 25% of the available memory for shared_buffers.
1394
1394
  # and the remaining as cache memory.
@@ -1,3 +0,0 @@
1
- # Copyright 2025 Canonical Ltd.
2
- # See LICENSE file for licensing details.
3
- """Utils and helpers for PostgreSQL charms."""