pjdev-postgres 4.1.0a2__tar.gz → 4.2.0a2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pjdev-postgres
3
- Version: 4.1.0a2
3
+ Version: 4.2.0a2
4
4
  Project-URL: Documentation, https://gitlab.purplejay.net/keystone/python/-/tree/main/keystone-postgres/README.md
5
5
  Project-URL: Issues, https://gitlab.purplejay.net/keystone/python/-/issues
6
6
  Project-URL: Source, https://gitlab.purplejay.net/keystone/python
@@ -22,6 +22,7 @@ Requires-Dist: ruff; extra == 'dev'
22
22
  Provides-Extra: test
23
23
  Requires-Dist: coverage; extra == 'test'
24
24
  Requires-Dist: pytest; extra == 'test'
25
+ Requires-Dist: pytz; extra == 'test'
25
26
  Description-Content-Type: text/markdown
26
27
 
27
28
  # keystone-postgres
@@ -34,7 +34,8 @@ dev = [
34
34
 
35
35
  test = [
36
36
  "pytest",
37
- "coverage"
37
+ "coverage",
38
+ "pytz"
38
39
  ]
39
40
 
40
41
  [project.urls]
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Chris O'Neill <chris@purplejay.io>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "4.1.0a2"
4
+ __version__ = "4.2.0a2"
@@ -55,4 +55,4 @@ def make_date_validator_for_formats(
55
55
  return validator
56
56
 
57
57
 
58
- date_validator = BeforeValidator(make_date_validator_for_formats(["iso"], date))
58
+ date_validator = BeforeValidator(make_date_validator_for_formats(["iso"], datetime))
@@ -1,9 +1,9 @@
1
- from datetime import datetime, UTC
1
+ from datetime import datetime, timezone
2
2
  from typing import Optional, Annotated
3
- from uuid import UUID
3
+ from uuid import UUID, uuid4
4
4
 
5
5
  from pydantic import BaseModel
6
- from sqlmodel import SQLModel, Field, Index
6
+ from sqlmodel import SQLModel, Field
7
7
 
8
8
  from pjdev_postgres.model_validators import date_validator
9
9
 
@@ -22,7 +22,7 @@ class Auditable(BaseModel):
22
22
  created_by_id: Optional[str] = None
23
23
  created_by: Optional[str] = None
24
24
  created_datetime: Annotated[
25
- datetime, date_validator, Field(default_factory=lambda: datetime.now(UTC))
25
+ datetime, date_validator, Field(default_factory=lambda: datetime.now(timezone.utc), index=True)
26
26
  ]
27
27
  last_modified_by_id: Optional[str] = None
28
28
  last_modified_by: Optional[str] = None
@@ -30,7 +30,7 @@ class Auditable(BaseModel):
30
30
 
31
31
 
32
32
  class TableModel(SQLModel):
33
- row_id: Optional[int] = Field(default=None, primary_key=True)
33
+ id: Annotated[UUID, Field(default_factory=uuid4, primary_key=True)]
34
34
 
35
35
 
36
36
  class Savable(Versioned, Auditable, TableModel):
@@ -38,19 +38,13 @@ class Savable(Versioned, Auditable, TableModel):
38
38
 
39
39
 
40
40
  class History(TableModel, table=True):
41
- entity_name: str
42
- entity_id: int
41
+ entity_name: Annotated[str, Field(index=True)]
42
+ entity_id: Annotated[UUID, Field(index=True)]
43
43
  value: str
44
44
  timestamp: Annotated[
45
- datetime, date_validator, Field(default_factory=lambda: datetime.now(UTC))
45
+ datetime, date_validator, Field(default_factory=lambda: datetime.now(timezone.utc), index=True)
46
46
  ]
47
47
 
48
- __table_args__ = (
49
- Index("ix_history_entity_id", "entity_id"),
50
- Index("ix_history_entity_name", "entity_name"),
51
- Index("ix_history_timestamp", "timestamp"),
52
- )
53
-
54
48
 
55
49
  class ConcurrencyException(BaseException):
56
50
  def __init__(self, message: str):
@@ -1,10 +1,10 @@
1
1
  from contextlib import contextmanager
2
- from datetime import datetime, UTC
3
- from typing import List, Optional, Type, TypeVar, Callable, Tuple
2
+ from datetime import datetime, timezone
3
+ from typing import List, Optional, Type, TypeVar, Callable, Tuple, Any, Generator
4
4
  from uuid import UUID
5
5
 
6
6
  from sqlalchemy import Engine
7
- from sqlmodel import SQLModel, create_engine, Session as SQLModelSession
7
+ from sqlmodel import SQLModel, create_engine, Session as SQLModelSession, Session
8
8
 
9
9
  from pjdev_postgres import postgres_settings, concurrency_service
10
10
  from pjdev_postgres.models import ConnectionOptions, Savable, History
@@ -20,8 +20,9 @@ __ctx = Context()
20
20
 
21
21
 
22
22
  def initialize_engine(
23
- tables: List[Type[SQLModel]],
24
- options: Optional[ConnectionOptions] = None,
23
+ tables: List[Type[SQLModel]],
24
+ options: Optional[ConnectionOptions] = None,
25
+ create_tables: bool = False,
25
26
  ) -> Engine:
26
27
  if options is None:
27
28
  options = ConnectionOptions()
@@ -31,7 +32,7 @@ def initialize_engine(
31
32
  if len(tables) == 0:
32
33
  raise ValueError("Must specify at least one table")
33
34
 
34
- db_url = f"postgresql+psycopg://{settings.username}:{settings.password}@{settings.host}:{settings.port}/{settings.name}"
35
+ db_url = settings.get_url()
35
36
 
36
37
  engine = create_engine(
37
38
  db_url,
@@ -40,20 +41,21 @@ def initialize_engine(
40
41
  max_overflow=options.max_overflow,
41
42
  )
42
43
 
43
- for t in tables:
44
- t.__table__.create(bind=engine, checkfirst=True)
44
+ if create_tables:
45
+ for t in tables:
46
+ t.__table__.create(bind=engine, checkfirst=True)
45
47
 
46
48
  return engine
47
49
 
48
50
 
49
51
  def configure_single_context(
50
- tables: List[Type[SQLModel]], options: Optional[ConnectionOptions] = None
52
+ tables: List[Type[SQLModel]], options: Optional[ConnectionOptions] = None
51
53
  ):
52
54
  __ctx.engine = initialize_engine(tables, options)
53
55
 
54
56
 
55
57
  @contextmanager
56
- def session_context() -> SQLModelSession:
58
+ def session_context() -> Generator[Session, Any, None]:
57
59
  with SQLModelSession(__ctx.engine) as session:
58
60
  try:
59
61
  yield session
@@ -61,20 +63,20 @@ def session_context() -> SQLModelSession:
61
63
  session.close()
62
64
 
63
65
 
64
- def get_session() -> SQLModelSession:
66
+ def get_session() -> Generator[Session, Any, None]:
65
67
  with SQLModelSession(__ctx.engine) as session:
66
68
  yield session
67
69
 
68
70
 
69
71
  def save(
70
- obj: T,
71
- user_resolver: Callable[[], Tuple[Optional[str], str]],
72
- session: SQLModelSession,
73
- concurrency_token: Optional[UUID] = None,
74
- commit: bool = True,
72
+ obj: T,
73
+ user_resolver: Callable[[], Tuple[Optional[str], str]],
74
+ session: SQLModelSession,
75
+ concurrency_token: Optional[UUID] = None,
76
+ commit: bool = True,
75
77
  ) -> T:
76
78
  updated_obj = concurrency_service.validate_version(obj, concurrency_token)
77
- updated_obj.last_modified_datetime = datetime.now(UTC)
79
+ updated_obj.last_modified_datetime = datetime.now(timezone.utc)
78
80
  user_oid, username = user_resolver()
79
81
  updated_obj.last_modified_by_id = user_oid
80
82
  updated_obj.last_modified_by = username
@@ -83,7 +85,7 @@ def save(
83
85
 
84
86
  history = History(
85
87
  entity_name=updated_obj.__class__.__name__.lower(),
86
- entity_id=updated_obj.row_id,
88
+ entity_id=updated_obj.id,
87
89
  value=updated_obj.model_dump_json(),
88
90
  )
89
91
  session.add(history)
@@ -16,6 +16,9 @@ class PostgresSettings(BaseSettings):
16
16
  env_prefix="db_",
17
17
  )
18
18
 
19
+ def get_url(self) -> str:
20
+ return f"postgresql+psycopg://{self.username}:{self.password}@{self.host}:{self.port}/{self.name}"
21
+
19
22
 
20
23
  class Context:
21
24
  settings: Optional[PostgresSettings] = None
@@ -7,4 +7,4 @@ collecting ... collected 1 item
7
7
 
8
8
  tests/tests_for_history.py::test_history_item_is_created_and_saved PASSED [100%]
9
9
 
10
- ============================== 1 passed in 0.63s ===============================
10
+ ============================== 1 passed in 1.06s ===============================
@@ -2,7 +2,7 @@ from pjdev_postgres import postgres_service
2
2
  from pjdev_postgres.models import Savable, History
3
3
  from sqlmodel import Session, select
4
4
  from utilities import init_test_db
5
-
5
+ import pytz
6
6
 
7
7
  def test_history_item_is_created_and_saved():
8
8
  class MockObj(Savable):
@@ -25,8 +25,8 @@ def test_history_item_is_created_and_saved():
25
25
  assert saved_obj.last_modified_by_id is not None
26
26
 
27
27
  with Session(engine) as session2:
28
- mock_row = session2.exec(
29
- select(MockTable).where(MockTable.row_id == obj.row_id)
28
+ mock_row: MockTable = session2.exec(
29
+ select(MockTable).where(MockTable.id == obj.id)
30
30
  ).one_or_none()
31
31
  history_mock = session2.exec(select(History)).all()
32
32
  assert len(history_mock) == 1
@@ -37,6 +37,11 @@ def test_history_item_is_created_and_saved():
37
37
  # https://github.com/tiangolo/sqlmodel/issues/52#issuecomment-1311987732
38
38
 
39
39
  historical_mock_row = MockObj.model_validate_json(history_mock[0].value)
40
+
41
+ # Must add the utc timezone info due to sqlite not preserving tz info
42
+ mock_row.last_modified_datetime = pytz.utc.localize(mock_row.last_modified_datetime)
43
+ mock_row.created_datetime = pytz.utc.localize(mock_row.created_datetime)
44
+
40
45
  assert historical_mock_row == MockObj.model_validate(mock_row)
41
46
  assert mock_row.last_modified_datetime is not None
42
47
  assert mock_row.last_modified_by is not None