Skip to content

Commit 1541070

Browse files
authored
Merge pull request #120 from schireson/dc/psycopg
feat: Use psycopg instead of psycopg2 for postgres.
2 parents 2ac184d + a01a2c2 commit 1541070

17 files changed

Lines changed: 222 additions & 439 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
python-version: ${{ matrix.python-version }}
3838

3939
- name: Install dependencies
40-
run: uv sync --extra psycopg2-binary --extra s3
40+
run: uv sync --extra postgres --extra s3
4141

4242
- name: Install specific sqlalchemy version
4343
run: uv pip install 'sqlalchemy~=${{ matrix.sqlalchemy-version }}'

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### v2.9.0
4+
5+
* Swap to psycopg from psycopg2
6+
37
### [v2.8.5](https://github.com/schireson/databudgie/compare/v2.8.4...v2.8.5) (2024-02-20)
48

59
#### Features

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ COPY src src
1818

1919
RUN poetry build
2020
RUN (export version=$(find dist -name '*.whl'); \
21-
pip install "${version}[s3,psycopg2]")
21+
pip install "${version}[s3,postgres]")
2222

2323
FROM python:3.9
2424

Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@
33
VERSION=$(shell python -c 'from importlib import metadata; print(metadata.version("databudgie"))')
44

55
install:
6-
uv sync --extra psycopg2-binary --extra s3
6+
uv sync --extra postgres --extra s3
77

88

99
format:
1010
uv run ruff check --fix src tests
11-
uv run black src tests
11+
uv run ruff format src tests
1212

1313
lint:
1414
uv run ruff check src tests || exit 1
1515
uv run mypy --namespace-packages src tests || exit 1
16-
uv run black --check --diff src tests || exit 1
1716

1817
test:
1918
uv run coverage run -a -m pytest src tests

