pgpack-dumper 0.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 (33) hide show
  1. pgpack_dumper-0.1.2/CHANGELOG.md +32 -0
  2. pgpack_dumper-0.1.2/MANIFEST.in +1 -0
  3. pgpack_dumper-0.1.2/PKG-INFO +139 -0
  4. pgpack_dumper-0.1.2/README.md +125 -0
  5. pgpack_dumper-0.1.2/pgpack_dumper/__init__.py +35 -0
  6. pgpack_dumper-0.1.2/pgpack_dumper/connector.py +11 -0
  7. pgpack_dumper-0.1.2/pgpack_dumper/copy.py +166 -0
  8. pgpack_dumper-0.1.2/pgpack_dumper/dumper.py +169 -0
  9. pgpack_dumper-0.1.2/pgpack_dumper/errors.py +14 -0
  10. pgpack_dumper-0.1.2/pgpack_dumper/logger.py +67 -0
  11. pgpack_dumper-0.1.2/pgpack_dumper/metadata.py +36 -0
  12. pgpack_dumper-0.1.2/pgpack_dumper/multiquery.py +22 -0
  13. pgpack_dumper-0.1.2/pgpack_dumper/query_path.py +7 -0
  14. pgpack_dumper-0.1.2/pgpack_dumper/query_template.py +10 -0
  15. pgpack_dumper-0.1.2/pgpack_dumper/queryes/attributes.sql +4 -0
  16. pgpack_dumper-0.1.2/pgpack_dumper/queryes/copy_from.sql +1 -0
  17. pgpack_dumper-0.1.2/pgpack_dumper/queryes/copy_to.sql +1 -0
  18. pgpack_dumper-0.1.2/pgpack_dumper/queryes/prepare.sql +4 -0
  19. pgpack_dumper-0.1.2/pgpack_dumper/queryes/relkind.sql +1 -0
  20. pgpack_dumper-0.1.2/pgpack_dumper/random_name.py +7 -0
  21. pgpack_dumper-0.1.2/pgpack_dumper/search_object.py +13 -0
  22. pgpack_dumper-0.1.2/pgpack_dumper/structs.py +34 -0
  23. pgpack_dumper-0.1.2/pgpack_dumper/version.py +1 -0
  24. pgpack_dumper-0.1.2/pgpack_dumper.egg-info/PKG-INFO +139 -0
  25. pgpack_dumper-0.1.2/pgpack_dumper.egg-info/SOURCES.txt +32 -0
  26. pgpack_dumper-0.1.2/pgpack_dumper.egg-info/dependency_links.txt +1 -0
  27. pgpack_dumper-0.1.2/pgpack_dumper.egg-info/not-zip-safe +1 -0
  28. pgpack_dumper-0.1.2/pgpack_dumper.egg-info/requires.txt +3 -0
  29. pgpack_dumper-0.1.2/pgpack_dumper.egg-info/top_level.txt +1 -0
  30. pgpack_dumper-0.1.2/pyproject.toml +3 -0
  31. pgpack_dumper-0.1.2/requirements.txt +3 -0
  32. pgpack_dumper-0.1.2/setup.cfg +14 -0
  33. pgpack_dumper-0.1.2/setup.py +31 -0
