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.
@@ -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,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1 @@
1
+ perfact