perfact-api-base 0.6__py2.py3-none-any.whl
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/__init__.py +17 -0
- perfact/api/base/authinfo.py +18 -0
- perfact/api/base/model.py +65 -0
- perfact/api/base/py.typed +0 -0
- perfact/api/base/visibility.py +37 -0
- perfact_api_base-0.6.dist-info/METADATA +59 -0
- perfact_api_base-0.6.dist-info/RECORD +9 -0
- perfact_api_base-0.6.dist-info/WHEEL +6 -0
- perfact_api_base-0.6.dist-info/top_level.txt +1 -0
|
@@ -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,9 @@
|
|
|
1
|
+
perfact/api/base/__init__.py,sha256=ALT8E3u0sHO1hXPuSxH56FsQL5jdaJYlb7Gl3KPVPE0,340
|
|
2
|
+
perfact/api/base/authinfo.py,sha256=w9vXRZg1JUsE_g4CP4STsk3Gsin12dqSMv_A0nLMgTU,395
|
|
3
|
+
perfact/api/base/model.py,sha256=jQnQyF6nwf2DP67gV1LiruM8jgN4996DQ_BQ0aLcWNs,2157
|
|
4
|
+
perfact/api/base/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
perfact/api/base/visibility.py,sha256=QQirZqxWzEjMxMLDcY9pggOtMD7FMUs3I8CEogxT-GU,1158
|
|
6
|
+
perfact_api_base-0.6.dist-info/METADATA,sha256=bPm8uA3Q1ANZqACM_PE4_iXxcraBw2WG1VMyjLdKp2c,1854
|
|
7
|
+
perfact_api_base-0.6.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109
|
|
8
|
+
perfact_api_base-0.6.dist-info/top_level.txt,sha256=1odO3B1JiDF2Lqgnop8k7K4Xs1y_LdwehM53l1NDOnc,8
|
|
9
|
+
perfact_api_base-0.6.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
perfact
|