Skip to content

Commit dd40d36

Browse files
authored
Merge pull request #3 from dorosch/develop
Release 0.4
2 parents e216511 + 6c7cf30 commit dd40d36

File tree

11 files changed

+128
-35
lines changed

11 files changed

+128
-35
lines changed

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sqlalchemy-timescaledb"
3-
version = "0.3.1"
3+
version = "0.4"
44
authors = [
55
{ name="Andrei Kliatsko", email="andrey.daraschenka@gmail.com" },
66
]
@@ -15,9 +15,11 @@ test = [
1515
"pytest==7.2.1",
1616
"pytest-cov==4.0.0",
1717
"pytest-factoryboy==2.5.1",
18-
"sqlalchemy>=1.3",
18+
"sqlalchemy[asyncio]>=1.3",
1919
"psycopg2-binary==2.9.5",
20-
"alembic==1.9.4"
20+
"alembic==1.9.4",
21+
"asyncpg==0.27.0",
22+
"pytest-asyncio==0.20.3"
2123
]
2224

2325
[project.urls]

sqlalchemy_timescaledb/dialect.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from sqlalchemy import schema
1+
from sqlalchemy import schema, event, DDL
22
from sqlalchemy.dialects.postgresql.asyncpg import PGDialect_asyncpg
33
from sqlalchemy.dialects.postgresql.base import PGDDLCompiler
44
from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
@@ -19,15 +19,30 @@ def post_create_table(self, table):
1919
hypertable = table.kwargs.get('timescaledb_hypertable', {})
2020

2121
if hypertable:
22-
hypertable_sql = \
23-
';\n\nSELECT create_hypertable(' \
24-
f"'{table.name}', '{hypertable['time_column_name']}'" \
25-
");"
26-
27-
return super().post_create_table(table) + hypertable_sql
22+
event.listen(
23+
table,
24+
'after_create',
25+
self.ddl_hypertable(
26+
table.name, hypertable
27+
).execute_if(
28+
dialect='timescaledb'
29+
)
30+
)
2831

2932
return super().post_create_table(table)
3033

34+
@staticmethod
35+
def ddl_hypertable(table_name, hypertable):
36+
return DDL(
37+
f"""
38+
SELECT create_hypertable(
39+
'{table_name}',
40+
'{hypertable['time_column_name']}',
41+
if_not_exists => TRUE
42+
);
43+
"""
44+
)
45+
3146

3247
class TimescaledbDialect:
3348
name = 'timescaledb'

tests/async/__init__.py

Whitespace-only changes.

tests/async/conftest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pytest_asyncio
2+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
3+
4+
from tests.models import Base, DATABASE_URL
5+
6+
7+
@pytest_asyncio.fixture
8+
def async_engine():
9+
yield create_async_engine(
10+
DATABASE_URL.set(drivername='timescaledb+asyncpg')
11+
)
12+
13+
14+
@pytest_asyncio.fixture
15+
async def async_session(async_engine):
16+
async with AsyncSession(async_engine) as session:
17+
yield session
18+
19+
20+
@pytest_asyncio.fixture(autouse=True)
21+
async def setup(async_engine):
22+
async with async_engine.begin() as connection:
23+
await connection.run_sync(Base.metadata.create_all)
24+
yield
25+
async with async_engine.begin() as connection:
26+
await connection.run_sync(Base.metadata.drop_all)

tests/async/test_hypertable.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
from sqlalchemy import text
3+
4+
from tests.models import Metric, User
5+
6+
7+
@pytest.mark.asyncio
8+
class TestHypertable:
9+
async def test_is_hypertable(self, async_session):
10+
assert (await async_session.execute(
11+
text(
12+
f"""
13+
SELECT count(*)
14+
FROM _timescaledb_catalog.hypertable
15+
WHERE table_name = '{Metric.__tablename__}'
16+
"""
17+
)
18+
)).scalar_one()
19+
20+
async def test_is_not_hypertable(self, async_session):
21+
assert not (await async_session.execute(
22+
text(
23+
f"""
24+
SELECT count(*)
25+
FROM _timescaledb_catalog.hypertable
26+
WHERE table_name = '{User.__tablename__}'
27+
"""
28+
)
29+
)).scalar_one()

tests/conftest.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
import pytest
2-
from sqlalchemy import text
32
from pytest_factoryboy import register
3+
from sqlalchemy import text, create_engine
4+
from sqlalchemy.orm import Session
45