@@ -0,0 +1,32 @@
1
+ # Version History
2
+
3
+ ## 0.1.2
4
+
5
+ * Change metadata structure
6
+ * Update requirements.txt
7
+
8
+ ## 0.1.1
9
+
10
+ * Rename project to pgpack_dumper
11
+ * Fix legacy setup.py bdist_wheel mechanism, which will be removed in a future version
12
+ * Fix multiquery
13
+ * Add CHANGELOG.md
14
+
15
+ ## 0.1.0
16
+
17
+ * Add CopyBufferObjectError & CopyBufferTableNotDefined
18
+ * Add PGObject
19
+ * Add logger
20
+ * Add sqlparse for cut comments from query
21
+ * Add multiquery
22
+ * Update requirements.txt
23
+
24
+ ## 0.0.2
25
+
26
+ * Fix include *.sql
27
+ * Fix requirements.txt
28
+ * Docs change README.md
29
+
30
+ ## 0.0.1
31
+
32
+ First version of the library pgcrypt_dumper
@@ -0,0 +1 @@
1
+ recursive-include pgpack_dumper *.sql
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.1
2
+ Name: pgpack_dumper
3
+ Version: 0.1.2
4
+ Summary: Library for read and write PGPack format between PostgreSQL and file.
5
+ Home-page: https://github.com/0xMihalich/pgpack_dumper
6
+ Author: 0xMihalich
7
+ Author-email: bayanmobile87@gmail.com
8
+ Description-Content-Type: text/markdown
9
+ License-File: README.md
10
+ License-File: CHANGELOG.md
11
+ Requires-Dist: pgpack==0.1.3
12
+ Requires-Dist: psycopg>=3.2.9
13
+ Requires-Dist: sqlparse>=0.5.3
14
+
15
+ # PGPackDumper
16
+
17
+ Library for read and write PGPack format between PostgreSQL and file
18
+
19
+ ## Examples
20
+
21
+ ### Initialization
22
+
23
+ ```python
24
+ from pgpack_dumper import (
25
+ CompressionMethod,
26
+ PGConnector,
27
+ PGPackDumper,
28
+ )
29
+
30
+ connector = PGConnector(
31
+ host = <your host>,
32
+ dbname = <your database>,
33
+ user = <your username>,
34
+ password = <your password>,
35
+ port = <your port>,
36
+ )
37
+
38
+ dumper = PGPackDumper(
39
+ connector=connector,
40
+ compression_method=CompressionMethod.LZ4, # or CompressionMethod.ZSTD or CompressionMethod.NONE
41
+ )
42
+ ```
43
+
44
+ ### Read dump from PostgreSQL into file
45
+
46
+ ```python
47
+ file_name = "pgpack.lz4"
48
+ # you need define one of parameter query or table_name
49
+ query = "select ..." # some sql query
50
+ table_name = "public.test_table" # or some table
51
+
52
+ with open(file_name, "wb") as fileobj:
53
+ dumper.read_dump(
54
+ fileobj,
55
+ query,
56
+ table_name,
57
+ )
58
+ ```
59
+
60
+ ### Write dump from file into PostgreSQL
61
+
62
+ ```python
63
+ file_name = "pgpack.lz4"
64
+ # you need define one of parameter table_name
65
+ table_name = "public.test_table" # some table
66
+
67
+ with open(file_name, "rb") as fileobj:
68
+ dumper.write_dump(
69
+ fileobj,
70
+ table_name,
71
+ )
72
+ ```
73
+
74
+ ### Write from PostgreSQL into PostgreSQL
75
+
76
+ Same server
77
+
78
+ ```python
79
+
80
+ table_dest = "public.test_table_write" # some table for write
81
+ table_src = "public.test_table_read" # some table for read
82
+ query_src = "select ..." # or some sql query for read
83
+
84
+ dumper.write_between(
85
+ table_dest,
86
+ table_src,
87
+ query_src,
88
+ )
89
+ ```
90
+
91
+ Different servers
92
+
93
+ ```python
94
+
95
+ connector_src = PGConnector(
96
+ host = <host src>,
97
+ dbname = <database src>,
98
+ user = <username src>,
99
+ password = <password src>,
100
+ port = <port src>,
101
+ )
102
+
103
+ dumper_src = PGPackDumper(connector=connector_src)
104
+
105
+ table_dest = "public.test_table_write" # some table for write
106
+ table_src = "public.test_table_read" # some table for read
107
+ query_src = "select ..." # or some sql query for read
108
+
109
+ dumper.write_between(
110
+ table_dest,
111
+ table_src,
112
+ query_src,
113
+ dumper_src.cursor,
114
+ )
115
+ ```
116
+
117
+ ### Open PGPack file format
118
+
119
+ Get info from my another repository https://github.com/0xMihalich/pgpack
120
+
121
+ ## Installation
122
+
123
+ ### From pip
124
+
125
+ ```bash
126
+ pip install pgpack_dumper
127
+ ```
128
+
129
+ ### From local directory
130
+
131
+ ```bash
132
+ pip install .
133
+ ```
134
+
135
+ ### From git
136
+
137
+ ```bash
138
+ pip install git+https://github.com/0xMihalich/pgpack_dumper
139
+ ```
@@ -0,0 +1,125 @@
1
+ # PGPackDumper
2
+
3
+ Library for read and write PGPack format between PostgreSQL and file
4
+
5
+ ## Examples
6
+
7
+ ### Initialization
8
+
9
+ ```python
10
+ from pgpack_dumper import (
11
+ CompressionMethod,
12
+ PGConnector,
13
+ PGPackDumper,
14
+ )
15
+
16
+ connector = PGConnector(
17
+ host = <your host>,
18
+ dbname = <your database>,
19
+ user = <your username>,
20
+ password = <your password>,
21
+ port = <your port>,
22
+ )
23
+
24
+ dumper = PGPackDumper(
25
+ connector=connector,
26
+ compression_method=CompressionMethod.LZ4, # or CompressionMethod.ZSTD or CompressionMethod.NONE
27
+ )
28
+ ```
29
+
30
+ ### Read dump from PostgreSQL into file
31
+
32
+ ```python
33
+ file_name = "pgpack.lz4"
34
+ # you need define one of parameter query or table_name
35
+ query = "select ..." # some sql query
36
+ table_name = "public.test_table" # or some table
37
+
38
+ with open(file_name, "wb") as fileobj:
39
+ dumper.read_dump(
40
+ fileobj,
41
+ query,
42
+ table_name,
43
+ )
44
+ ```
45
+
46
+ ### Write dump from file into PostgreSQL
47
+
48
+ ```python
49
+ file_name = "pgpack.lz4"
50
+ # you need define one of parameter table_name
51
+ table_name = "public.test_table" # some table
52
+
53
+ with open(file_name, "rb") as fileobj:
54
+ dumper.write_dump(
55
+ fileobj,
56
+ table_name,
57
+ )
58
+ ```
59
+
60
+ ### Write from PostgreSQL into PostgreSQL
61
+
62
+ Same server
63
+
64
+ ```python
65
+
66
+ table_dest = "public.test_table_write" # some table for write
67
+ table_src = "public.test_table_read" # some table for read
68
+ query_src = "select ..." # or some sql query for read
69
+
70
+ dumper.write_between(
71
+ table_dest,
72
+ table_src,
73
+ query_src,
74
+ )
75
+ ```
76
+
77
+ Different servers
78
+
79
+ ```python
80
+
81
+ connector_src = PGConnector(
82
+ host = <host src>,
83
+ dbname = <database src>,
84
+ user = <username src>,
85
+ password = <password src>,
86
+ port = <port src>,
87
+ )
88
+
89
+ dumper_src = PGPackDumper(connector=connector_src)
90
+
91
+ table_dest = "public.test_table_write" # some table for write
92
+ table_src = "public.test_table_read" # some table for read
93
+ query_src = "select ..." # or some sql query for read
94
+
95
+ dumper.write_between(
96
+ table_dest,
97
+ table_src,
98
+ query_src,
99
+ dumper_src.cursor,
100
+ )
101
+ ```
102
+
103
+ ### Open PGPack file format
104
+
105
+ Get info from my another repository https://github.com/0xMihalich/pgpack
106
+
107
+ ## Installation
108
+
109
+ ### From pip
110
+
111
+ ```bash
112
+ pip install pgpack_dumper
113
+ ```
114
+
115
+ ### From local directory
116
+
117
+ ```bash
118
+ pip install .
119
+ ```
120
+
121
+ ### From git
122
+
123
+ ```bash
124
+ pip install git+https://github.com/0xMihalich/pgpack_dumper
125
+ ```
@@ -0,0 +1,35 @@
1
+ """Library for read and write PGPack format between PostgreSQL and file."""
2
+
3
+ from pgcopylib import PGCopy
4
+ from pgpack import (
5
+ CompressionMethod,
6
+ PGPackReader,
7
+ PGPackWriter,
8
+ )
9
+
10
+ from .connector import PGConnector
11
+ from .copy import CopyBuffer
12
+ from .dumper import PGPackDumper
13
+ from .errors import (
14
+ CopyBufferError,
15
+ CopyBufferObjectError,
16
+ CopyBufferTableNotDefined,
17
+ PGPackDumperError,
18
+ )
19
+ from .version import __version__
20
+
21
+ __all__ = (
22
+ "__version__",
23
+ "CompressionMethod",
24
+ "CopyBuffer",
25
+ "CopyBufferError",
26
+ "CopyBufferObjectError",
27
+ "CopyBufferTableNotDefined",
28
+ "PGConnector",
29
+ "PGCopy",
30
+ "PGPackDumper",
31
+ "PGPackDumperError",
32
+ "PGPackReader",
33
+ "PGPackWriter",
34
+ )
35
+ __author__ = "0xMihalich"
@@ -0,0 +1,11 @@
1
+ from typing import NamedTuple
2
+
3
+
4
+ class PGConnector(NamedTuple):
5
+ """Connector for PostgreSQL."""
6
+
7
+ host: str
8
+ dbname: str
9
+ user: str
10
+ password: str
11
+ port: int
@@ -0,0 +1,166 @@
1
+ from io import BufferedReader
2
+ from logging import Logger
3
+ from typing import (
4
+ Generator,
5
+ Iterator,
6
+ )
7
+
8
+ from psycopg import (
9
+ Copy,
10
+ Cursor,
11
+ )
12
+
13
+ from .errors import (
14
+ CopyBufferObjectError,
15
+ CopyBufferTableNotDefined,
16
+ )
17
+ from .query_template import query_template
18
+ from .search_object import search_object
19
+ from .structs import PGObject
20
+ from .metadata import read_metadata
21
+
22
+
23
+ class CopyBuffer:
24
+
25
+ def __init__(
26
+ self,
27
+ cursor: Cursor,
28
+ logger: Logger,
29
+ query: str | None = None,
30
+ table_name: str | None = None,
31
+ ) -> None:
32
+ """Class initialization."""
33
+
34
+ self.cursor = cursor
35
+ self.logger = logger
36
+ self.query = query
37
+ self.table_name = table_name
38
+ self.pos = 0
39
+
40
+ @property
41
+ def metadata(self) -> bytes:
42
+ """Get metadata as bytes."""
43
+
44
+ host = self.cursor.connection.info.host
45
+ self.logger.info(f"Start read metadata from host {host}.")
46
+ metadata = read_metadata(
47
+ self.cursor,
48
+ self.query,
49
+ self.table_name,
50
+ )
51
+ self.logger.info(f"Read metadata from host {host} done.")
52
+ return metadata
53
+
54
+ def copy_to(self) -> Iterator[Copy]:
55
+ """Get copy object from PostgreSQL."""
56
+
57
+ if not self.query and not self.table_name:
58
+ error_msg = "Query or table not defined."
59
+ self.logger.error(error_msg)
60
+ raise CopyBufferTableNotDefined(error_msg)
61
+
62
+ host = self.cursor.connection.info.host
63
+
64
+ if not self.query:
65
+ self.logger.info(f"Start read from {host}.{self.table_name}.")
66
+ self.cursor.execute(query_template("relkind").format(
67
+ table_name=self.table_name,
68
+ ))
69
+ relkind = self.cursor.fetchone()[0]
70
+ pg_object = PGObject[relkind]
71
+ if not pg_object.is_readable:
72
+ error_msg = f"Read from {pg_object} not support."
73
+ self.logger.error(error_msg)
74
+ raise CopyBufferObjectError(error_msg)
75
+ self.logger.info(f"Use method read from {pg_object}.")
76
+ if not pg_object.is_readobject:
77
+ self.table_name = f"(select * from {self.table_name})"
78
+ elif self.query:
79
+ self.logger.info(f"Start read query from {host}.")
80
+ self.logger.info("Use method read from select.")
81
+ self.table_name = f"({self.query})"
82
+
83
+ return self.cursor.copy(
84
+ query_template("copy_to").format(table_name=self.table_name)
85
+ )
86
+
87
+ def copy_from(
88
+ self,
89
+ copyobj: BufferedReader,
90
+ ) -> None:
91
+ """Write PGCopy dump into PostgreSQL."""
92
+
93
+ if not self.table_name:
94
+ error_msg = "Table not defined."
95
+ self.logger.error(error_msg)
96
+ raise CopyBufferTableNotDefined(error_msg)
97
+
98
+ host = self.cursor.connection.info.host
99
+ self.logger.info(f"Start write into {host}.{self.table_name}.")
100
+
101
+ with self.cursor.copy(
102
+ query_template("copy_from").format(table_name=self.table_name)
103
+ ) as cp:
104
+ while chunk := copyobj.read(262_144):
105
+ cp.write(chunk)
106
+
107
+ self.logger.info(f"Write into {host}.{self.table_name} done.")
108
+
109
+ def copy_between(
110
+ self,
111
+ copy_buffer: "CopyBuffer",
112
+ ) -> None:
113
+ """Write from PostgreSQL into PostgreSQL."""
114
+
115
+ with copy_buffer.copy_to() as copy_to:
116
+ destination_host = self.cursor.connection.info.host
117
+ source_host = copy_buffer.cursor.connection.info.host
118
+ source_object = search_object(
119
+ copy_buffer.table_name,
120
+ copy_buffer.query,
121
+ )
122
+ self.logger.info(
123
+ f"Copy {source_object} from {source_host} into "
124
+ f"{destination_host}.{self.table_name} started."
125
+ )
126
+ with self.cursor.copy(
127
+ query_template("copy_from").format(table_name=self.table_name)
128
+ ) as copy_from:
129
+ [copy_from.write(data) for data in copy_to]
130
+ self.logger.info(
131
+ f"Copy {source_object} from {source_host}"
132
+ f"into {destination_host}.{self.table_name} done."
133
+ )
134
+
135
+ def copy_reader(self, size: int = -1) -> Generator[bytes, None, None]:
136
+ """Read bytes from copy object."""
137
+
138
+ host = self.cursor.connection.info.host
139
+ source = search_object(
140
+ self.table_name,
141
+ self.query,
142
+ )
143
+
144
+ with self.copy_to() as copy_object:
145
+ for data in copy_object:
146
+ self.pos += len(data)
147
+ if size != -1 and self.pos >= size:
148
+ try:
149
+ end_pos = size % (self.pos - len(data))
150
+ except ZeroDivisionError:
151
+ end_pos = size
152
+ yield data[:end_pos]
153
+ break
154
+ yield data
155
+
156
+ self.logger.info(f"Read {source} from {host} done.")
157
+
158
+ def read(self, size: int = -1) -> bytes:
159
+ """Read bytes from copy object."""
160
+
161
+ return b"".join(self.copy_reader(size))
162
+
163
+ def tell(self) -> int:
164
+ """Get read size."""
165
+
166
+ return self.pos
@@ -0,0 +1,169 @@
1
+ from io import (
2
+ BufferedReader,
3
+ BufferedWriter,
4
+ )
5
+ from logging import Logger
6
+ from types import MethodType
7
+
8
+ from pgpack import (
9
+ CompressionMethod,
10
+ PGPackReader,
11
+ PGPackWriter,
12
+ )
13
+ from psycopg import (
14
+ Connection,
15
+ Cursor,
16
+ )
17
+ from sqlparse import format as sql_format
18
+
19
+ from .copy import CopyBuffer
20
+ from .connector import PGConnector
21
+ from .errors import PGPackDumperError
22
+ from .logger import DumperLogger
23
+ from .multiquery import chunk_query
24
+
25
+
26
+ class PGPackDumper:
27
+ """Class for read and write PGPack format."""
28
+
29
+ def __init__(
30
+ self,
31
+ connector: PGConnector,
32
+ compression_method: CompressionMethod = CompressionMethod.LZ4,
33
+ logger: Logger = DumperLogger(),
34
+ ) -> None:
35
+ """Class initialization."""
36
+
37
+ try:
38
+ self.connector: PGConnector = connector
39
+ self.connect: Connection = Connection.connect(
40
+ **self.connector._asdict()
41
+ )
42
+ self.cursor: Cursor = self.connect.cursor()
43
+ self.compression_method: CompressionMethod = compression_method
44
+ self.logger = logger
45
+ self.copy_buffer: CopyBuffer = CopyBuffer(self.cursor, self.logger)
46
+ except Exception as error:
47
+ logger.error(error)
48
+ raise PGPackDumperError(error)
49
+
50
+ self.logger.info(
51
+ f"PGPackDumper initialized for host {self.connector.host}."
52
+ )
53
+
54
+ @staticmethod
55
+ def multiquery(dump_method: MethodType):
56
+ """Multiquery decorator."""
57
+
58
+ def wrapper(*args, **kwargs):
59
+
60
+ first_part: list[str]
61
+ second_part: list[str]
62
+
63
+ self: PGPackDumper = args[0]
64
+ cursor: Cursor = kwargs.get("cursor_src") or self.cursor
65
+ query: str = kwargs.get("query_src") or kwargs.get("query")
66
+ part: int = 0
67
+ first_part, second_part = chunk_query(self.query_formatter(query))
68
+
69
+ if first_part:
70
+ self.logger.info("Multiquery detected.")
71
+
72
+ for query in first_part:
73
+ self.logger.info(f"Execute query {part}.")
74
+ cursor.execute(query)
75
+ part += 1
76
+
77
+ if second_part:
78
+ for key in ("query", "query_src"):
79
+ if key in kwargs:
80
+ kwargs[key] = second_part.pop(0)
81
+ break
82
+
83
+ self.logger.info(f"Execute query {part or ''}(copy method).")
84
+ dump_method(*args, **kwargs)
85
+
86
+ if second_part:
87
+ for query in second_part:
88
+ part += 1
89
+ self.logger.info(f"Execute query {part}.")
90
+ cursor.execute(query)
91
+
92
+ return wrapper
93
+
94
+ def query_formatter(self, query: str) -> str | None:
95
+ """Reformat query."""
96
+
97
+ if not query:
98
+ return
99
+ return sql_format(sql=query, strip_comments=True).strip().strip(";")
100
+
101
+ def make_buffer_obj(
102
+ self,
103
+ cursor: Cursor | None = None,
104
+ query: str | None = None,
105
+ table_name: str | None = None,
106
+ ) -> CopyBuffer:
107
+ """Make new buffer object for read."""
108
+
109
+ cursor = cursor or Connection.connect(
110
+ **self.connector._asdict()
111
+ ).cursor()
112
+ host = cursor.connection.info.host
113
+ self.logger.info(f"Make new cursor object for host {host}.")
114
+
115
+ return CopyBuffer(
116
+ cursor,
117
+ query,
118
+ table_name,
119
+ )
120
+
121
+ @multiquery
122
+ def read_dump(
123
+ self,
124
+ fileobj: BufferedWriter,
125
+ query: str | None = None,
126
+ table_name: str | None = None,
127
+ ) -> None:
128
+ """Read PGPack dump from PostgreSQL/GreenPlum."""
129
+
130
+ pgpack = PGPackWriter(fileobj, self.compression_method)
131
+ self.copy_buffer.query = query
132
+ self.copy_buffer.table_name = table_name
133
+ pgpack.write(
134
+ self.copy_buffer.metadata,
135
+ self.copy_buffer,
136
+ )
137
+
138
+ def write_dump(
139
+ self,
140
+ fileobj: BufferedReader,
141
+ table_name: str,
142
+ ) -> None:
143
+ """Write PGPack dump into PostgreSQL/GreenPlum."""
144
+
145
+ fileobj.seek(0)
146
+ pgpack = PGPackReader(fileobj)
147
+ pgpack.pgcopy_compressor.seek(0)
148
+ self.copy_buffer.table_name = table_name
149
+ self.copy_buffer.copy_from(pgpack.pgcopy_compressor)
150
+ self.connect.commit()
151
+
152
+ @multiquery
153
+ def write_between(
154
+ self,
155
+ table_dest: str,
156
+ table_src: str | None = None,
157
+ query_src: str | None = None,
158
+ cursor_src: Cursor | None = None,
159
+ ) -> None:
160
+ """Write from PostgreSQL/GreenPlum into PostgreSQL/GreenPlum."""
161
+
162
+ source_copy_buffer = self.make_buffer_obj(
163
+ cursor=cursor_src,
164
+ query=query_src,
165
+ table_name=table_src,
166
+ )
167
+ self.copy_buffer.table_name = table_dest
168
+ self.copy_buffer.copy_between(source_copy_buffer)
169
+ self.connect.commit()
@@ -0,0 +1,14 @@
1
+ class CopyBufferError(Exception):
2
+ """CopyBuffer base error."""
3
+
4
+
5
+ class CopyBufferObjectError(TypeError):
6
+ """Destination object not support."""
7
+
8
+
9
+ class CopyBufferTableNotDefined(ValueError):
10
+ """Destination table not defined."""
11
+
12
+
13
+ class PGPackDumperError(Exception):
14
+ """PGPackDumper base error."""
@@ -0,0 +1,67 @@
1
+ from datetime import datetime
2
+ from logging import (
3
+ DEBUG,
4
+ FileHandler,
5
+ Formatter,
6
+ Logger,
7
+ StreamHandler,
8
+ )
9
+ from os import makedirs
10
+ from os.path import dirname
11
+ from sys import stdout
12
+
13
+ from .version import __version__
14
+
15
+
16
+ def root_dir() -> str:
17
+ """Get project directory."""
18
+
19
+ import __main__
20
+
21
+ return dirname(__main__.__file__)
22
+
23
+
24
+ class DumperLogger(Logger):
25
+ """PGPackDumper logger."""
26
+
27
+ def __init__(
28
+ self,
29
+ level: int = DEBUG,
30
+ use_console: bool = True,
31
+ ) -> None:
32
+ """Class initialize."""
33
+
34
+ super().__init__("PGPackDumper")
35
+
36
+ self.fmt = (
37
+ f"%(asctime)s | %(levelname)-8s | ver {__version__}"
38
+ "| %(funcName)s-%(filename)s-%(lineno)04d <%(message)s>"
39
+ )
40
+ self.setLevel(level)
41
+ self.log_path = f"{root_dir()}/pgpack_logs"
42
+ makedirs(self.log_path, exist_ok=True)
43
+
44
+ formatter = Formatter(
45
+ fmt=self.fmt,
46
+ datefmt="%Y-%m-%d %H:%M:%S",
47
+ )
48
+
49
+ file_handler = FileHandler(
50
+ "{}/{:%Y-%m-%d}_{}.log".format(
51
+ self.log_path,
52
+ datetime.now(),
53
+ self.name,
54
+ ),
55
+ encoding="utf-8",
56
+ )
57
+ file_handler.setLevel(DEBUG)
58
+ file_handler.setFormatter(formatter)
59
+ self.addHandler(file_handler)
60
+
61
+ if use_console:
62
+ console_handler = StreamHandler(stdout)
63
+ console_handler.setLevel(level)
64
+ console_handler.setFormatter(formatter)
65
+ self.addHandler(console_handler)
66
+
67
+ self.propagate = False
@@ -0,0 +1,36 @@
1
+ from psycopg import Cursor
2
+
3
+ from .query_template import query_template
4
+ from .random_name import random_name
5
+
6
+
7
+ def read_metadata(
8
+ cursor: Cursor,
9
+ query: str | None = None,
10
+ table_name: str | None = None,
11
+ ) -> bytes:
12
+ """Read metadata for query or table."""
13
+
14
+ if not query and not table_name:
15
+ raise ValueError()
16
+
17
+ if query:
18
+ session_name = random_name()
19
+ prepare_name = f"{session_name}_prepare"
20
+ table_name = f"{session_name}_temp"
21
+ cursor.execute(query_template("prepare").format(
22
+ prepare_name=prepare_name,
23
+ query=query,
24
+ table_name=table_name,
25
+ ))
26
+
27
+ cursor.execute(query_template("attributes").format(
28
+ table_name=table_name,
29
+ ))
30
+
31
+ metadata: bytes = cursor.fetchone()[0]
32
+
33
+ if query:
34
+ cursor.execute(f"drop table if exists {table_name};")
35
+
36
+ return metadata
@@ -0,0 +1,22 @@
1
+ def chunk_query(query: str | None) -> tuple[list[str]]:
2
+ """Chunk multiquery to queryes."""
3
+
4
+ if not query:
5
+ return [], []
6
+
7
+ first_part: list[str] = [
8
+ part.strip()
9
+ for part in query.split(";")
10
+ ]
11
+ second_part: list[str] = []
12
+
13
+ for _ in first_part:
14
+ second_part.append(first_part.pop())
15
+ if any(
16
+ word == second_part[-1][:len(word)].lower()
17
+ for word in ("with", "select")
18
+ ):
19
+ second_part = list(reversed(second_part))
20
+ break
21
+
22
+ return first_part, second_part
@@ -0,0 +1,7 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def query_path() -> str:
5
+ """Path for queryes."""
6
+
7
+ return f"{Path(__file__).parent.absolute()}/queryes/{{}}.sql"
@@ -0,0 +1,10 @@
1
+ from .query_path import query_path
2
+
3
+
4
+ def query_template(query_name: str) -> str:
5
+ """Get query template for his name."""
6
+
7
+ path = query_path().format(query_name)
8
+
9
+ with open(path, encoding="utf-8") as query:
10
+ return query.read()
@@ -0,0 +1,4 @@
1
+ select json_agg(json_build_array(attnum, json_build_array(attname, atttypid::int4,
2
+ case when atttypid = 1042 then atttypmod - 4 when atttypid = 1700 then (atttypmod - 4) >> 16 else attlen end,
3
+ case when atttypid = 1700 then (atttypmod - 4) & 65535 else 0 end)))::text::bytea as metadata
4
+ from pg_attribute where attrelid = '{table_name}'::regclass and attnum > 0 and not attisdropped;
@@ -0,0 +1 @@
1
+ copy {table_name} from stdin with (format binary);
@@ -0,0 +1 @@
1
+ copy {table_name} to stdout with (format binary);
@@ -0,0 +1,4 @@
1
+ prepare {prepare_name} as {query} limit 0;
2
+ drop table if exists {table_name};
3
+ create temporary table {table_name} as execute {prepare_name} (null);
4
+ deallocate prepare {prepare_name};
@@ -0,0 +1 @@
1
+ select relkind from pg_class where oid = '{table_name}'::regclass;
@@ -0,0 +1,7 @@
1
+ from random import randbytes
2
+
3
+
4
+ def random_name() -> str:
5
+ """Generate random name for prepare and temp table."""
6
+
7
+ return f"session_{randbytes(8).hex()}" # noqa: S311
@@ -0,0 +1,13 @@
1
+ from re import match
2
+
3
+
4
+ pattern = r"\(select \* from (.*)\)|(.*)"
5
+
6
+
7
+ def search_object(table: str, query: str = "") -> str:
8
+ """Return current string for object."""
9
+
10
+ if query:
11
+ return "query"
12
+
13
+ return match(pattern, table).group(1) or table
@@ -0,0 +1,34 @@
1
+ from enum import Enum
2
+ from typing import NamedTuple
3
+
4
+
5
+ class RelClass(NamedTuple):
6
+ """Postgres objects."""
7
+
8
+ rel_name: str
9
+ is_readobject: bool
10
+ is_readable: bool
11
+
12
+
13
+ class PGObject(RelClass, Enum):
14
+ """RelClass object from relkind value."""
15
+
16
+ r = RelClass("Relation table", True, True)
17
+ i = RelClass("Index", False, False)
18
+ S = RelClass("Sequence", False, False)
19
+ t = RelClass("Toast table", False, False)
20
+ v = RelClass("View", False, True)
21
+ m = RelClass("Materialized view", False, True)
22
+ c = RelClass("Composite type", False, False)
23
+ f = RelClass("Foreign table", False, True)
24
+ p = RelClass("Partitioned table", True, True)
25
+ I = RelClass("Partitioned index", False, True) # noqa: E741
26
+ u = RelClass("Temporary table", True, True)
27
+ o = RelClass("Optimized files", False, False)
28
+ b = RelClass("Block directory", False, False)
29
+ M = RelClass("Visibility map", False, False)
30
+
31
+ def __str__(self) -> str:
32
+ """String representation class."""
33
+
34
+ return self.rel_name
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.1
2
+ Name: pgpack-dumper
3
+ Version: 0.1.2
4
+ Summary: Library for read and write PGPack format between PostgreSQL and file.
5
+ Home-page: https://github.com/0xMihalich/pgpack_dumper
6
+ Author: 0xMihalich
7
+ Author-email: bayanmobile87@gmail.com
8
+ Description-Content-Type: text/markdown
9
+ License-File: README.md
10
+ License-File: CHANGELOG.md
11
+ Requires-Dist: pgpack==0.1.3
12
+ Requires-Dist: psycopg>=3.2.9
13
+ Requires-Dist: sqlparse>=0.5.3
14
+
15
+ # PGPackDumper
16
+
17
+ Library for read and write PGPack format between PostgreSQL and file
18
+
19
+ ## Examples
20
+
21
+ ### Initialization
22
+
23
+ ```python
24
+ from pgpack_dumper import (
25
+ CompressionMethod,
26
+ PGConnector,
27
+ PGPackDumper,
28
+ )
29
+
30
+ connector = PGConnector(
31
+ host = <your host>,
32
+ dbname = <your database>,
33
+ user = <your username>,
34
+ password = <your password>,
35
+ port = <your port>,
36
+ )
37
+
38
+ dumper = PGPackDumper(
39
+ connector=connector,
40
+ compression_method=CompressionMethod.LZ4, # or CompressionMethod.ZSTD or CompressionMethod.NONE
41
+ )
42
+ ```
43
+
44
+ ### Read dump from PostgreSQL into file
45
+
46
+ ```python
47
+ file_name = "pgpack.lz4"
48
+ # you need define one of parameter query or table_name
49
+ query = "select ..." # some sql query
50
+ table_name = "public.test_table" # or some table
51
+
52
+ with open(file_name, "wb") as fileobj:
53
+ dumper.read_dump(
54
+ fileobj,
55
+ query,
56
+ table_name,
57
+ )
58
+ ```
59
+
60
+ ### Write dump from file into PostgreSQL
61
+
62
+ ```python
63
+ file_name = "pgpack.lz4"
64
+ # you need define one of parameter table_name
65
+ table_name = "public.test_table" # some table
66
+
67
+ with open(file_name, "rb") as fileobj:
68
+ dumper.write_dump(
69
+ fileobj,
70
+ table_name,
71
+ )
72
+ ```
73
+
74
+ ### Write from PostgreSQL into PostgreSQL
75
+
76
+ Same server
77
+
78
+ ```python
79
+
80
+ table_dest = "public.test_table_write" # some table for write
81
+ table_src = "public.test_table_read" # some table for read
82
+ query_src = "select ..." # or some sql query for read
83
+
84
+ dumper.write_between(
85
+ table_dest,
86
+ table_src,
87
+ query_src,
88
+ )
89
+ ```
90
+
91
+ Different servers
92
+
93
+ ```python
94
+
95
+ connector_src = PGConnector(
96
+ host = <host src>,
97
+ dbname = <database src>,
98
+ user = <username src>,
99
+ password = <password src>,
100
+ port = <port src>,
101
+ )
102
+
103
+ dumper_src = PGPackDumper(connector=connector_src)
104
+
105
+ table_dest = "public.test_table_write" # some table for write
106
+ table_src = "public.test_table_read" # some table for read
107
+ query_src = "select ..." # or some sql query for read
108
+
109
+ dumper.write_between(
110
+ table_dest,
111
+ table_src,
112
+ query_src,
113
+ dumper_src.cursor,
114
+ )
115
+ ```
116
+
117
+ ### Open PGPack file format
118
+
119
+ Get info from my another repository https://github.com/0xMihalich/pgpack
120
+
121
+ ## Installation
122
+
123
+ ### From pip
124
+
125
+ ```bash
126
+ pip install pgpack_dumper
127
+ ```
128
+
129
+ ### From local directory
130
+
131
+ ```bash
132
+ pip install .
133
+ ```
134
+
135
+ ### From git
136
+
137
+ ```bash
138
+ pip install git+https://github.com/0xMihalich/pgpack_dumper
139
+ ```
@@ -0,0 +1,32 @@
1
+ CHANGELOG.md
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ requirements.txt
6
+ setup.cfg
7
+ setup.py
8
+ pgpack_dumper/__init__.py
9
+ pgpack_dumper/connector.py
10
+ pgpack_dumper/copy.py
11
+ pgpack_dumper/dumper.py
12
+ pgpack_dumper/errors.py
13
+ pgpack_dumper/logger.py
14
+ pgpack_dumper/metadata.py
15
+ pgpack_dumper/multiquery.py
16
+ pgpack_dumper/query_path.py
17
+ pgpack_dumper/query_template.py
18
+ pgpack_dumper/random_name.py
19
+ pgpack_dumper/search_object.py
20
+ pgpack_dumper/structs.py
21
+ pgpack_dumper/version.py
22
+ pgpack_dumper.egg-info/PKG-INFO
23
+ pgpack_dumper.egg-info/SOURCES.txt
24
+ pgpack_dumper.egg-info/dependency_links.txt
25
+ pgpack_dumper.egg-info/not-zip-safe
26
+ pgpack_dumper.egg-info/requires.txt
27
+ pgpack_dumper.egg-info/top_level.txt
28
+ pgpack_dumper/queryes/attributes.sql
29
+ pgpack_dumper/queryes/copy_from.sql
30
+ pgpack_dumper/queryes/copy_to.sql
31
+ pgpack_dumper/queryes/prepare.sql
32
+ pgpack_dumper/queryes/relkind.sql
@@ -0,0 +1,3 @@
1
+ pgpack==0.1.3
2
+ psycopg>=3.2.9
3
+ sqlparse>=0.5.3
@@ -0,0 +1 @@
1
+ pgpack_dumper
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 42.0.0"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,3 @@
1
+ pgpack==0.1.3
2
+ psycopg>=3.2.9
3
+ sqlparse>=0.5.3
@@ -0,0 +1,14 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
5
+ [metadata]
6
+ license_files = README.md, CHANGELOG.md
7
+
8
+ [options]
9
+ install_requires = file: requirements.txt
10
+
11
+ [options.package_data]
12
+ * =
13
+ queryes/*.sql
14
+
@@ -0,0 +1,31 @@
1
+ import shutil
2
+ from setuptools import (
3
+ find_packages,
4
+ setup,
5
+ )
6
+
7
+ shutil.rmtree("build", ignore_errors=True)
8
+ shutil.rmtree("pgpack.egg-info", ignore_errors=True)
9
+
10
+ with open(file="README.md", encoding="utf-8") as f:
11
+ long_description = f.read()
12
+
13
+ setup(
14
+ name="pgpack_dumper",
15
+ version="0.1.2",
16
+ packages=find_packages(),
17
+ author="0xMihalich",
18
+ author_email="bayanmobile87@gmail.com",
19
+ description=(
20
+ "Library for read and write PGPack "
21
+ "format between PostgreSQL and file."
22
+ ),
23
+ url="https://github.com/0xMihalich/pgpack_dumper",
24
+ long_description=long_description,
25
+ long_description_content_type="text/markdown",
26
+ zip_safe=False,
27
+ package_data={
28
+ "pgpack_dumper.queryes": ["*.sql"],
29
+ },
30
+ include_package_data=True,
31
+ )