postgresql-charms-single-kernel 16.1.0__tar.gz → 16.1.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 (15) hide show
  1. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/PKG-INFO +1 -1
  2. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/postgresql_charms_single_kernel.egg-info/PKG-INFO +1 -1
  3. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/pyproject.toml +5 -5
  4. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/single_kernel_postgresql/utils/postgresql.py +105 -8
  5. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/README.md +0 -0
  6. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/postgresql_charms_single_kernel.egg-info/SOURCES.txt +0 -0
  7. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/postgresql_charms_single_kernel.egg-info/dependency_links.txt +0 -0
  8. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/postgresql_charms_single_kernel.egg-info/top_level.txt +0 -0
  9. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/setup.cfg +0 -0
  10. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/single_kernel_postgresql/__init__.py +0 -0
  11. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/single_kernel_postgresql/abstract_charm.py +0 -0
  12. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/single_kernel_postgresql/config/__init__.py +0 -0
  13. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/single_kernel_postgresql/config/literals.py +0 -0
  14. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/single_kernel_postgresql/utils/__init__.py +0 -0
  15. {postgresql_charms_single_kernel-16.1.0 → postgresql_charms_single_kernel-16.1.2}/single_kernel_postgresql/utils/filesystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: postgresql-charms-single-kernel
3
- Version: 16.1.0
3
+ Version: 16.1.2
4
4
  Summary: Shared and reusable code for PostgreSQL-related charms
5
5
  Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: postgresql-charms-single-kernel
3
- Version: 16.1.0
3
+ Version: 16.1.2
4
4
  Summary: Shared and reusable code for PostgreSQL-related charms
5
5
  Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
6
6
  License-Expression: Apache-2.0
@@ -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.0"
7
+ version = "16.1.2"
8
8
  readme = "README.md"
9
9
  license = "Apache-2.0"
