diff --git a/.gitignore b/.gitignore index 1d09582..059a98e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +uv.lock +.venv # Unit test / coverage reports htmlcov/ diff --git a/README.md b/README.md index 31f7825..78e357f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov](https://codecov.io/gh/dorosch/sqlalchemy-timescaledb/branch/develop/graph/badge.svg?token=Gzh7KpADjZ)][3] [![Downloads](https://pepy.tech/badge/sqlalchemy-timescaledb)][4] -This is the TimescaleDB dialect driver for SQLAlchemy. Drivers `psycopg2` and `asyncpg` are supported. +This is the TimescaleDB dialect driver for SQLAlchemy. Drivers `psycopg2`, `psycopg` and `asyncpg` are supported. ## Install @@ -22,6 +22,7 @@ import datetime from sqlalchemy import create_engine, MetaData from sqlalchemy import Table, Column, Integer, String, DateTime +# or use timescaledb+psycopg/asyncpg engine = create_engine('timescaledb://user:password@host:port/database') metadata = MetaData() metadata.bind = engine diff --git a/pyproject.toml b/pyproject.toml index 785b9d1..f6eef3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,9 +16,10 @@ test = [ "pytest-cov==4.0.0", "pytest-factoryboy==2.5.1", "sqlalchemy[asyncio]>=1.3", - "psycopg2-binary==2.9.5", + "psycopg2-binary==2.9.9", + "psycopg[binary]==3.2.3", "alembic==1.9.4", - "asyncpg==0.27.0", + "asyncpg==0.30.0", "pytest-asyncio==0.20.3" ] @@ -28,6 +29,7 @@ test = [ [project.entry-points."sqlalchemy.dialects"] "timescaledb" = "sqlalchemy_timescaledb.dialect:TimescaledbPsycopg2Dialect" "timescaledb.psycopg2" = "sqlalchemy_timescaledb.dialect:TimescaledbPsycopg2Dialect" +"timescaledb.psycopg" = "sqlalchemy_timescaledb.dialect:TimescaledbPsycopgDialect" "timescaledb.asyncpg" = "sqlalchemy_timescaledb.dialect:TimescaledbAsyncpgDialect" [build-system] diff --git a/sqlalchemy_timescaledb/dialect.py b/sqlalchemy_timescaledb/dialect.py index 6274f55..2c82504 100644 --- a/sqlalchemy_timescaledb/dialect.py +++ b/sqlalchemy_timescaledb/dialect.py @@ -2,6 +2,7 @@ from sqlalchemy.dialects.postgresql.asyncpg import PGDialect_asyncpg from sqlalchemy.dialects.postgresql.base import PGDDLCompiler from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2 +from sqlalchemy.dialects.postgresql.psycopg import PGDialect_psycopg try: import alembic @@ -74,3 +75,8 @@ class TimescaledbPsycopg2Dialect(TimescaledbDialect, PGDialect_psycopg2): class TimescaledbAsyncpgDialect(TimescaledbDialect, PGDialect_asyncpg): driver = 'asyncpg' supports_statement_cache = True + + +class TimescaledbPsycopgDialect(TimescaledbDialect, PGDialect_psycopg): + driver = 'psycopg' + supports_statement_cache = True diff --git a/tests/psycopg/__init__.py b/tests/psycopg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/psycopg/conftest.py b/tests/psycopg/conftest.py new file mode 100644 index 0000000..02e2d27 --- /dev/null +++ b/tests/psycopg/conftest.py @@ -0,0 +1,43 @@ +import pytest +from sqlalchemy import create_engine, text + +from sqlalchemy.orm import Session + +from tests.factories import FactorySession +from tests.models import Base, DATABASE_URL + + +@pytest.fixture +def psycopg_engine(): + yield create_engine( + DATABASE_URL.set(drivername='timescaledb+psycopg') + ) + +@pytest.fixture +def session(psycopg_engine): + with Session(psycopg_engine) as session: + yield session + + +@pytest.fixture(autouse=True) +def setup(psycopg_engine): + FactorySession.configure(bind=psycopg_engine) + Base.metadata.create_all(bind=psycopg_engine) + yield + Base.metadata.drop_all(bind=psycopg_engine) + + +@pytest.fixture +def is_hypertable(session): + def check_hypertable(table): + return session.execute( + text( + f""" + SELECT count(*) + FROM _timescaledb_catalog.hypertable + WHERE table_name = '{table.__tablename__}' + """ + ) + ).scalar_one() == 1 + + return check_hypertable diff --git a/tests/psycopg/test_hypertable.py b/tests/psycopg/test_hypertable.py new file mode 100644 index 0000000..2210cf5 --- /dev/null +++ b/tests/psycopg/test_hypertable.py @@ -0,0 +1,9 @@ +from tests.models import Metric, User + + +class TestHypertable: + def test_is_hypertable(self, is_hypertable): + assert is_hypertable(Metric) + + def test_is_not_hypertable(self, is_hypertable): + assert not is_hypertable(User)