django-sync-env 0.2.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 (28) hide show
  1. django_sync_env-0.2.0/PKG-INFO +65 -0
  2. django_sync_env-0.2.0/README.md +49 -0
  3. django_sync_env-0.2.0/pyproject.toml +16 -0
  4. django_sync_env-0.2.0/src/django_sync_env/__init__.py +0 -0
  5. django_sync_env-0.2.0/src/django_sync_env/apps.py +20 -0
  6. django_sync_env-0.2.0/src/django_sync_env/checks.py +39 -0
  7. django_sync_env-0.2.0/src/django_sync_env/constants.py +3 -0
  8. django_sync_env-0.2.0/src/django_sync_env/db/__init__.py +0 -0
  9. django_sync_env-0.2.0/src/django_sync_env/db/base.py +186 -0
  10. django_sync_env-0.2.0/src/django_sync_env/db/exceptions.py +17 -0
  11. django_sync_env-0.2.0/src/django_sync_env/db/mongodb.py +54 -0
  12. django_sync_env-0.2.0/src/django_sync_env/db/mysql.py +45 -0
  13. django_sync_env-0.2.0/src/django_sync_env/db/postgresql.py +127 -0
  14. django_sync_env-0.2.0/src/django_sync_env/db/sqlite.py +98 -0
  15. django_sync_env-0.2.0/src/django_sync_env/log.py +11 -0
  16. django_sync_env-0.2.0/src/django_sync_env/management/__init__.py +0 -0
  17. django_sync_env-0.2.0/src/django_sync_env/management/commands/__init__.py +0 -0
  18. django_sync_env-0.2.0/src/django_sync_env/management/commands/_base.py +160 -0
  19. django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_backup_db.py +88 -0
  20. django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_backup_media.py +128 -0
  21. django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_list_db_backups.py +52 -0
  22. django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_list_media_backups.py +51 -0
  23. django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_restore_db.py +166 -0
  24. django_sync_env-0.2.0/src/django_sync_env/management/commands/sync_env_restore_media.py +170 -0
  25. django_sync_env-0.2.0/src/django_sync_env/settings.py +53 -0
  26. django_sync_env-0.2.0/src/django_sync_env/storage.py +290 -0
  27. django_sync_env-0.2.0/src/django_sync_env/tasks.py +27 -0
  28. django_sync_env-0.2.0/src/django_sync_env/utils.py +462 -0
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.1
2
+ Name: django-sync-env
3
+ Version: 0.2.0
4
+ Summary: Backup, Sync and Restore Databases and Media
5
+ Author: Dan Brosnan
6
+ Author-email: dan.brosnan@octave.nz
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: django-storages (>=1.14.2,<2.0.0)
13
+ Requires-Dist: inquirer (>=3.1.3,<4.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ ## Django sync env
17
+
18
+ django-sync-env is a Django app to manage backing up and restoring django databases and media assets easy.
19
+
20
+ Detailed documentation is in the "docs" directory. (needs some work)
21
+
22
+
23
+ ## Requirements:
24
+ - configurable backups and restore which can be ran on demand, celery cron or in dev.
25
+ - support s3 bucket and file storage options first, add azure blob storage later
26
+ - interactive prompts for local dev
27
+ - command line options with --no-input for CICD and automations
28
+ - restore latest option for media and backups for X env
29
+ - backup before restore option
30
+
31
+
32
+ ## TODO:
33
+ - update docs around how to development this package
34
+ - remove command options which we wont use
35
+ - make sure all management commands work will good error logging and handling
36
+
37
+
38
+ Quick start
39
+ -----------
40
+
41
+ 1. Add "django_sync_env" to your INSTALLED_APPS setting like this:
42
+
43
+ ```
44
+ INSTALLED_APPS = [
45
+ ...,
46
+ "django_sync_env",
47
+ ]
48
+ ```
49
+
50
+ 2. Configure the app via a settings file .typically `./settings/partial/sync_env.py` for base configuration,
51
+ don't forget to import this file via your base.py settings file
52
+ And override any required settings per environment via `./settings/partials/[env].py`
53
+ see [example_partials](docs/example_partials.md)
54
+
55
+ There is a [s3 terraform example](docs/example_terraform_aws_s3_bucket.md) for provisioning
56
+ an aws s3 bucket, iam user, roles and policy to allow for remote backup/restore to/from a secure s3 bucket.
57
+
58
+ See [management commands](docs/management_commands.md) for more details for each of the commands available.
59
+
60
+ - `./manage.py sync_env_backup_db`
61
+ - `./manage.py sync_env_backup_media`
62
+ - `./manage.py sync_env_restore_db`
63
+ - `./manage.py sync_env_restore_media`
64
+ - `./manage.py sync_env_list_backups`
65
+
@@ -0,0 +1,49 @@
1
+ ## Django sync env
2
+
3
+ django-sync-env is a Django app to manage backing up and restoring django databases and media assets easy.
4
+
5
+ Detailed documentation is in the "docs" directory. (needs some work)
6
+
7
+
8
+ ## Requirements:
9
+ - configurable backups and restore which can be ran on demand, celery cron or in dev.
10
+ - support s3 bucket and file storage options first, add azure blob storage later
11
+ - interactive prompts for local dev
12
+ - command line options with --no-input for CICD and automations
13
+ - restore latest option for media and backups for X env
14
+ - backup before restore option
15
+
16
+
17
+ ## TODO:
18
+ - update docs around how to development this package
19
+ - remove command options which we wont use
20
+ - make sure all management commands work will good error logging and handling
21
+
22
+
23
+ Quick start
24
+ -----------
25
+
26
+ 1. Add "django_sync_env" to your INSTALLED_APPS setting like this:
27
+
28
+ ```
29
+ INSTALLED_APPS = [
30
+ ...,
31
+ "django_sync_env",
32
+ ]
33
+ ```
34
+
35
+ 2. Configure the app via a settings file .typically `./settings/partial/sync_env.py` for base configuration,
36
+ don't forget to import this file via your base.py settings file
37
+ And override any required settings per environment via `./settings/partials/[env].py`
38
+ see [example_partials](docs/example_partials.md)
39
+
40
+ There is a [s3 terraform example](docs/example_terraform_aws_s3_bucket.md) for provisioning
41
+ an aws s3 bucket, iam user, roles and policy to allow for remote backup/restore to/from a secure s3 bucket.
42
+
43
+ See [management commands](docs/management_commands.md) for more details for each of the commands available.
44
+
45
+ - `./manage.py sync_env_backup_db`
46
+ - `./manage.py sync_env_backup_media`
47
+ - `./manage.py sync_env_restore_db`
48
+ - `./manage.py sync_env_restore_media`
49
+ - `./manage.py sync_env_list_backups`
@@ -0,0 +1,16 @@
1
+ [tool.poetry]
2
+ name = "django-sync-env"
3
+ version = "0.2.0"
4
+ description = "Backup, Sync and Restore Databases and Media"
5
+ authors = ["Dan Brosnan <dan.brosnan@octave.nz>", "Will Cook <will@octave.nz>"]
6
+ readme = "README.md"
7
+ packages = [{ include = "django_sync_env", from = "src" }]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ inquirer = "^3.1.3"
12
+ django-storages = "^1.14.2"
13
+
14
+ [build-system]
15
+ requires = ["poetry-core"]
16
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,20 @@
1
+ """Apps for SyncEnv"""
2
+
3
+ from django.apps import AppConfig
4
+ from django.utils.translation import gettext_lazy
5
+
6
+ from django_sync_env import log
7
+
8
+
9
+ class SyncEnvConfig(AppConfig):
10
+ """
11
+ Config for SyncEnv
12
+ """
13
+
14
+ name = "django_sync_env"
15
+ label = "django_sync_env"
16
+ verbose_name = gettext_lazy("sync env")
17
+ default_auto_field = "django.db.models.AutoField"
18
+
19
+ def ready(self):
20
+ log.load_logger()
@@ -0,0 +1,39 @@
1
+ import re
2
+
3
+ from django.core.checks import Tags, Warning, register
4
+
5
+ from syncenv import settings
6
+
7
+ W001 = Warning(
8
+ "SYNC_ENV_BACKUP_CONFIG setting has not be configured",
9
+ hint="Set up settings.SYNC_ENV_BACKUP_CONFIG see syncenv readme",
10
+ id="syncenv.W001",
11
+ )
12
+
13
+ W002 = Warning(
14
+ "SYNC_ENV_RESTORE_CONFIG setting has not be configured",
15
+ hint="Set up settings.SYNC_ENV_RESTORE_CONFIG see syncenv readme",
16
+ id="syncenv.W002",
17
+ )
18
+
19
+ W005 = Warning(
20
+ "Invalid DATE_FORMAT parameter",
21
+ hint="settings.SYNC_ENV_DATE_FORMAT can contain only [A-Za-z0-9%_-]",
22
+ id="syncenv.W005",
23
+ )
24
+
25
+
26
+ @register(Tags.compatibility)
27
+ def check_settings(app_configs, **kwargs):
28
+ errors = []
29
+
30
+ if not settings.SYNC_ENV_BACKUP_CONFIG:
31
+ errors.append(W001)
32
+
33
+ if not settings.SYNC_ENV_RESTORE_CONFIG:
34
+ errors.append(W001)
35
+
36
+ if re.search(r"[^A-Za-z0-9%_-]", settings.DATE_FORMAT):
37
+ errors.append(W005)
38
+
39
+ return errors
@@ -0,0 +1,3 @@
1
+ BACKUP_DATABASE_MANAGEMENT_COMMAND_NAME = 'sync_env_backup_db'
2
+ BACKUP_LISTING_MANAGEMENT_COMMAND_NAME = 'sync_env_list_db_backups'
3
+ BACKUP_MEDIA_MANAGEMENT_COMMAND_NAME = 'sync_env_backup_media'
@@ -0,0 +1,186 @@
1
+ """
2
+ Base database connectors
3
+ """
4
+ import logging
5
+ import os
6
+ import shlex
7
+ from importlib import import_module
8
+ from subprocess import Popen
9
+ from tempfile import SpooledTemporaryFile
10
+
11
+ from django.core.files.base import File
12
+
13
+ from django_sync_env import settings, utils
14
+
15
+ from django_sync_env.db import exceptions
16
+
17
+ logger = logging.getLogger("sync_env")
18
+ logger.setLevel(logging.DEBUG)
19
+
20
+ CONNECTOR_MAPPING = {
21
+ # "django.db.backends.sqlite3": "syncenv.db.sqlite.SqliteConnector",
22
+ # "django.db.backends.mysql": "syncenv.db.mysql.MysqlDumpConnector",
23
+ "django.db.backends.postgresql": "django_sync_env.db.postgresql.PgDumpBinaryConnector",
24
+ "django.db.backends.postgresql_psycopg2": "django_sync_env.db.postgresql.PgDumpBinaryConnector",
25
+ # "django.db.backends.oracle": None,
26
+ # "django_mongodb_engine": "syncenv.db.mongodb.MongoDumpConnector",
27
+ # "djongo": "syncenv.db.mongodb.MongoDumpConnector",
28
+ # "django.contrib.gis.db.backends.postgis": "syncenv.db.postgresql.PgDumpGisConnector",
29
+ # "django.contrib.gis.db.backends.mysql": "syncenv.db.mysql.MysqlDumpConnector",
30
+ # "django.contrib.gis.db.backends.oracle": None,
31
+ # "django.contrib.gis.db.backends.spatialite": "syncenv.db.sqlite.SqliteConnector",
32
+ # "django_prometheus.db.backends.postgresql": "syncenv.db.postgresql.PgDumpBinaryConnector",
33
+ # "django_prometheus.db.backends.sqlite3": "syncenv.db.sqlite.SqliteConnector",
34
+ # "django_prometheus.db.backends.mysql": "syncenv.db.mysql.MysqlDumpConnector",
35
+ # "django_prometheus.db.backends.postgis": "syncenv.db.postgresql.PgDumpGisConnector",
36
+ }
37
+
38
+ if settings.CUSTOM_CONNECTOR_MAPPING:
39
+ CONNECTOR_MAPPING.update(settings.CUSTOM_CONNECTOR_MAPPING)
40
+
41
+
42
+ def get_connector(database_name=None):
43
+ """
44
+ Get a connector from its database key in settings.
45
+ """
46
+ from django.db import DEFAULT_DB_ALIAS, connections
47
+
48
+ # Get DB
49
+ database_name = database_name or DEFAULT_DB_ALIAS
50
+ connection = connections[database_name]
51
+ engine = connection.settings_dict["ENGINE"]
52
+ connector_settings = settings.CONNECTORS.get(database_name, {})
53
+ connector_path = connector_settings.get("CONNECTOR", CONNECTOR_MAPPING[engine])
54
+ connector_module_path = ".".join(connector_path.split(".")[:-1])
55
+ print("DEBUG connector_module_path: ", connector_module_path)
56
+ module = import_module(connector_module_path)
57
+ connector_name = connector_path.split(".")[-1]
58
+ connector = getattr(module, connector_name)
59
+ return connector(database_name, **connector_settings)
60
+
61
+
62
+ class BaseDBConnector:
63
+ """
64
+ Base class for create database connector. This kind of object creates
65
+ interaction with database and allow backup and restore operations.
66
+ """
67
+
68
+ extension = "dump"
69
+ exclude = []
70
+
71
+ def __init__(self, database_name=None, **kwargs):
72
+ from django.db import DEFAULT_DB_ALIAS, connections
73
+
74
+ self.database_name = database_name or DEFAULT_DB_ALIAS
75
+ self.connection = connections[self.database_name]
76
+ for attr, value in kwargs.items():
77
+ setattr(self, attr.lower(), value)
78
+
79
+ @property
80
+ def settings(self):
81
+ """Mix of database and connector settings."""
82
+ if not hasattr(self, "_settings"):
83
+ sett = self.connection.settings_dict.copy()
84
+ sett.update(settings.CONNECTORS.get(self.database_name, {}))
85
+ self._settings = sett
86
+ return self._settings
87
+
88
+ def generate_filename(self, environment, server_name=None, database_name=None):
89
+ return utils.filename_generate(
90
+ self.extension,
91
+ database_name=database_name,
92
+ environment=environment
93
+ )
94
+
95
+ def create_dump(self):
96
+ return self._create_dump()
97
+
98
+ def _create_dump(self):
99
+ """
100
+ Override this method to define dump creation.
101
+ """
102
+ raise NotImplementedError("_create_dump not implemented")
103
+
104
+ def restore_dump(self, dump):
105
+ """
106
+ :param dump: Dump file
107
+ :type dump: file
108
+ """
109
+ return self._restore_dump(dump)
110
+
111
+ def _restore_dump(self, dump):
112
+ """
113
+ Override this method to define dump creation.
114
+ :param dump: Dump file
115
+ :type dump: file
116
+ """
117
+ raise NotImplementedError("_restore_dump not implemented")
118
+
119
+
120
+ class BaseCommandDBConnector(BaseDBConnector):
121
+ """
122
+ Base class for create database connector based on command line tools.
123
+ """
124
+
125
+ dump_prefix = ""
126
+ dump_suffix = ""
127
+ restore_prefix = ""
128
+ restore_suffix = ""
129
+
130
+ use_parent_env = True
131
+ env = {}
132
+ dump_env = {}
133
+ restore_env = {}
134
+
135
+ def run_command(self, command, stdin=None, env=None):
136
+ """
137
+ Launch a shell command line.
138
+
139
+ :param command: Command line to launch
140
+ :type command: str
141
+ :param stdin: Standard input of command
142
+ :type stdin: file
143
+ :param env: Environment variable used in command
144
+ :type env: dict
145
+ :return: Standard output of command
146
+ :rtype: file
147
+ """
148
+ logger.debug(command)
149
+ cmd = shlex.split(command)
150
+ stdout = SpooledTemporaryFile(
151
+ max_size=settings.TMP_FILE_MAX_SIZE, dir=settings.TMP_DIR
152
+ )
153
+ stderr = SpooledTemporaryFile(
154
+ max_size=settings.TMP_FILE_MAX_SIZE, dir=settings.TMP_DIR
155
+ )
156
+ full_env = os.environ.copy() if self.use_parent_env else {}
157
+ full_env.update(self.env)
158
+ full_env.update(env or {})
159
+ try:
160
+ if isinstance(stdin, File):
161
+ process = Popen(
162
+ cmd,
163
+ stdin=stdin.open("rb"),
164
+ stdout=stdout,
165
+ stderr=stderr,
166
+ env=full_env,
167
+ )
168
+ else:
169
+ process = Popen(
170
+ cmd, stdin=stdin, stdout=stdout, stderr=stderr, env=full_env
171
+ )
172
+ process.wait()
173
+ if process.poll():
174
+ stderr.seek(0)
175
+ raise exceptions.CommandConnectorError(
176
+ "Error running: {}\n{}".format(
177
+ command, stderr.read().decode("utf-8")
178
+ )
179
+ )
180
+ stdout.seek(0)
181
+ stderr.seek(0)
182
+ return stdout, stderr
183
+ except OSError as err:
184
+ raise exceptions.CommandConnectorError(
185
+ f"Error running: {command}\n{str(err)}"
186
+ )
@@ -0,0 +1,17 @@
1
+ """Exceptions for database connectors."""
2
+
3
+
4
+ class ConnectorError(Exception):
5
+ """Base connector error"""
6
+
7
+
8
+ class DumpError(ConnectorError):
9
+ """Error on dump"""
10
+
11
+
12
+ class RestoreError(ConnectorError):
13
+ """Error on restore"""
14
+
15
+
16
+ class CommandConnectorError(ConnectorError):
17
+ """Failing command"""
@@ -0,0 +1,54 @@
1
+ from syncenv import utils
2
+
3
+ from .base import BaseCommandDBConnector
4
+
5
+
6
+ class MongoDumpConnector(BaseCommandDBConnector):
7
+ """
8
+ MongoDB connector, creates dump with ``mongodump`` and restore with
9
+ ``mongorestore``.
10
+ """
11
+
12
+ dump_cmd = "mongodump"
13
+ restore_cmd = "mongorestore"
14
+ object_check = True
15
+ drop = True
16
+
17
+ def _create_dump(self):
18
+ cmd = f"{self.dump_cmd} --db {self.settings['NAME']}"
19
+ host = self.settings.get("HOST") or "localhost"
20
+ port = self.settings.get("PORT") or 27017
21
+ cmd += f" --host {host}:{port}"
22
+ if self.settings.get("USER"):
23
+ cmd += f" --username {self.settings['USER']}"
24
+ if self.settings.get("PASSWORD"):
25
+ cmd += f" --password {utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
26
+
27
+ if self.settings.get("AUTH_SOURCE"):
28
+ cmd += f" --authenticationDatabase {self.settings['AUTH_SOURCE']}"
29
+ for collection in self.exclude:
30
+ cmd += f" --excludeCollection {collection}"
31
+ cmd += " --archive"
32
+ cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
33
+ stdout, stderr = self.run_command(cmd, env=self.dump_env)
34
+ return stdout
35
+
36
+ def _restore_dump(self, dump):
37
+ cmd = self.restore_cmd
38
+ host = self.settings.get("HOST") or "localhost"
39
+ port = self.settings.get("PORT") or 27017
40
+ cmd += f" --host {host}:{port}"
41
+ if self.settings.get("USER"):
42
+ cmd += f" --username {self.settings['USER']}"
43
+ if self.settings.get("PASSWORD"):
44
+ cmd += f" --password {utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
45
+
46
+ if self.settings.get("AUTH_SOURCE"):
47
+ cmd += f" --authenticationDatabase {self.settings['AUTH_SOURCE']}"
48
+ if self.object_check:
49
+ cmd += " --objcheck"
50
+ if self.drop:
51
+ cmd += " --drop"
52
+ cmd += " --archive"
53
+ cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
54
+ return self.run_command(cmd, stdin=dump, env=self.restore_env)
@@ -0,0 +1,45 @@
1
+ from syncenv import utils
2
+
3
+ from .base import BaseCommandDBConnector
4
+
5
+
6
+ class MysqlDumpConnector(BaseCommandDBConnector):
7
+ """
8
+ MySQL connector, creates dump with ``mysqldump`` and restore with
9
+ ``mysql``.
10
+ """
11
+
12
+ dump_cmd = "mysqldump"
13
+ restore_cmd = "mysql"
14
+
15
+ def _create_dump(self):
16
+ cmd = f"{self.dump_cmd} {self.settings['NAME']} --quick"
17
+ if self.settings.get("HOST"):
18
+ cmd += f" --host={self.settings['HOST']}"
19
+ if self.settings.get("PORT"):
20
+ cmd += f" --port={self.settings['PORT']}"
21
+ if self.settings.get("USER"):
22
+ cmd += f" --user={self.settings['USER']}"
23
+ if self.settings.get("PASSWORD"):
24
+ cmd += f" --password={utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
25
+
26
+ for table in self.exclude:
27
+ cmd += f" --ignore-table={self.settings['NAME']}.{table}"
28
+ cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
29
+ stdout, stderr = self.run_command(cmd, env=self.dump_env)
30
+ return stdout
31
+
32
+ def _restore_dump(self, dump):
33
+ cmd = f"{self.restore_cmd} {self.settings['NAME']}"
34
+ if self.settings.get("HOST"):
35
+ cmd += f" --host={self.settings['HOST']}"
36
+ if self.settings.get("PORT"):
37
+ cmd += f" --port={self.settings['PORT']}"
38
+ if self.settings.get("USER"):
39
+ cmd += f" --user={self.settings['USER']}"
40
+ if self.settings.get("PASSWORD"):
41
+ cmd += f" --password={utils.get_escaped_command_arg(self.settings['PASSWORD'])}"
42
+
43
+ cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
44
+ stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
45
+ return stdout, stderr
@@ -0,0 +1,127 @@
1
+ import logging
2
+ from urllib.parse import quote
3
+
4
+ from .base import BaseCommandDBConnector
5
+ from .exceptions import DumpError
6
+
7
+ logger = logging.getLogger("sync_env")
8
+
9
+
10
+ def create_postgres_uri(self):
11
+ host = self.settings.get("HOST")
12
+ if not host:
13
+ raise DumpError("A host name is required")
14
+
15
+ dbname = self.settings.get("NAME") or ""
16
+ user = quote(self.settings.get("USER") or "")
17
+ password = self.settings.get("PASSWORD") or ""
18
+ password = f":{quote(password)}" if password else ""
19
+ if not user:
20
+ password = ""
21
+ else:
22
+ host = "@" + host
23
+
24
+ port = ":{}".format(self.settings.get("PORT")) if self.settings.get("PORT") else ""
25
+ dbname = f"--dbname=postgresql://{user}{password}{host}{port}/{dbname}"
26
+ return dbname
27
+
28
+
29
+ class PgDumpConnector(BaseCommandDBConnector):
30
+ """
31
+ PostgreSQL connector, it uses pg_dump`` to create an SQL text file
32
+ and ``psql`` for restore it.
33
+ """
34
+
35
+ extension = "psql"
36
+ dump_cmd = "pg_dump"
37
+ restore_cmd = "psql"
38
+ single_transaction = True
39
+ drop = True
40
+
41
+ def _create_dump(self):
42
+ cmd = f"{self.dump_cmd} "
43
+ cmd = cmd + create_postgres_uri(self)
44
+
45
+ for table in self.exclude:
46
+ cmd += f" --exclude-table-data={table}"
47
+ if self.drop:
48
+ cmd += " --clean"
49
+
50
+ cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
51
+ stdout, stderr = self.run_command(cmd, env=self.dump_env)
52
+ return stdout
53
+
54
+ def _restore_dump(self, dump):
55
+ cmd = f"{self.restore_cmd} "
56
+ cmd = cmd + create_postgres_uri(self)
57
+
58
+ # without this, psql terminates with an exit value of 0 regardless of errors
59
+ cmd += " --set ON_ERROR_STOP=on"
60
+ if self.single_transaction:
61
+ cmd += " --single-transaction"
62
+ cmd += " {}".format(self.settings["NAME"])
63
+ cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
64
+ stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
65
+ return stdout, stderr
66
+
67
+
68
+ class PgDumpGisConnector(PgDumpConnector):
69
+ """
70
+ PostgreGIS connector, same than :class:`PgDumpGisConnector` but enable
71
+ postgis if not made.
72
+ """
73
+
74
+ psql_cmd = "psql"
75
+
76
+ def _enable_postgis(self):
77
+ cmd = f'{self.psql_cmd} -c "CREATE EXTENSION IF NOT EXISTS postgis;"'
78
+ cmd += " --username={}".format(self.settings["ADMIN_USER"])
79
+ cmd += " --no-password"
80
+ if self.settings.get("HOST"):
81
+ cmd += " --host={}".format(self.settings["HOST"])
82
+ if self.settings.get("PORT"):
83
+ cmd += " --port={}".format(self.settings["PORT"])
84
+ return self.run_command(cmd)
85
+
86
+ def _restore_dump(self, dump):
87
+ if self.settings.get("ADMIN_USER"):
88
+ self._enable_postgis()
89
+ return super()._restore_dump(dump)
90
+
91
+
92
+ class PgDumpBinaryConnector(PgDumpConnector):
93
+ """
94
+ PostgreSQL connector, it uses pg_dump`` to create an SQL text file
95
+ and ``pg_restore`` for restore it.
96
+ """
97
+
98
+ extension = "psql.bin"
99
+ dump_cmd = "pg_dump"
100
+ restore_cmd = "pg_restore"
101
+ single_transaction = True
102
+ drop = True
103
+
104
+ def _create_dump(self):
105
+ cmd = f"{self.dump_cmd} "
106
+ cmd = cmd + create_postgres_uri(self)
107
+
108
+ cmd += " --format=custom"
109
+ for table in self.exclude:
110
+ cmd += f" --exclude-table-data={table}"
111
+ cmd = f"{self.dump_prefix} {cmd} {self.dump_suffix}"
112
+ logger.info(cmd)
113
+ stdout, stderr = self.run_command(cmd, env=self.dump_env)
114
+ return stdout
115
+
116
+ def _restore_dump(self, dump):
117
+ dbname = create_postgres_uri(self)
118
+ cmd = f"{self.restore_cmd} {dbname}"
119
+
120
+ if self.single_transaction:
121
+ cmd += " --single-transaction"
122
+ if self.drop:
123
+ cmd += " --clean"
124
+ cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
125
+ logger.info(cmd)
126
+ stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
127
+ return stdout, stderr