pyproject.toml

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "databudgie"
3-
version = "2.8.6"
3+
version = "2.9.0"
44
description = "Ergonomic and flexible tool for database backup and restore"
55
readme = "README.md"
66
license = { text = "MIT" }
@@ -20,26 +20,7 @@ dependencies = [
2020

2121
[project.optional-dependencies]
2222
s3 = ["boto3"]
23-
psycopg2 = ["psycopg2>=2.7"]
24-
psycopg2-binary = ["psycopg2-binary>=2.7"]
25-
dev = [
26-
"boto3==1.34.100",
27-
"black==22.3.0",
28-
"boto3-stubs[s3]>=1.18.38",
29-
"coverage>=5",
30-
"freezegun",
31-
"mypy>=1.14",
32-
"pytest>=6.2.4",
33-
"pytest-mock-resources[docker]>=2.1.10",
34-
"responses>=0.10.9",
35-
"ruff==0.0.254",
36-
"types-freezegun>=0.1.3",
37-
"types-requests>=0.1.11",
38-
"faker>=8.12.1",
39-
"moto[s3]>=5.0.0",
40-
"sqlalchemy-model-factory>=0.4.5",
41-
"types-click>=7.1.5",
42-
]
23+
postgres = ["psycopg[binary]>=3.0"]
4324

4425
[project.scripts]
4526
databudgie = "databudgie.__main__:run"
@@ -52,16 +33,15 @@ package = true
5233

5334
[dependency-groups]
5435
dev = [
55-
"boto3==1.34.100",
56-
"black==22.3.0",
57-
"boto3-stubs[s3]>=1.18.38",
36+
"boto3",
37+
"boto3-stubs[s3]",
5838
"coverage>=5",
5939
"freezegun",
6040
"mypy>=0.991",
6141
"pytest>=6.2.4",
6242
"pytest-mock-resources[docker]>=2.1.10",
6343
"responses>=0.10.9",
64-
"ruff==0.0.254",
44+
"ruff>=0.9",
6545
"types-freezegun>=0.1.3",
6646
"types-requests>=0.1.11",
6747
"faker>=8.12.1",
@@ -70,16 +50,40 @@ dev = [
7050
"types-click>=7.1.5",
7151
]
7252

73-
[tool.black]
74-
line_length = 120
53+
[tool.ruff]
54+
line-length = 120
55+
src = ["src", "tests"]
56+
57+
[tool.ruff.lint]
58+
select = ["C", "D", "E", "F", "I", "N", "Q", "RET", "RUF", "S", "T", "UP", "YTT"]
59+
ignore = ["C901", "E501", "S101", "D1", "D203", "D213", "D406", "D407", "D408", "D409", "D413"]
60+
extend-ignore = [
61+
"D1",
62+
63+
"D203",
64+
"D204",
65+
"D213",
66+
"D215",
67+
"D400",
68+
"D404",
69+
"D406",
70+
"D407",
71+
"D408",
72+
"D409",
73+
"D413",
74+
]
75+
76+
[tool.ruff.lint.isort]
77+
order-by-type = false
78+
79+
[tool.ruff.lint.per-file-ignores]
80+
"tests/*" = ["T201", "D", "S", "N801", "N802", 'N806']
7581

76-
[tool.isort]
77-
profile = 'black'
78-
known_first_party = 'databudgie,tests'
79-
line_length = 120
80-
float_to_top = true
81-
order_by_type = false
82-
use_parentheses = true
82+
[tool.ruff.lint.pyupgrade]
83+
keep-runtime-typing = true
84+
85+
[tool.ruff.format]
86+
docstring-code-format = true
8387

8488
[tool.mypy]
8589
strict_optional = true
@@ -100,10 +104,6 @@ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__mai
100104
source = ["src"]
101105
branch = true
102106

103-
[tool.pydocstyle]
104-
ignore = 'D1,D200,D202,D203,D204,D213,D406,D407,D413'
105-
match_dir = '^[^\.{]((?!igrations).)*'
106-
107107
[tool.pytest.ini_options]
108108
doctest_optionflags = "NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ELLIPSIS"
109109
addopts = "--doctest-modules -vv --ff --strict-markers"

ruff.toml

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/databudgie/adapter/postgres.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import os
44
import shlex
55
import shutil
6-
import subprocess # nosec
6+
import subprocess
77
from typing import cast, Dict, List
88

9-
from psycopg2._psycopg import cursor
9+
from psycopg import Cursor, sql
1010
from sqlalchemy import text
1111
from sqlalchemy.engine import create_engine, Engine
1212
from sqlalchemy.engine.url import URL
13+
from typing_extensions import LiteralString
1314

1415
from databudgie.adapter.base import Adapter, QueryResult
1516
from databudgie.output import Console, default_console
@@ -46,24 +47,29 @@ def export_query(self, query: str) -> QueryResult:
4647
result = QueryResult()
4748
with result.binary_buffer() as buffer:
4849
with contextlib.closing(engine.raw_connection()) as conn:
49-
with cast(cursor, conn.cursor()) as cursor_:
50-
copy = f"COPY ({query}) TO STDOUT CSV HEADER"
50+
with cast(Cursor, conn.cursor()) as cursor:
51+
statement = sql.SQL(cast(LiteralString, f"COPY ({query}) TO STDOUT CSV HEADER"))
5152

52-
cursor_.copy_expert(copy, buffer)
53-
result.row_count = cursor_.rowcount
53+
with cursor.copy(statement) as copy:
54+
while data := copy.read():
55+
buffer.write(data)
56+
result.row_count = cursor.rowcount
5457

5558
return result
5659

5760
def import_csv(self, csv_file: io.TextIOBase, table: str):
5861
engine: Engine = cast(Engine, self.session.get_bind())
5962

6063
# Reading the header line from the buffer removes it for the ingest
61-
columns: List[str] = [f'"{c}"' for c in csv_file.readline().strip().split(",")]
62-
copy = "COPY {table} ({columns}) FROM STDIN CSV".format(table=table, columns=",".join(columns))
64+
columns = [sql.Identifier(c) for c in csv_file.readline().strip().split(",")]
65+
statement = sql.SQL("COPY {table} ({columns}) FROM STDIN CSV").format(
66+
table=sql.Identifier(*table.split(".")), columns=sql.SQL(", ").join(columns)
67+
)
6368

6469
with contextlib.closing(engine.raw_connection()) as conn:
65-
with cast(cursor, conn.cursor()) as cursor_:
66-
cursor_.copy_expert(copy, csv_file)
70+
with cast(Cursor, conn.cursor()) as cursor:
71+
with cursor.copy(statement) as copy:
72+
copy.write(csv_file.read())
6773
conn.commit()
6874

6975
def export_schema_ddl(self, name: str, console: Console = default_console) -> bytes:
@@ -73,7 +79,10 @@ def export_schema_ddl(self, name: str, console: Console = default_console) -> by
7379

7480
url = self.session.connection().engine.url
7581
result = pg_dump(url, f"--schema-only --schema={name} --exclude-table={name}.*")
76-
return result.replace(f"CREATE SCHEMA {name};".encode(), f"CREATE SCHEMA IF NOT EXISTS {name};".encode())
82+
return result.replace(
83+
f"CREATE SCHEMA {name};".encode(),
84+
f"CREATE SCHEMA IF NOT EXISTS {name};".encode(),
85+
)
7786

7887
def export_table_ddl(self, table_name: str, console: Console = default_console):
7988
if not shutil.which("pg_dump"):
@@ -205,7 +214,8 @@ def collect_table_dependencies(self, table_op: TableOp, console: Console = defau
205214
)
206215

207216
results = self.session.execute(
208-
collect_tables, params={"schema": table_op.schema, "table_name": table_op.table_name}
217+
collect_tables,
218+
params={"schema": table_op.schema, "table_name": table_op.table_name},
209219
)
210220

211221
return [row[0] for row in results]
@@ -235,10 +245,18 @@ def collect_table_sequences(self) -> Dict[str, List[str]]:
235245
return result
236246

237247
def collect_sequence_value(self, sequence_name: str) -> int:
238-
return cast(int, self.session.execute(text(f"SELECT last_value from {sequence_name}")).scalar()) # noqa: S608
248+
return cast(
249+
int,
250+
self.session.execute(
251+
text(f"SELECT last_value from {sequence_name}") # noqa: S608
252+
).scalar(),
253+
)
239254

240255
def restore_sequence_value(self, sequence_name: str, value: int) -> int:
241-
return cast(int, self.session.execute(text(f"SELECT setval('{sequence_name}', {value})")).scalar())
256+
return cast(
257+
int,
258+
self.session.execute(text(f"SELECT setval('{sequence_name}', {value})")).scalar(),
259+
)
242260

243261

244262
def pg_dump(url: URL, rest: str = "", no_comments=True, clean=True) -> bytes:
@@ -254,8 +272,11 @@ def pg_dump(url: URL, rest: str = "", no_comments=True, clean=True) -> bytes:
254272
command = shlex.split(raw_command)
255273

256274
try:
257-
result = subprocess.run( # nosec
258-
command, capture_output=True, env={**os.environ, "PGPASSWORD": str(url.password or "")}, check=True
275+
result = subprocess.run( # noqa: S603
276+
command,
277+
capture_output=True,
278+
env={**os.environ, "PGPASSWORD": str(url.password or "")},
279+
check=True,
259280
)
260281
except subprocess.CalledProcessError as e:
261282
raise RuntimeError(e.stderr)

src/databudgie/compression.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import gzip
22
import io
3-
from typing import Dict, Optional, Type
3+
from typing import ClassVar, Dict, Optional, Type
44

55

66
class Compressor:
77
name: str = ""
88
extension: str = ""
99

10-
compressors: Dict[Optional[str], "Compressor"] = {}
10+
compressors: ClassVar[Dict[Optional[str], "Compressor"]] = {}
1111

1212
@classmethod
1313
def get_with_name(cls, name: Optional[str]) -> "Compressor":

src/databudgie/manifest/manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ def __init__(self, session: Session, table_name: str, action: str):
1717

1818
self._transaction_id: Optional[int] = None
1919

20-
@functools.lru_cache()
20+
@functools.lru_cache
2121
def manifest_table(self):
2222
schema, table = parse_table(self.table_name)
2323
self.metadata = MetaData()
2424
self.metadata.reflect(bind=self.session.get_bind(), schema=schema, only=[table])
2525
return Table(table, self.metadata, autoload=True, schema=schema)
2626

27-
@functools.lru_cache()
27+
@functools.lru_cache
2828
def transaction_id(self):
2929
if self._transaction_id is None:
3030
table = self.manifest_table()

src/databudgie/output.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ def update(self, task, description, advance=1):
4949

5050
__all__ = [
5151
"Console",
52-
"default_console",
5352
"Progress",
54-
"Traceback",
5553
"Table",
54+
"Traceback",
55+
"default_console",
5656
]

0 commit comments

Comments
 (0)