perfact-api-base 0.6__tar.gz → 0.7__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.
- perfact_api_base-0.7/.gitea/workflows/check.yml +17 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/PKG-INFO +1 -1
- {perfact_api_base-0.6 → perfact_api_base-0.7}/pyproject.toml +1 -1
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact/api/base/__init__.py +2 -0
- perfact_api_base-0.7/src/perfact/api/base/discovery.py +33 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact_api_base.egg-info/PKG-INFO +1 -1
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact_api_base.egg-info/SOURCES.txt +5 -0
- perfact_api_base-0.7/tests/conftest.py +61 -0
- perfact_api_base-0.7/tests/test_authinfo.py +8 -0
- perfact_api_base-0.7/tests/test_discovery.py +55 -0
- perfact_api_base-0.7/tests/test_model.py +276 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/tox.ini +2 -3
- perfact_api_base-0.6/.gitea/workflows/check.yml +0 -26
- {perfact_api_base-0.6 → perfact_api_base-0.7}/.gitea/workflows/publish.yml +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/.gitignore +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/.vscode/settings.json +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/README.md +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/bandit.yml +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/setup.cfg +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact/api/base/authinfo.py +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact/api/base/model.py +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact/api/base/py.typed +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact/api/base/visibility.py +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact_api_base.egg-info/dependency_links.txt +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact_api_base.egg-info/requires.txt +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact_api_base.egg-info/top_level.txt +0 -0
- {perfact_api_base-0.6 → perfact_api_base-0.7}/tests/test_visibility_policy.py +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .authinfo import AuthInfo
|
|
2
|
+
from .discovery import generic_discover
|
|
2
3
|
from .model import Base, PydanticBase, View
|
|
3
4
|
from .visibility import (
|
|
4
5
|
VisibilityAwareModel,
|
|
@@ -14,4 +15,5 @@ __all__ = [
|
|
|
14
15
|
"VisibilityAwareModel",
|
|
15
16
|
"VisibilityPolicy",
|
|
16
17
|
"VisibilityPolicyRegistry",
|
|
18
|
+
"generic_discover",
|
|
17
19
|
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib.metadata import entry_points
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger("perfact.api.base.discovery")
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generic_discover(registry: T, entrypoint_group: str) -> T:
|
|
11
|
+
"""
|
|
12
|
+
generic discovery loader. Finds all modules with the given entrypoint_group.
|
|
13
|
+
Every module is loaded and the load()-method is called with the given loader.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
if not entrypoint_group:
|
|
17
|
+
raise RuntimeError("No entrypoint_group defined, discovery not possible.")
|
|
18
|
+
|
|
19
|
+
modules_count = 0
|
|
20
|
+
log.info(f"start discovery for {entrypoint_group}")
|
|
21
|
+
for ep in entry_points(group=entrypoint_group):
|
|
22
|
+
log.info(f" try to include {ep.value}")
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
ep.load()(registry)
|
|
26
|
+
modules_count += 1
|
|
27
|
+
except Exception:
|
|
28
|
+
log.exception(f" failed to include plugin {ep.value}")
|
|
29
|
+
log.info(
|
|
30
|
+
f"finished discovery, included {modules_count} plugins for {entrypoint_group}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return registry
|
|
@@ -8,6 +8,7 @@ tox.ini
|
|
|
8
8
|
.vscode/settings.json
|
|
9
9
|
src/perfact/api/base/__init__.py
|
|
10
10
|
src/perfact/api/base/authinfo.py
|
|
11
|
+
src/perfact/api/base/discovery.py
|
|
11
12
|
src/perfact/api/base/model.py
|
|
12
13
|
src/perfact/api/base/py.typed
|
|
13
14
|
src/perfact/api/base/visibility.py
|
|
@@ -16,4 +17,8 @@ src/perfact_api_base.egg-info/SOURCES.txt
|
|
|
16
17
|
src/perfact_api_base.egg-info/dependency_links.txt
|
|
17
18
|
src/perfact_api_base.egg-info/requires.txt
|
|
18
19
|
src/perfact_api_base.egg-info/top_level.txt
|
|
20
|
+
tests/conftest.py
|
|
21
|
+
tests/test_authinfo.py
|
|
22
|
+
tests/test_discovery.py
|
|
23
|
+
tests/test_model.py
|
|
19
24
|
tests/test_visibility_policy.py
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pytest_postgresql import factories
|
|
3
|
+
from sqlalchemy import URL, create_engine, text
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from perfact.api.base.model import Base
|
|
7
|
+
|
|
8
|
+
postgresql_proc = factories.postgresql_proc(port=None)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(scope="module")
|
|
12
|
+
def engine(postgresql_proc):
|
|
13
|
+
"""Module-scoped engine: creates DB, db_username() function, and all tables once."""
|
|
14
|
+
admin_url = URL.create(
|
|
15
|
+
"postgresql+psycopg",
|
|
16
|
+
username=postgresql_proc.user,
|
|
17
|
+
host=postgresql_proc.host,
|
|
18
|
+
port=postgresql_proc.port,
|
|
19
|
+
database="postgres",
|
|
20
|
+
)
|
|
21
|
+
db_name = "test_perfact_base"
|
|
22
|
+
admin_engine = create_engine(admin_url, isolation_level="AUTOCOMMIT")
|
|
23
|
+
with admin_engine.connect() as conn:
|
|
24
|
+
conn.execute(text(f"DROP DATABASE IF EXISTS {db_name}"))
|
|
25
|
+
conn.execute(text(f"CREATE DATABASE {db_name}"))
|
|
26
|
+
admin_engine.dispose()
|
|
27
|
+
|
|
28
|
+
test_url = URL.create(
|
|
29
|
+
"postgresql+psycopg",
|
|
30
|
+
username=postgresql_proc.user,
|
|
31
|
+
host=postgresql_proc.host,
|
|
32
|
+
port=postgresql_proc.port,
|
|
33
|
+
database=db_name,
|
|
34
|
+
)
|
|
35
|
+
eng = create_engine(test_url)
|
|
36
|
+
with eng.connect() as conn:
|
|
37
|
+
conn.execute(
|
|
38
|
+
text(
|
|
39
|
+
"CREATE OR REPLACE FUNCTION db_username() "
|
|
40
|
+
"RETURNS text AS $$ SELECT current_user $$ LANGUAGE SQL"
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
conn.commit()
|
|
44
|
+
Base.metadata.create_all(eng)
|
|
45
|
+
|
|
46
|
+
yield eng
|
|
47
|
+
|
|
48
|
+
Base.metadata.drop_all(eng)
|
|
49
|
+
eng.dispose()
|
|
50
|
+
admin_engine2 = create_engine(admin_url, isolation_level="AUTOCOMMIT")
|
|
51
|
+
with admin_engine2.connect() as conn:
|
|
52
|
+
conn.execute(text(f"DROP DATABASE IF EXISTS {db_name}"))
|
|
53
|
+
admin_engine2.dispose()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def session(engine):
|
|
58
|
+
"""Function-scoped session that rolls back after each test for isolation."""
|
|
59
|
+
with Session(engine) as sess:
|
|
60
|
+
yield sess
|
|
61
|
+
sess.rollback()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from importlib.metadata import EntryPoint
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from perfact.api.base.discovery import generic_discover
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _Registry:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.calls = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _loader_ok(registry: _Registry) -> None:
|
|
15
|
+
registry.calls.append("ok")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _loader_fail(registry: _Registry) -> None:
|
|
19
|
+
raise ValueError("plugin broken")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ep(name: str) -> EntryPoint:
|
|
23
|
+
return EntryPoint(name=name, group="test.group", value=f"{__name__}:{name}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_empty_entrypoint_group_raises():
|
|
27
|
+
with pytest.raises(RuntimeError, match="No entrypoint_group defined"):
|
|
28
|
+
generic_discover(_Registry(), "")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_no_plugins_returns_registry():
|
|
32
|
+
registry = _Registry()
|
|
33
|
+
with patch("perfact.api.base.discovery.entry_points", return_value=[]):
|
|
34
|
+
result = generic_discover(registry, "test.group")
|
|
35
|
+
assert result is registry
|
|
36
|
+
assert result.calls == []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_plugin_loaded_and_called():
|
|
40
|
+
registry = _Registry()
|
|
41
|
+
with patch(
|
|
42
|
+
"perfact.api.base.discovery.entry_points", return_value=[_ep("_loader_ok")]
|
|
43
|
+
):
|
|
44
|
+
result = generic_discover(registry, "test.group")
|
|
45
|
+
assert result.calls == ["ok"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_plugin_load_exception_continues():
|
|
49
|
+
registry = _Registry()
|
|
50
|
+
with patch(
|
|
51
|
+
"perfact.api.base.discovery.entry_points", return_value=[_ep("_loader_fail")]
|
|
52
|
+
):
|
|
53
|
+
result = generic_discover(registry, "test.group")
|
|
54
|
+
assert result is registry
|
|
55
|
+
assert result.calls == []
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import BigInteger, String
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
|
+
|
|
6
|
+
from perfact.api.base.model import Base, ForeignKey, View, relationship
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Test models (registered in Base.metadata at import time)
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Article(Base):
|
|
14
|
+
title: Mapped[str]
|
|
15
|
+
word_count: Mapped[int]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Tag(Base):
|
|
19
|
+
__tablename__ = "tag"
|
|
20
|
+
label: Mapped[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExplicitColModel(Base):
|
|
24
|
+
# Column with an explicit DB name — should not be prefixed
|
|
25
|
+
raw: Mapped[str] = mapped_column("explicit_raw")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Category(Base):
|
|
29
|
+
name: Mapped[str]
|
|
30
|
+
posts: Mapped[list["Post"]] = relationship(back_populates="category")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Post(Base):
|
|
34
|
+
headline: Mapped[str]
|
|
35
|
+
# Cross-model FK: use object reference so it resolves after Category's columns
|
|
36
|
+
# are already prefixed (string "category.id" would fail at that point)
|
|
37
|
+
category_id: Mapped[int] = mapped_column(ForeignKey(Category.id))
|
|
38
|
+
category: Mapped[Category] = relationship(back_populates="posts")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TreeNode(Base):
|
|
42
|
+
label: Mapped[str]
|
|
43
|
+
# Self-referential FK: string form works because it resolves during
|
|
44
|
+
# super().__init_subclass__(), before the prefix step runs
|
|
45
|
+
parent_treenode_id: Mapped[int | None] = mapped_column(ForeignKey("treenode.id"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DiffKeyModel(Base):
|
|
49
|
+
# key differs from name: the prefix guard (col.name == col.key) is False,
|
|
50
|
+
# so this column is intentionally left unprefixed
|
|
51
|
+
value: Mapped[str] = mapped_column("explicit_db_name", key="different_key")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ReportView(View):
|
|
55
|
+
__tablename__ = "v_report"
|
|
56
|
+
# View columns carry explicit names so they match the SQL view definition
|
|
57
|
+
id: Mapped[int] = mapped_column("id", primary_key=True)
|
|
58
|
+
summary: Mapped[str] = mapped_column("summary")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Unit tests — no database required
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestTableName:
|
|
67
|
+
def test_auto_lowercase(self):
|
|
68
|
+
assert Article.__tablename__ == "article"
|
|
69
|
+
|
|
70
|
+
def test_explicit_preserved(self):
|
|
71
|
+
assert Tag.__tablename__ == "tag"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestColumnPrefixing:
|
|
75
|
+
def test_own_columns_prefixed(self):
|
|
76
|
+
names = {c.name for c in Article.__table__.columns}
|
|
77
|
+
assert "article_title" in names
|
|
78
|
+
assert "article_word_count" in names
|
|
79
|
+
|
|
80
|
+
def test_inherited_columns_prefixed(self):
|
|
81
|
+
names = {c.name for c in Article.__table__.columns}
|
|
82
|
+
assert "article_id" in names
|
|
83
|
+
assert "article_modtime" in names
|
|
84
|
+
assert "article_author" in names
|
|
85
|
+
|
|
86
|
+
def test_unprefixed_names_absent(self):
|
|
87
|
+
names = {c.name for c in Article.__table__.columns}
|
|
88
|
+
assert "title" not in names
|
|
89
|
+
assert "id" not in names
|
|
90
|
+
|
|
91
|
+
def test_explicit_tablename_used_as_prefix(self):
|
|
92
|
+
names = {c.name for c in Tag.__table__.columns}
|
|
93
|
+
assert "tag_label" in names
|
|
94
|
+
|
|
95
|
+
def test_explicit_column_name_used_as_base(self):
|
|
96
|
+
# mapped_column("explicit_raw") → name=key="explicit_raw" → prefixed using
|
|
97
|
+
# the explicit name, not the Python attribute name "raw"
|
|
98
|
+
names = {c.name for c in ExplicitColModel.__table__.columns}
|
|
99
|
+
assert "explicitcolmodel_explicit_raw" in names
|
|
100
|
+
assert "explicitcolmodel_raw" not in names
|
|
101
|
+
|
|
102
|
+
def test_fk_column_prefixed(self):
|
|
103
|
+
names = {c.name for c in Post.__table__.columns}
|
|
104
|
+
assert "post_category_id" in names
|
|
105
|
+
|
|
106
|
+
def test_self_ref_fk_column_prefixed(self):
|
|
107
|
+
names = {c.name for c in TreeNode.__table__.columns}
|
|
108
|
+
assert "treenode_parent_treenode_id" in names
|
|
109
|
+
|
|
110
|
+
def test_column_with_different_key_not_prefixed(self):
|
|
111
|
+
# col.name != col.key → guard skips it; DB name stays as declared
|
|
112
|
+
names = {c.name for c in DiffKeyModel.__table__.columns}
|
|
113
|
+
assert "explicit_db_name" in names
|
|
114
|
+
assert "diffkeymodel_explicit_db_name" not in names
|
|
115
|
+
|
|
116
|
+
def test_abstract_subclass_skips_prefix_step(self):
|
|
117
|
+
# Abstract subclasses never get __table__, so __init_subclass__ exits
|
|
118
|
+
# at the hasattr guard without attempting any column renaming
|
|
119
|
+
class InlineAbstract(Base):
|
|
120
|
+
__abstract__ = True
|
|
121
|
+
|
|
122
|
+
assert not hasattr(InlineAbstract, "__table__")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestColumnProperties:
|
|
126
|
+
def test_id_is_primary_key(self):
|
|
127
|
+
pk = {c.name for c in Article.__table__.primary_key.columns}
|
|
128
|
+
assert "article_id" in pk
|
|
129
|
+
|
|
130
|
+
def test_modtime_has_server_default(self):
|
|
131
|
+
# Table.c index uses original keys; find by name via iteration
|
|
132
|
+
col = next(c for c in Article.__table__.columns if c.name == "article_modtime")
|
|
133
|
+
assert col.server_default is not None
|
|
134
|
+
|
|
135
|
+
def test_author_is_nullable(self):
|
|
136
|
+
col = next(c for c in Article.__table__.columns if c.name == "article_author")
|
|
137
|
+
assert col.nullable is True
|
|
138
|
+
|
|
139
|
+
def test_author_has_server_default(self):
|
|
140
|
+
col = next(c for c in Article.__table__.columns if c.name == "article_author")
|
|
141
|
+
assert col.server_default is not None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestTypeAnnotationMap:
|
|
145
|
+
def test_base_str_maps_to_string(self):
|
|
146
|
+
assert isinstance(Base.type_annotation_map[str], String)
|
|
147
|
+
|
|
148
|
+
def test_base_int_maps_to_biginteger(self):
|
|
149
|
+
assert isinstance(Base.type_annotation_map[int], BigInteger)
|
|
150
|
+
|
|
151
|
+
def test_view_str_maps_to_string(self):
|
|
152
|
+
assert isinstance(View.type_annotation_map[str], String)
|
|
153
|
+
|
|
154
|
+
def test_view_int_maps_to_biginteger(self):
|
|
155
|
+
assert isinstance(View.type_annotation_map[int], BigInteger)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestViewClass:
|
|
159
|
+
def test_columns_not_prefixed(self):
|
|
160
|
+
names = {c.name for c in ReportView.__table__.columns}
|
|
161
|
+
assert "id" in names
|
|
162
|
+
assert "summary" in names
|
|
163
|
+
assert "v_report_id" not in names
|
|
164
|
+
|
|
165
|
+
def test_not_in_base_metadata(self):
|
|
166
|
+
assert "v_report" not in Base.metadata.tables
|
|
167
|
+
|
|
168
|
+
def test_in_view_metadata(self):
|
|
169
|
+
assert "v_report" in View.metadata.tables
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestReexports:
|
|
173
|
+
def test_foreignkey_is_sqlalchemy_class(self):
|
|
174
|
+
from sqlalchemy import ForeignKey as SAFK
|
|
175
|
+
|
|
176
|
+
assert ForeignKey is SAFK
|
|
177
|
+
|
|
178
|
+
def test_relationship_is_sqlalchemy_function(self):
|
|
179
|
+
from sqlalchemy.orm import relationship as sa_rel
|
|
180
|
+
|
|
181
|
+
assert relationship is sa_rel
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Integration tests — require the `engine` / `session` fixtures (PostgreSQL)
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestDatabaseOperations:
|
|
190
|
+
def test_tables_created(self, engine):
|
|
191
|
+
from sqlalchemy import inspect as sa_inspect
|
|
192
|
+
|
|
193
|
+
tables = sa_inspect(engine).get_table_names()
|
|
194
|
+
assert "article" in tables
|
|
195
|
+
assert "tag" in tables
|
|
196
|
+
assert "category" in tables
|
|
197
|
+
assert "post" in tables
|
|
198
|
+
assert "treenode" in tables
|
|
199
|
+
|
|
200
|
+
def test_view_not_created_as_table(self, engine):
|
|
201
|
+
from sqlalchemy import inspect as sa_inspect
|
|
202
|
+
|
|
203
|
+
tables = sa_inspect(engine).get_table_names()
|
|
204
|
+
assert "v_report" not in tables
|
|
205
|
+
|
|
206
|
+
def test_insert_and_retrieve(self, session):
|
|
207
|
+
article = Article(title="Hello", word_count=500)
|
|
208
|
+
session.add(article)
|
|
209
|
+
session.flush()
|
|
210
|
+
result = session.get(Article, article.id)
|
|
211
|
+
assert result is not None
|
|
212
|
+
assert result.title == "Hello"
|
|
213
|
+
assert result.word_count == 500
|
|
214
|
+
|
|
215
|
+
def test_id_auto_assigned(self, session):
|
|
216
|
+
article = Article(title="Numbered", word_count=1)
|
|
217
|
+
session.add(article)
|
|
218
|
+
session.flush()
|
|
219
|
+
assert isinstance(article.id, int)
|
|
220
|
+
assert article.id > 0
|
|
221
|
+
|
|
222
|
+
def test_modtime_auto_set_by_server(self, session):
|
|
223
|
+
article = Article(title="Timed", word_count=1)
|
|
224
|
+
session.add(article)
|
|
225
|
+
session.flush()
|
|
226
|
+
session.refresh(article)
|
|
227
|
+
assert isinstance(article.modtime, datetime)
|
|
228
|
+
|
|
229
|
+
def test_author_auto_set_by_server(self, session):
|
|
230
|
+
article = Article(title="Authored", word_count=1)
|
|
231
|
+
session.add(article)
|
|
232
|
+
session.flush()
|
|
233
|
+
session.refresh(article)
|
|
234
|
+
assert article.author is not None
|
|
235
|
+
|
|
236
|
+
def test_author_can_be_explicitly_set(self, session):
|
|
237
|
+
# author=None omits the column → server default fires; an explicit string
|
|
238
|
+
# value takes precedence over the server default
|
|
239
|
+
article = Article(title="Override", word_count=1, author="custom_user")
|
|
240
|
+
session.add(article)
|
|
241
|
+
session.flush()
|
|
242
|
+
session.refresh(article)
|
|
243
|
+
assert article.author == "custom_user"
|
|
244
|
+
|
|
245
|
+
def test_fk_relationship(self, session):
|
|
246
|
+
cat = Category(name="Science")
|
|
247
|
+
post = Post(headline="Big Discovery", category=cat)
|
|
248
|
+
session.add_all([cat, post])
|
|
249
|
+
session.flush()
|
|
250
|
+
session.refresh(cat)
|
|
251
|
+
assert len(cat.posts) == 1
|
|
252
|
+
assert cat.posts[0].headline == "Big Discovery"
|
|
253
|
+
|
|
254
|
+
def test_fk_back_reference(self, session):
|
|
255
|
+
cat = Category(name="Tech")
|
|
256
|
+
post = Post(headline="AI News", category=cat)
|
|
257
|
+
session.add_all([cat, post])
|
|
258
|
+
session.flush()
|
|
259
|
+
session.refresh(post)
|
|
260
|
+
assert post.category.name == "Tech"
|
|
261
|
+
|
|
262
|
+
def test_self_referential_fk(self, session):
|
|
263
|
+
root = TreeNode(label="root")
|
|
264
|
+
session.add(root)
|
|
265
|
+
session.flush()
|
|
266
|
+
child = TreeNode(label="child", parent_treenode_id=root.id)
|
|
267
|
+
session.add(child)
|
|
268
|
+
session.flush()
|
|
269
|
+
assert child.parent_treenode_id == root.id
|
|
270
|
+
|
|
271
|
+
def test_session_isolation_via_rollback(self, session):
|
|
272
|
+
# Data inserted here is rolled back by the fixture — other tests are unaffected
|
|
273
|
+
article = Article(title="Ephemeral", word_count=0)
|
|
274
|
+
session.add(article)
|
|
275
|
+
session.flush()
|
|
276
|
+
assert session.get(Article, article.id) is not None
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
envlist = py3
|
|
3
3
|
isolated_build = true
|
|
4
4
|
|
|
5
|
-
[pytest]
|
|
6
|
-
|
|
7
5
|
[testenv]
|
|
6
|
+
pythonpath = {toxinidir}
|
|
8
7
|
deps =
|
|
9
8
|
ruff
|
|
10
9
|
pytest
|
|
@@ -21,4 +20,4 @@ commands =
|
|
|
21
20
|
ruff check
|
|
22
21
|
bandit --configfile bandit.yml -r src
|
|
23
22
|
mypy src
|
|
24
|
-
|
|
23
|
+
pytest --cov-branch --cov=perfact.api.base --cov-report=term-missing --cov-fail-under=100 {posargs:tests}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
name: Tox tests
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
branches:
|
|
5
|
-
- 'main'
|
|
6
|
-
pull_request: {}
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
build:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
strategy:
|
|
12
|
-
matrix:
|
|
13
|
-
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
|
14
|
-
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
-
uses: actions/setup-python@v5
|
|
19
|
-
with:
|
|
20
|
-
python-version: ${{ matrix.python-version }}
|
|
21
|
-
- name: Install dependencies
|
|
22
|
-
run: |
|
|
23
|
-
python -m pip install --upgrade pip
|
|
24
|
-
python -m pip install tox tox-gh-actions
|
|
25
|
-
- name: Test with tox
|
|
26
|
-
run: tox
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{perfact_api_base-0.6 → perfact_api_base-0.7}/src/perfact_api_base.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|