10
10
  authors = [
@@ -23,15 +23,15 @@ lib = [
23
23
  "psycopg2>=2.9.10",
24
24
  ]
25
25
  format = [
26
- "ruff==0.13.2"
26
+ "ruff==0.14.3"
27
27
  ]
28
28
  lint = [
29
29
  "codespell==2.4.1",
30
- "pyright==1.1.405"
30
+ "pyright==1.1.407"
31
31
  ]
32
32
  unit = [
33
- "coverage[toml]==7.10.7; python_version > '3.8'",
34
- "pytest==8.4.2; python_version >= '3.9'"
33
+ "coverage[toml]==7.11.0; python_version >= '3.10'",
34
+ "pytest==8.4.2; python_version >= '3.10'"
35
35
  ]
36
36
 
37
37
  # Testing tools configuration
@@ -114,6 +114,10 @@ class PostgreSQLCreateUserError(PostgreSQLBaseError):
114
114
  self.message = message
115
115
 
116
116
 
117
+ class PostgreSQLUpdateUserError(PostgreSQLBaseError):
118
+ """Exception raised when creating a user fails."""
119
+
120
+
117
121
  class PostgreSQLUndefinedHostError(PostgreSQLBaseError):
118
122
  """Exception when host is not set."""
119
123
 
@@ -146,6 +150,10 @@ class PostgreSQLGetPostgreSQLVersionError(PostgreSQLBaseError):
146
150
  """Exception raised when retrieving PostgreSQL version fails."""
147
151
 
148
152
 
153
+ class PostgreSQLListDatabasesError(PostgreSQLBaseError):
154
+ """Exception raised when retrieving the databases."""
155
+
156
+
149
157
  class PostgreSQLListAccessibleDatabasesForUserError(PostgreSQLBaseError):
150
158
  """Exception raised when retrieving the accessible databases for a user fails."""
151
159
 
@@ -439,14 +447,28 @@ class PostgreSQL:
439
447
  Returns:
440
448
  A tuple containing the adjusted user definition and a list of additional statements.
441
449
  """
450
+ db_roles, connect_statements = self._adjust_user_roles(user, roles, database)
451
+ if db_roles:
452
+ str_roles = [f'"{role}"' for role in db_roles]
453
+ user_definition += f" IN ROLE {', '.join(str_roles)}"
454
+ return user_definition, connect_statements
455
+
456
+ def _adjust_user_roles(
457
+ self, user: str, roles: Optional[List[str]], database: Optional[str]
458
+ ) -> Tuple[List[str], List[str]]:
459
+ """Adjusts the user definition to include additional statements.
460
+
461
+ Returns:
462
+ A tuple containing the adjusted user definition and a list of additional statements.
463
+ """
464
+ db_roles = []
442
465
  connect_statements = []
443
466
  if database:
444
467
  if roles is not None and not any(
445
- True
446
- for role in roles
447
- if role in [ROLE_STATS, ROLE_READ, ROLE_DML, ROLE_BACKUP, ROLE_DBA]
468
+ role in [ROLE_STATS, ROLE_READ, ROLE_DML, ROLE_BACKUP, ROLE_DBA] for role in roles
448
469
  ):
449
- user_definition += f' IN ROLE "charmed_{database}_admin", "charmed_{database}_dml"'
470
+ db_roles.append(f"charmed_{database}_admin")
471
+ db_roles.append(f"charmed_{database}_dml")
450
472
  else:
451
473
  connect_statements.append(
452
474
  SQL("GRANT CONNECT ON DATABASE {} TO {};").format(
@@ -454,9 +476,7 @@ class PostgreSQL:
454
476
  )
455
477
  )
456
478
  if roles is not None and any(
457
- True
458
- for role in roles
459
- if role
479
+ role
460
480
  in [
461
481
  ROLE_STATS,
462
482
  ROLE_READ,
@@ -466,6 +486,7 @@ class PostgreSQL:
466
486
  ROLE_ADMIN,
467
487
  ROLE_DATABASES_OWNER,
468
488
  ]
489
+ for role in roles
469
490
  ):
470
491
  for system_database in ["postgres", "template1"]:
471
492
  connect_statements.append(
@@ -473,7 +494,7 @@ class PostgreSQL:
473
494
  Identifier(system_database), Identifier(user)
474
495
  )
475
496
  )
476
- return user_definition, connect_statements
497
+ return db_roles, connect_statements
477
498
 
478
499
  def _process_extra_user_roles(
479
500
  self, user: str, extra_user_roles: Optional[List[str]] = None
@@ -1841,3 +1862,79 @@ $$ LANGUAGE plpgsql security definer;""" # noqa: S608
1841
1862
  finally:
1842
1863
  if connection:
1843
1864
  connection.close()
1865
+
1866
+ def list_databases(self, prefix: Optional[str] = None) -> List[str]:
1867
+ """List non-system databases starting with prefix."""
1868
+ prefix_stmt = (
1869
+ SQL(" AND datname LIKE {}").format(Literal(prefix + "%")) if prefix else SQL("")
1870
+ )
1871
+ try:
1872
+ with self._connect_to_database() as connection, connection.cursor() as cursor:
1873
+ cursor.execute(
1874
+ SQL(
1875
+ "SELECT datname FROM pg_database WHERE datistemplate = false AND datname <>'postgres'{};"
1876
+ ).format(prefix_stmt)
1877
+ )
1878
+ return [row[0] for row in cursor.fetchall()]
1879
+ except psycopg2.Error as e:
1880
+ raise PostgreSQLListDatabasesError() from e
1881
+ finally:
1882
+ if connection:
1883
+ connection.close()
1884
+
1885
+ def add_user_to_databases(
1886
+ self, user: str, databases: List[str], extra_user_roles: Optional[List[str]] = None
1887
+ ) -> None:
1888
+ """Grant user access to a database."""
1889
+ try:
1890
+ roles, _ = self._process_extra_user_roles(user, extra_user_roles)
1891
+ connect_stmt = []
1892
+ for database in databases:
1893
+ db_roles, db_connect_stmt = self._adjust_user_roles(user, roles, database)
1894
+ roles += db_roles
1895
+ connect_stmt += db_connect_stmt
1896
+ with self._connect_to_database() as connection, connection.cursor() as cursor:
1897
+ cursor.execute(SQL("RESET ROLE;"))
1898
+ cursor.execute(SQL("BEGIN;"))
1899
+ cursor.execute(SQL("SET LOCAL log_statement = 'none';"))
1900
+ cursor.execute(SQL("COMMIT;"))
1901
+
1902
+ # Add extra user roles to the new user.
1903
+ for role in roles:
1904
+ cursor.execute(
1905
+ SQL("GRANT {} TO {};").format(Identifier(role), Identifier(user))
1906
+ )
1907
+ for statement in connect_stmt:
1908
+ cursor.execute(statement)
1909
+ except psycopg2.Error as e:
1910
+ logger.error(f"Failed to add user: {e}")
1911
+ raise PostgreSQLUpdateUserError() from e
1912
+
1913
+ def remove_user_from_databases(self, user: str, databases: List[str]) -> None:
1914
+ """Revoke user access to a database."""
1915
+ try:
1916
+ for database in databases:
1917
+ with self._connect_to_database() as connection, connection.cursor() as cursor:
1918
+ cursor.execute(
1919
+ SQL("REVOKE CONNECT ON DATABASE {} FROM {};").format(
1920
+ Identifier(database), Identifier(user)
1921
+ )
1922
+ )
1923
+ cursor.execute(
1924
+ SQL("REVOKE {} FROM {};").format(
1925
+ Identifier(f"charmed_{database}_owner"), Identifier(user)
1926
+ )
1927
+ )
1928
+ cursor.execute(
1929
+ SQL("REVOKE {} FROM {};").format(
1930
+ Identifier(f"charmed_{database}_admin"), Identifier(user)
1931
+ )
1932
+ )
1933
+ cursor.execute(
1934
+ SQL("REVOKE {} FROM {};").format(
1935
+ Identifier(f"charmed_{database}_dml"), Identifier(user)
1936
+ )
1937
+ )
1938
+ except psycopg2.Error as e:
1939
+ logger.error(f"Failed to remove user: {e}")
1940
+ raise PostgreSQLUpdateUserError() from e