5-
from tests.models import engine, Session, Base
6-
from tests.factories import MetricFactory
6+
from tests.factories import MetricFactory, FactorySession
7+
from tests.models import Base, DATABASE_URL
78

89
register(MetricFactory)
910

1011

12+
@pytest.fixture
13+
def engine():
14+
yield create_engine(DATABASE_URL)
15+
16+
17+
@pytest.fixture
18+
def session(engine):
19+
with Session(engine) as session:
20+
yield session
21+
22+
1123
@pytest.fixture(autouse=True)
12-
def setup():
24+
def setup(engine):
25+
FactorySession.configure(bind=engine)
1326
Base.metadata.create_all(bind=engine)
1427
yield
1528
Base.metadata.drop_all(bind=engine)
1629

1730

18-
@pytest.fixture
19-
def session():
20-
yield Session
21-
Session.close()
22-
23-
2431
@pytest.fixture
2532
def is_hypertable(session):
2633
def check_hypertable(table):

tests/factories.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import factory
22
from factory.fuzzy import FuzzyText
3+
from sqlalchemy import orm
34

4-
from tests.models import Metric, Session
5+
from tests.models import Metric
6+
7+
FactorySession = orm.scoped_session(orm.sessionmaker())
58

69

710
class MetricFactory(factory.alchemy.SQLAlchemyModelFactory):
811
class Meta:
912
model = Metric
10-
sqlalchemy_session = Session
13+
sqlalchemy_session = FactorySession
14+
sqlalchemy_session_persistence = 'commit'
1115

1216
name = FuzzyText()
1317
value = 0

tests/migrations/env.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
if config.config_file_name is not None:
1414
fileConfig(config.config_file_name)
1515

16-
from models import Base #, database_url, Metric, User
16+
from models import Base, DATABASE_URL
17+
1718
config.set_section_option(
1819
config.config_ini_section,
1920
"sqlalchemy.url",
20-
# database_url.render_as_string(hide_password=False)
21-
'timescaledb://user:password@0.0.0.0:5432/database'
21+
DATABASE_URL.render_as_string(hide_password=False)
2222
)
2323

2424
target_metadata = Base.metadata

tests/models.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,18 @@
22
import os
33

44
from sqlalchemy import Column, String, DateTime, Float, Integer
5-
from sqlalchemy import create_engine
65
from sqlalchemy.engine import URL
76
from sqlalchemy.orm import declarative_base
8-
from sqlalchemy.orm import sessionmaker, scoped_session
97

10-
database_url = URL.create(
8+
DATABASE_URL = URL.create(
119
host=os.environ.get('POSTGRES_HOST', '0.0.0.0'),
1210
port=os.environ.get('POSTGRES_PORT', 5432),
1311
username=os.environ.get('POSTGRES_USER', 'user'),
1412
password=os.environ.get('POSTGRES_PASSWORD', 'password'),
1513
database=os.environ.get('POSTGRES_DB', 'database'),
1614
drivername=os.environ.get('DRIVERNAME', 'timescaledb')
1715
)
18-
engine = create_engine(database_url)
19-
Session = scoped_session(sessionmaker(bind=engine))
16+
2017
Base = declarative_base()
2118

2219

tests/test_alembic.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
import os
2+
from pathlib import Path
23

34
from alembic import command
45
from alembic.config import Config
56

6-
from tests.models import Base, engine
7+
from tests.models import Base
78

89

910
class TestAlembic:
1011
def setup_class(self):
12+
# TODO: Disable output for alembic
1113
self.config = Config(
1214
os.path.join(os.path.dirname(__file__), 'alembic.ini')
1315
)
16+
self.migration_versions_path = os.path.join(
17+
os.path.dirname(__file__), 'migrations', 'versions'
18+
)
1419

15-
def test_create_revision(self):
20+
def test_create_revision(self, engine):
1621
Base.metadata.drop_all(bind=engine)
17-
script = command.revision(self.config, autogenerate=True)
18-
Base.metadata.create_all(bind=engine)
22+
script = command.revision(
23+
self.config, message='initial', autogenerate=True
24+
)
25+
migration_file = os.path.join(
26+
self.migration_versions_path, f'{script.revision}_initial.py'
27+
)
1928

2029
assert script.down_revision is None
30+
assert Path(migration_file).is_file()
31+
32+
Path(migration_file).unlink()
33+
Base.metadata.create_all(bind=engine)

0 commit comments

Comments
 (0)