perfact-api-base 0.6__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.6/.gitea/workflows/check.yml +26 -0
- perfact_api_base-0.6/.gitea/workflows/publish.yml +31 -0
- perfact_api_base-0.6/.gitignore +7 -0
- perfact_api_base-0.6/.vscode/settings.json +6 -0
- perfact_api_base-0.6/PKG-INFO +59 -0
- perfact_api_base-0.6/README.md +44 -0
- perfact_api_base-0.6/bandit.yml +2 -0
- perfact_api_base-0.6/pyproject.toml +41 -0
- perfact_api_base-0.6/setup.cfg +4 -0
- perfact_api_base-0.6/src/perfact/api/base/__init__.py +17 -0
- perfact_api_base-0.6/src/perfact/api/base/authinfo.py +18 -0
- perfact_api_base-0.6/src/perfact/api/base/model.py +65 -0
- perfact_api_base-0.6/src/perfact/api/base/py.typed +0 -0
- perfact_api_base-0.6/src/perfact/api/base/visibility.py +37 -0
- perfact_api_base-0.6/src/perfact_api_base.egg-info/PKG-INFO +59 -0
- perfact_api_base-0.6/src/perfact_api_base.egg-info/SOURCES.txt +19 -0
- perfact_api_base-0.6/src/perfact_api_base.egg-info/dependency_links.txt +1 -0
- perfact_api_base-0.6/src/perfact_api_base.egg-info/requires.txt +3 -0
- perfact_api_base-0.6/src/perfact_api_base.egg-info/top_level.txt +1 -0
- perfact_api_base-0.6/tests/test_visibility_policy.py +86 -0
- perfact_api_base-0.6/tox.ini +24 -0
|
@@ -0,0 +1,26 @@
|
|
|
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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "[0-9]*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.x"
|
|
20
|
+
|
|
21
|
+
- name: Install build dependencies
|
|
22
|
+
run: python -m pip install --upgrade build twine
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: python -m build
|
|
26
|
+
|
|
27
|
+
- name: Publish to PyPI
|
|
28
|
+
env:
|
|
29
|
+
TWINE_USERNAME: __token__
|
|
30
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
|
31
|
+
run: twine upload dist/*
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: perfact-api-base
|
|
3
|
+
Version: 0.6
|
|
4
|
+
Summary: PerFact API - base package for common infrastructure
|
|
5
|
+
Author-email: Viktor Dick <viktor.dick@perfact.de>
|
|
6
|
+
License: GPL-2.0-or-later
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: SQL
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: psycopg[binary]
|
|
13
|
+
Requires-Dist: sqlalchemy
|
|
14
|
+
Requires-Dist: pydantic-settings
|
|
15
|
+
|
|
16
|
+
# perfact-api-base
|
|
17
|
+
|
|
18
|
+
Base package for PerFact API common infrastructure. Part of the `perfact.api.base` namespace.
|
|
19
|
+
|
|
20
|
+
## What it provides
|
|
21
|
+
|
|
22
|
+
### `Base`
|
|
23
|
+
|
|
24
|
+
Declarative base for all ORM table mappings. Subclasses automatically get:
|
|
25
|
+
|
|
26
|
+
- `id` — `BigInteger` primary key
|
|
27
|
+
- `modtime` — `DateTime` with timezone, defaults to `now()`
|
|
28
|
+
- `author` — set from the DB session user via `db_username()`
|
|
29
|
+
- Column names are automatically prefixed with the table name (e.g. a field `name` on `AppUser` becomes the DB column `appuser_name`)
|
|
30
|
+
- `__tablename__` is derived from the class name in lowercase if not set explicitly
|
|
31
|
+
|
|
32
|
+
### `View`
|
|
33
|
+
|
|
34
|
+
Separate declarative base for SQL view definitions. Views are not created as tables during schema generation. Columns must be declared with explicit names matching the view definition.
|
|
35
|
+
|
|
36
|
+
### Re-exports
|
|
37
|
+
|
|
38
|
+
`ForeignKey`, `relationship`, `Mapped`, `mapped_column` are re-exported so model packages only need to import from this module.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
|
|
44
|
+
|
|
45
|
+
class MyEntity(Base):
|
|
46
|
+
name: Mapped[str]
|
|
47
|
+
parent_id: Mapped[int | None] = mapped_column(ForeignKey("myentity.myentity_id"))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Dependencies
|
|
51
|
+
|
|
52
|
+
- `sqlalchemy`
|
|
53
|
+
- `psycopg[c]`
|
|
54
|
+
- `pydantic-settings`
|
|
55
|
+
|
|
56
|
+
## Maintainers
|
|
57
|
+
|
|
58
|
+
- Viktor Dick <viktor.dick@perfact.de>
|
|
59
|
+
- Alexander Rolfes <alexander.rolfes@perfact.de>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# perfact-api-base
|
|
2
|
+
|
|
3
|
+
Base package for PerFact API common infrastructure. Part of the `perfact.api.base` namespace.
|
|
4
|
+
|
|
5
|
+
## What it provides
|
|
6
|
+
|
|
7
|
+
### `Base`
|
|
8
|
+
|
|
9
|
+
Declarative base for all ORM table mappings. Subclasses automatically get:
|
|
10
|
+
|
|
11
|
+
- `id` — `BigInteger` primary key
|
|
12
|
+
- `modtime` — `DateTime` with timezone, defaults to `now()`
|
|
13
|
+
- `author` — set from the DB session user via `db_username()`
|
|
14
|
+
- Column names are automatically prefixed with the table name (e.g. a field `name` on `AppUser` becomes the DB column `appuser_name`)
|
|
15
|
+
- `__tablename__` is derived from the class name in lowercase if not set explicitly
|
|
16
|
+
|
|
17
|
+
### `View`
|
|
18
|
+
|
|
19
|
+
Separate declarative base for SQL view definitions. Views are not created as tables during schema generation. Columns must be declared with explicit names matching the view definition.
|
|
20
|
+
|
|
21
|
+
### Re-exports
|
|
22
|
+
|
|
23
|
+
`ForeignKey`, `relationship`, `Mapped`, `mapped_column` are re-exported so model packages only need to import from this module.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
|
|
29
|
+
|
|
30
|
+
class MyEntity(Base):
|
|
31
|
+
name: Mapped[str]
|
|
32
|
+
parent_id: Mapped[int | None] = mapped_column(ForeignKey("myentity.myentity_id"))
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Dependencies
|
|
36
|
+
|
|
37
|
+
- `sqlalchemy`
|
|
38
|
+
- `psycopg[c]`
|
|
39
|
+
- `pydantic-settings`
|
|
40
|
+
|
|
41
|
+
## Maintainers
|
|
42
|
+
|
|
43
|
+
- Viktor Dick <viktor.dick@perfact.de>
|
|
44
|
+
- Alexander Rolfes <alexander.rolfes@perfact.de>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.2", "setuptools-scm>=8.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "perfact-api-base"
|
|
7
|
+
authors = [
|
|
8
|
+
{name="Viktor Dick", email="viktor.dick@perfact.de"},
|
|
9
|
+
]
|
|
10
|
+
description = "PerFact API - base package for common infrastructure"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
license = {text = "GPL-2.0-or-later"}
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: SQL",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"psycopg[binary]",
|
|
20
|
+
"sqlalchemy",
|
|
21
|
+
"pydantic-settings",
|
|
22
|
+
]
|
|
23
|
+
dynamic = ["version"]
|
|
24
|
+
requires-python = ">=3.10"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
|
|
28
|
+
[tool.distutils.bdist_wheel]
|
|
29
|
+
universal = 1
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
include-package-data = true
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools_scm]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "W", "I"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .authinfo import AuthInfo
|
|
2
|
+
from .model import Base, PydanticBase, View
|
|
3
|
+
from .visibility import (
|
|
4
|
+
VisibilityAwareModel,
|
|
5
|
+
VisibilityPolicy,
|
|
6
|
+
VisibilityPolicyRegistry,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AuthInfo",
|
|
11
|
+
"Base",
|
|
12
|
+
"PydanticBase",
|
|
13
|
+
"View",
|
|
14
|
+
"VisibilityAwareModel",
|
|
15
|
+
"VisibilityPolicy",
|
|
16
|
+
"VisibilityPolicyRegistry",
|
|
17
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AuthInfo:
|
|
9
|
+
name: Optional[str]
|
|
10
|
+
roles: list[str]
|
|
11
|
+
appstc: Optional[int]
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_unauthorized_authinfo() -> AuthInfo:
|
|
15
|
+
"""
|
|
16
|
+
Returns a predefined AuthInfo for unauthorized requests.
|
|
17
|
+
"""
|
|
18
|
+
return AuthInfo(None, [], None)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
from sqlalchemy import BigInteger, DateTime, ForeignKey, String, func
|
|
5
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
6
|
+
|
|
7
|
+
# Re-export for easier use
|
|
8
|
+
ForeignKey = ForeignKey
|
|
9
|
+
relationship = relationship
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Base(DeclarativeBase):
|
|
13
|
+
"""
|
|
14
|
+
Base class for table mappings. Allows declaring fields without their table prefix,
|
|
15
|
+
the subclass hook will automatically rewrite them.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
type_annotation_map = {str: String(), int: BigInteger()}
|
|
19
|
+
|
|
20
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
21
|
+
modtime: Mapped[datetime] = mapped_column(
|
|
22
|
+
DateTime(timezone=True), server_default=func.now()
|
|
23
|
+
)
|
|
24
|
+
author: Mapped[str | None] = mapped_column(server_default=func.db_username())
|
|
25
|
+
|
|
26
|
+
def __init_subclass__(cls, **kw):
|
|
27
|
+
"""
|
|
28
|
+
Automatically add __tablename__ and prefix mapped columns with table name
|
|
29
|
+
"""
|
|
30
|
+
# 1. Ensure tablename exists before mapping
|
|
31
|
+
if "__tablename__" not in cls.__dict__:
|
|
32
|
+
cls.__tablename__ = cls.__name__.lower()
|
|
33
|
+
|
|
34
|
+
# 2. Let SQLAlchemy build the mapper, table, columns, etc.
|
|
35
|
+
super().__init_subclass__(**kw)
|
|
36
|
+
|
|
37
|
+
# 3. Now we have cls.__table__ and real Column objects. Adjust these so they
|
|
38
|
+
# map to the prefixed column names on the database
|
|
39
|
+
if hasattr(cls, "__table__"):
|
|
40
|
+
prefix = cls.__tablename__ + "_"
|
|
41
|
+
|
|
42
|
+
for col in cls.__table__.columns:
|
|
43
|
+
# Only touch columns that still use their key as name
|
|
44
|
+
# (i.e., no explicit name was given)
|
|
45
|
+
if col.name == col.key:
|
|
46
|
+
new_name = prefix + col.key
|
|
47
|
+
col.name = new_name
|
|
48
|
+
col.key = new_name # keep ORM key in sync
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class View(DeclarativeBase):
|
|
52
|
+
"""
|
|
53
|
+
Separate base for view definitions, so they are not created as tables but
|
|
54
|
+
can be used like tables
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
type_annotation_map = {str: String(), int: BigInteger()}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PydanticBase(BaseModel):
|
|
61
|
+
model_config = ConfigDict(from_attributes=True)
|
|
62
|
+
|
|
63
|
+
id: int
|
|
64
|
+
author: str | None
|
|
65
|
+
modtime: datetime
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
5
|
+
|
|
6
|
+
from .authinfo import AuthInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VisibilityAwareModel:
|
|
10
|
+
"""Marker mixin; signals that a model has an associated VisibilityPolicy."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VisibilityPolicy(ABC):
|
|
14
|
+
"""Row-level visibility rule; subclasses must set model and implement filter()."""
|
|
15
|
+
|
|
16
|
+
model: ClassVar[type]
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def filter(self, auth: AuthInfo) -> ColumnElement[bool]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VisibilityPolicyRegistry:
|
|
23
|
+
"""Model-to-policy map; register() skips if already set, override() always wins."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._policies: dict[type, VisibilityPolicy] = {}
|
|
27
|
+
|
|
28
|
+
def register(self, model: type, policy_cls: type[VisibilityPolicy]) -> None:
|
|
29
|
+
if model not in self._policies:
|
|
30
|
+
self._policies[model] = policy_cls()
|
|
31
|
+
|
|
32
|
+
def override(self, model: type, policy_cls: type[VisibilityPolicy]) -> None:
|
|
33
|
+
self._policies[model] = policy_cls()
|
|
34
|
+
|
|
35
|
+
def get(self, model: type) -> VisibilityPolicy:
|
|
36
|
+
"""Raises KeyError if no policy is registered for model."""
|
|
37
|
+
return self._policies[model]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: perfact-api-base
|
|
3
|
+
Version: 0.6
|
|
4
|
+
Summary: PerFact API - base package for common infrastructure
|
|
5
|
+
Author-email: Viktor Dick <viktor.dick@perfact.de>
|
|
6
|
+
License: GPL-2.0-or-later
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: SQL
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: psycopg[binary]
|
|
13
|
+
Requires-Dist: sqlalchemy
|
|
14
|
+
Requires-Dist: pydantic-settings
|
|
15
|
+
|
|
16
|
+
# perfact-api-base
|
|
17
|
+
|
|
18
|
+
Base package for PerFact API common infrastructure. Part of the `perfact.api.base` namespace.
|
|
19
|
+
|
|
20
|
+
## What it provides
|
|
21
|
+
|
|
22
|
+
### `Base`
|
|
23
|
+
|
|
24
|
+
Declarative base for all ORM table mappings. Subclasses automatically get:
|
|
25
|
+
|
|
26
|
+
- `id` — `BigInteger` primary key
|
|
27
|
+
- `modtime` — `DateTime` with timezone, defaults to `now()`
|
|
28
|
+
- `author` — set from the DB session user via `db_username()`
|
|
29
|
+
- Column names are automatically prefixed with the table name (e.g. a field `name` on `AppUser` becomes the DB column `appuser_name`)
|
|
30
|
+
- `__tablename__` is derived from the class name in lowercase if not set explicitly
|
|
31
|
+
|
|
32
|
+
### `View`
|
|
33
|
+
|
|
34
|
+
Separate declarative base for SQL view definitions. Views are not created as tables during schema generation. Columns must be declared with explicit names matching the view definition.
|
|
35
|
+
|
|
36
|
+
### Re-exports
|
|
37
|
+
|
|
38
|
+
`ForeignKey`, `relationship`, `Mapped`, `mapped_column` are re-exported so model packages only need to import from this module.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
|
|
44
|
+
|
|
45
|
+
class MyEntity(Base):
|
|
46
|
+
name: Mapped[str]
|
|
47
|
+
parent_id: Mapped[int | None] = mapped_column(ForeignKey("myentity.myentity_id"))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Dependencies
|
|
51
|
+
|
|
52
|
+
- `sqlalchemy`
|
|
53
|
+
- `psycopg[c]`
|
|
54
|
+
- `pydantic-settings`
|
|
55
|
+
|
|
56
|
+
## Maintainers
|
|
57
|
+
|
|
58
|
+
- Viktor Dick <viktor.dick@perfact.de>
|
|
59
|
+
- Alexander Rolfes <alexander.rolfes@perfact.de>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
README.md
|
|
3
|
+
bandit.yml
|
|
4
|
+
pyproject.toml
|
|
5
|
+
tox.ini
|
|
6
|
+
.gitea/workflows/check.yml
|
|
7
|
+
.gitea/workflows/publish.yml
|
|
8
|
+
.vscode/settings.json
|
|
9
|
+
src/perfact/api/base/__init__.py
|
|
10
|
+
src/perfact/api/base/authinfo.py
|
|
11
|
+
src/perfact/api/base/model.py
|
|
12
|
+
src/perfact/api/base/py.typed
|
|
13
|
+
src/perfact/api/base/visibility.py
|
|
14
|
+
src/perfact_api_base.egg-info/PKG-INFO
|
|
15
|
+
src/perfact_api_base.egg-info/SOURCES.txt
|
|
16
|
+
src/perfact_api_base.egg-info/dependency_links.txt
|
|
17
|
+
src/perfact_api_base.egg-info/requires.txt
|
|
18
|
+
src/perfact_api_base.egg-info/top_level.txt
|
|
19
|
+
tests/test_visibility_policy.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
perfact
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from sqlalchemy.orm import Mapped
|
|
3
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
4
|
+
|
|
5
|
+
from perfact.api.base import (
|
|
6
|
+
AuthInfo,
|
|
7
|
+
VisibilityAwareModel,
|
|
8
|
+
VisibilityPolicy,
|
|
9
|
+
VisibilityPolicyRegistry,
|
|
10
|
+
)
|
|
11
|
+
from perfact.api.base.model import Base
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelA(Base, VisibilityAwareModel):
|
|
15
|
+
__tablename__ = "model_a"
|
|
16
|
+
name: Mapped[str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelB(Base, VisibilityAwareModel):
|
|
20
|
+
__tablename__ = "model_b"
|
|
21
|
+
name: Mapped[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PolicyForA(VisibilityPolicy):
|
|
25
|
+
model = ModelA
|
|
26
|
+
|
|
27
|
+
def filter(self, auth: AuthInfo) -> ColumnElement[bool]:
|
|
28
|
+
return ModelA.id > 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PolicyForAOverride(VisibilityPolicy):
|
|
32
|
+
model = ModelA
|
|
33
|
+
|
|
34
|
+
def filter(self, auth: AuthInfo) -> ColumnElement[bool]:
|
|
35
|
+
return ModelA.id > 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_register_sets_policy() -> None:
|
|
39
|
+
registry = VisibilityPolicyRegistry()
|
|
40
|
+
registry.register(ModelA, PolicyForA)
|
|
41
|
+
assert isinstance(registry.get(ModelA), PolicyForA)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_register_does_not_overwrite_existing() -> None:
|
|
45
|
+
registry = VisibilityPolicyRegistry()
|
|
46
|
+
registry.register(ModelA, PolicyForA)
|
|
47
|
+
registry.register(ModelA, PolicyForAOverride)
|
|
48
|
+
assert isinstance(registry.get(ModelA), PolicyForA)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_override_sets_policy() -> None:
|
|
52
|
+
registry = VisibilityPolicyRegistry()
|
|
53
|
+
registry.override(ModelA, PolicyForA)
|
|
54
|
+
assert isinstance(registry.get(ModelA), PolicyForA)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_override_replaces_existing() -> None:
|
|
58
|
+
registry = VisibilityPolicyRegistry()
|
|
59
|
+
registry.register(ModelA, PolicyForA)
|
|
60
|
+
registry.override(ModelA, PolicyForAOverride)
|
|
61
|
+
assert isinstance(registry.get(ModelA), PolicyForAOverride)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_second_override_wins() -> None:
|
|
65
|
+
registry = VisibilityPolicyRegistry()
|
|
66
|
+
registry.override(ModelA, PolicyForA)
|
|
67
|
+
registry.override(ModelA, PolicyForAOverride)
|
|
68
|
+
assert isinstance(registry.get(ModelA), PolicyForAOverride)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_registry_get_raises_for_unknown_model() -> None:
|
|
72
|
+
registry = VisibilityPolicyRegistry()
|
|
73
|
+
with pytest.raises(KeyError):
|
|
74
|
+
registry.get(ModelB)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_auth_info_dataclass() -> None:
|
|
78
|
+
auth = AuthInfo(name="alice", roles=["Admin"], appstc=42)
|
|
79
|
+
assert auth.name == "alice"
|
|
80
|
+
assert auth.roles == ["Admin"]
|
|
81
|
+
assert auth.appstc == 42
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_auth_info_optional_appstc() -> None:
|
|
85
|
+
auth = AuthInfo(name="bob", roles=[], appstc=None)
|
|
86
|
+
assert auth.appstc is None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[tox]
|
|
2
|
+
envlist = py3
|
|
3
|
+
isolated_build = true
|
|
4
|
+
|
|
5
|
+
[pytest]
|
|
6
|
+
|
|
7
|
+
[testenv]
|
|
8
|
+
deps =
|
|
9
|
+
ruff
|
|
10
|
+
pytest
|
|
11
|
+
coverage
|
|
12
|
+
psycopg[binary]
|
|
13
|
+
pytest-postgresql
|
|
14
|
+
pytest-cov
|
|
15
|
+
pytest-typing
|
|
16
|
+
bandit
|
|
17
|
+
mypy
|
|
18
|
+
|
|
19
|
+
commands =
|
|
20
|
+
ruff format --check
|
|
21
|
+
ruff check
|
|
22
|
+
bandit --configfile bandit.yml -r src
|
|
23
|
+
mypy src
|
|
24
|
+
# pytest --doctest-modules --cov-branch --cov=src --cov-report=term-missing {posargs:src}
|