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.
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/PKG-INFO +2 -1
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/pyproject.toml +2 -1
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/src/pjdev_postgres/__about__.py +1 -1
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/src/pjdev_postgres/model_validators.py +1 -1
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/src/pjdev_postgres/models.py +8 -14
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/src/pjdev_postgres/postgres_service.py +20 -18
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/src/pjdev_postgres/postgres_settings.py +3 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/test_report_pjdev-postgres.txt +1 -1
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/tests/tests_for_history.py +8 -3
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/.gitignore +0 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/LICENSE.txt +0 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/README.md +0 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/src/pjdev_postgres/__init__.py +0 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/src/pjdev_postgres/concurrency_service.py +0 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/test.sh +0 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/tests/__init__.py +0 -0
- {pjdev_postgres-4.1.0a2 → pjdev_postgres-4.2.0a2}/tests/utilities.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pjdev-postgres
|
|
3
|
-
Version: 4.
|
|
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
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from datetime import datetime,
|
|
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
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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,
|
|
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
|
-
|
|
24
|
-
|
|
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 =
|
|
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
|
-
|
|
44
|
-
t
|
|
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
|
-
|
|
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() ->
|
|
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() ->
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|