perfact-api-app 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.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: perfact-api-app
3
+ Version: 0.6
4
+ Summary: PerFact API - SQLAlchemy models for the app namespace
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: sqlalchemy
13
+ Requires-Dist: perfact-api-base-model
14
+
15
+ # perfact-api-app
16
+
17
+ SQLAlchemy models for the `app` namespace of the PerFact API.
18
+
19
+ ## Models
20
+
21
+ Provides ORM models under `perfact.api.app.model`:
22
+
23
+ - `AppUser` / `AppUserKey` / `AppUserLogin` — user accounts, API keys, login sessions
24
+ - `AppStc` — org areas (Strukturknoten), hierarchical
25
+ - `AppPerm` / `AppPermXGroup` / `AppPermXStc` — permissions and their assignments
26
+ - `AppGroup` / `AppUserXPerm` / `AppUserXStc` — group and user-permission mappings
27
+ - `AppTblCleanup` — table cleanup job model
28
+
29
+ ## Installation
30
+
31
+ ```sh
32
+ pip install perfact-api-app
33
+ ```
34
+
35
+ ## Development
36
+
37
+ ```sh
38
+ pip install -e ../perfact-api-base-model/
39
+ pip install -e .
40
+ tox
41
+ ```
42
+
43
+ ## Maintainers
44
+
45
+ - Viktor Dick <viktor.dick@perfact.de>
@@ -0,0 +1,31 @@
1
+ # perfact-api-app
2
+
3
+ SQLAlchemy models for the `app` namespace of the PerFact API.
4
+
5
+ ## Models
6
+
7
+ Provides ORM models under `perfact.api.app.model`:
8
+
9
+ - `AppUser` / `AppUserKey` / `AppUserLogin` — user accounts, API keys, login sessions
10
+ - `AppStc` — org areas (Strukturknoten), hierarchical
11
+ - `AppPerm` / `AppPermXGroup` / `AppPermXStc` — permissions and their assignments
12
+ - `AppGroup` / `AppUserXPerm` / `AppUserXStc` — group and user-permission mappings
13
+ - `AppTblCleanup` — table cleanup job model
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ pip install perfact-api-app
19
+ ```
20
+
21
+ ## Development
22
+
23
+ ```sh
24
+ pip install -e ../perfact-api-base-model/
25
+ pip install -e .
26
+ tox
27
+ ```
28
+
29
+ ## Maintainers
30
+
31
+ - Viktor Dick <viktor.dick@perfact.de>
@@ -0,0 +1,40 @@
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-app"
7
+ authors = [
8
+ {name="Viktor Dick", email="viktor.dick@perfact.de"},
9
+ ]
10
+ description = "PerFact API - SQLAlchemy models for the app namespace"
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
+ "sqlalchemy",
20
+ "perfact-api-base-model",
21
+ ]
22
+ dynamic = ["version"]
23
+ requires-python = ">=3.10"
24
+
25
+ [tool.distutils.bdist_wheel]
26
+ universal = 1
27
+
28
+ [tool.setuptools]
29
+ include-package-data = true
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.setuptools_scm]
35
+ root = ".."
36
+ fallback_version = "0.0.0"
37
+
38
+ [tool.ruff]
39
+ [tool.ruff.lint]
40
+ select = ["E", "F", "W", "I"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ from .appgroup import AppGroup
2
+ from .appperm import AppPerm
3
+ from .apppermxgroup import AppPermXGroup
4
+ from .apppermxstc import AppPermXStc
5
+ from .appstc import AppStc, AppStc_Paths
6
+ from .appuser import AppUser
7
+ from .appuserkey import AppUserKey
8
+ from .appuserlogin import AppUserLogin
9
+ from .appuserxperm import AppUserXPerm
10
+ from .appuserxstc import AppUserXStc
11
+ from .authinfo import AuthInfo
12
+ from .selfilterable import Selfilterable
13
+
14
+ __all__ = [
15
+ "AppGroup",
16
+ "AppPerm",
17
+ "AppPermXGroup",
18
+ "AppPermXStc",
19
+ "AppStc",
20
+ "AppStc_Paths",
21
+ "AppUser",
22
+ "AppUserKey",
23
+ "AppUserLogin",
24
+ "AppUserXPerm",
25
+ "AppUserXStc",
26
+ "Selfilterable",
27
+ "AuthInfo",
28
+ ]
@@ -0,0 +1,5 @@
1
+ from perfact.api.base.model import Base, Mapped
2
+
3
+
4
+ class AppGroup(Base):
5
+ zoperole: Mapped[str]
@@ -0,0 +1,5 @@
1
+ from perfact.api.base.model import Base, Mapped
2
+
3
+
4
+ class AppPerm(Base):
5
+ name: Mapped[str]
@@ -0,0 +1,11 @@
1
+ from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
2
+
3
+ from .appgroup import AppGroup
4
+ from .appperm import AppPerm
5
+
6
+
7
+ class AppPermXGroup(Base):
8
+ appgroup_id: Mapped[int] = mapped_column(ForeignKey(AppGroup.id))
9
+ appperm_id: Mapped[int] = mapped_column(ForeignKey(AppPerm.id))
10
+ perm: Mapped[AppPerm] = relationship()
11
+ group: Mapped[AppGroup] = relationship()
@@ -0,0 +1,12 @@
1
+ from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
2
+
3
+ from .appperm import AppPerm
4
+ from .appstc import AppStc
5
+
6
+
7
+ class AppPermXStc(Base):
8
+ appperm_id: Mapped[int] = mapped_column(ForeignKey(AppPerm.id))
9
+ appstc_id: Mapped[int] = mapped_column(ForeignKey(AppStc.id))
10
+
11
+ perm: Mapped[AppPerm] = relationship()
12
+ stc: Mapped[AppStc] = relationship()
@@ -0,0 +1,16 @@
1
+ from perfact.api.base.model import Base, ForeignKey, Mapped, View, mapped_column
2
+ from sqlalchemy.types import ARRAY, Integer
3
+
4
+
5
+ class AppStc(Base):
6
+ name: Mapped[str]
7
+ parent_appstc_id: Mapped[int | None] = mapped_column(ForeignKey("appstc.id"))
8
+
9
+
10
+ class AppStc_Paths(View):
11
+ __tablename__ = "appstc_paths"
12
+ id: Mapped[int] = mapped_column("id", primary_key=True)
13
+ id_path: Mapped[list[int]] = mapped_column(
14
+ "id_path", ARRAY(Integer, as_tuple=True, zero_indexes=True)
15
+ )
16
+ depth: Mapped[int] = mapped_column("depth")
@@ -0,0 +1,99 @@
1
+ import time
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+
5
+ from perfact.api.base.model import Base
6
+ from sqlalchemy import (
7
+ Column,
8
+ DateTime,
9
+ Integer,
10
+ MetaData,
11
+ Table,
12
+ delete,
13
+ select,
14
+ text,
15
+ )
16
+ from sqlalchemy.orm import Mapped, Session
17
+
18
+ metadata_obj = MetaData()
19
+
20
+
21
+ class AppTblCleanup(Base):
22
+ tablename: Mapped[str]
23
+ range: Mapped[Optional[str]]
24
+ filter: Mapped[Optional[str]]
25
+
26
+ @dataclass
27
+ class RunResult:
28
+ count: int # number of deleted records
29
+ elapsed: float # time in seconds it took
30
+ limit_reached: bool # if the limit was reached
31
+
32
+ def run(self, session: Session) -> RunResult:
33
+ """
34
+ Execute cleanup for the selected table.
35
+ """
36
+ start = time.monotonic()
37
+ LIMIT = 100_000
38
+ table = Table(
39
+ self.tablename,
40
+ metadata_obj,
41
+ Column(f"{self.tablename}_id", Integer, primary_key=True, key="id"),
42
+ Column(f"{self.tablename}_modtime", DateTime(timezone=True), key="modtime"),
43
+ extend_existing=True,
44
+ )
45
+ ids_select = (
46
+ select(table.c.id)
47
+ .where(table.c.modtime < text("now() - (:ival)::interval"))
48
+ .where(text(self.filter or "true"))
49
+ .params(ival=self.range)
50
+ .order_by(table.c.id)
51
+ .limit(LIMIT)
52
+ )
53
+ # Use connection().execute() (Core layer) instead of session.execute()
54
+ # (ORM layer): return type is CursorResult, which exposes rowcount.
55
+ # Also skips autoflush — intentional, this worker runs in its own
56
+ # session with no pending state.
57
+ result = session.connection().execute(
58
+ delete(table).where(table.c.id.in_(ids_select))
59
+ )
60
+ count = result.rowcount
61
+ elapsed = time.monotonic() - start
62
+ return AppTblCleanup.RunResult(
63
+ count=count,
64
+ limit_reached=(count == LIMIT),
65
+ elapsed=elapsed,
66
+ )
67
+
68
+ @staticmethod
69
+ def run_cleanup_batches(session: Session):
70
+ """
71
+ Clean up data according to given rules in apptblcleanup.
72
+ Commits after each processed batch. Returns a mapping from table name to
73
+ number of entries deleted.
74
+ """
75
+
76
+ result = session.execute(select(AppTblCleanup))
77
+ rows = list(result.scalars().all())
78
+ stats = {
79
+ row.tablename: AppTblCleanup.RunResult(
80
+ count=0,
81
+ elapsed=0.0,
82
+ limit_reached=False,
83
+ )
84
+ for row in rows
85
+ }
86
+ while rows:
87
+ new_rows = []
88
+ for row in rows:
89
+ res = row.run(session)
90
+ tgt = stats[row.tablename]
91
+ tgt.count += res.count
92
+ tgt.elapsed += res.elapsed
93
+ tgt.limit_reached = tgt.limit_reached or res.limit_reached
94
+ if res.limit_reached:
95
+ new_rows.append(row)
96
+
97
+ yield stats
98
+
99
+ rows = new_rows
@@ -0,0 +1,6 @@
1
+ from perfact.api.base.model import Base, Mapped
2
+
3
+
4
+ class AppUser(Base):
5
+ name: Mapped[str]
6
+ password: Mapped[str | None]
@@ -0,0 +1,9 @@
1
+ from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
2
+
3
+ from .appuser import AppUser
4
+
5
+
6
+ class AppUserKey(Base):
7
+ appuser_id: Mapped[int] = mapped_column(ForeignKey(AppUser.id))
8
+ key: Mapped[str]
9
+ appuser: Mapped[AppUser] = relationship()
@@ -0,0 +1,11 @@
1
+ from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
2
+
3
+ from .appuser import AppUser
4
+
5
+
6
+ class AppUserLogin(Base):
7
+ appuser_id: Mapped[int] = mapped_column(ForeignKey(AppUser.id))
8
+ cookie: Mapped[str | None]
9
+ nextcookie: Mapped[str | None]
10
+ done: Mapped[bool] = mapped_column(default=False)
11
+ user: Mapped[AppUser] = relationship()
@@ -0,0 +1,12 @@
1
+ from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
2
+
3
+ from .appperm import AppPerm
4
+ from .appuser import AppUser
5
+
6
+
7
+ class AppUserXPerm(Base):
8
+ appuser_id: Mapped[int] = mapped_column(ForeignKey(AppUser.id))
9
+ appperm_id: Mapped[int] = mapped_column(ForeignKey(AppPerm.id))
10
+ needsgrant: Mapped[bool] = mapped_column(server_default="false")
11
+ user: Mapped[AppUser] = relationship()
12
+ perm: Mapped[AppPerm] = relationship()
@@ -0,0 +1,12 @@
1
+ from perfact.api.base.model import Base, ForeignKey, Mapped, mapped_column, relationship
2
+
3
+ from .appstc import AppStc
4
+ from .appuser import AppUser
5
+
6
+
7
+ class AppUserXStc(Base):
8
+ appuser_id: Mapped[int] = mapped_column(ForeignKey(AppUser.id))
9
+ appstc_id: Mapped[int] = mapped_column(ForeignKey(AppStc.id))
10
+
11
+ user: Mapped[AppUser] = relationship()
12
+ stc: Mapped[AppStc] = relationship()
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class AuthInfo:
7
+ """
8
+ Authentication information for the current user
9
+ """
10
+
11
+ name: str
12
+ roles: list[str]
13
+ appstc: Optional[int]
@@ -0,0 +1,137 @@
1
+ from typing import List, Optional, Sequence, Type
2
+
3
+ from perfact.api.base.model import Base
4
+ from sqlalchemy import (
5
+ ARRAY,
6
+ BigInteger,
7
+ ColumnElement,
8
+ ScalarSelect,
9
+ Select,
10
+ and_,
11
+ cast,
12
+ func,
13
+ join,
14
+ literal,
15
+ or_,
16
+ select,
17
+ )
18
+ from sqlalchemy.dialects.postgresql import array
19
+
20
+ from .appstc import AppStc_Paths
21
+ from .authinfo import AuthInfo
22
+
23
+
24
+ class Selfilterable:
25
+ """
26
+ selfilters is a mechanism which creates visibility rules for a table.
27
+ The rule is defined by a SQL WHERE statement.
28
+ This statement should be added to all queries to the database.
29
+ The selfilter can be retrived by calling the classmethod `get_selfilter`.
30
+ """
31
+
32
+ @classmethod
33
+ def get_selfilter(cls, auth: Optional[AuthInfo]) -> ColumnElement[bool]:
34
+ """
35
+ returns the selfilter as a ColumnElement which can be used in
36
+ a WHERE/filter statement.
37
+
38
+ usage:
39
+ select(MtArt).filter(
40
+ MtArt.get_selfilter(...)
41
+ )
42
+ """
43
+ raise NotImplementedError(f"No selfilter defined for {cls.__name__}")
44
+
45
+ @staticmethod
46
+ def generate_selfilter_stc_query(
47
+ entity: Type[Base],
48
+ stcrefcol: Optional[str] = None,
49
+ stcmxntable: Optional[Type[Base]] = None,
50
+ mxnflags: Sequence[str] = [],
51
+ appstc_ids=None,
52
+ inherit=False,
53
+ ) -> Select:
54
+ """
55
+ this implements
56
+ PerFact.WebApp.db_selfilter_sql_d.methods.appstc_query_iq
57
+ the sqlalchemy way.
58
+
59
+ The stcrefcol-column (e.g. <entity>_appstc_id) is used via AppStc_Paths to
60
+ find object ids linked to the given appstc.
61
+ If `stcmxntable` is set, the mxn-table is used to find
62
+ the object ids linked to the given appstc.
63
+
64
+ mxnflags:
65
+ Optional list of flags that must exist as additional boolean
66
+ columns of the MxN table. If set, only entries with this flag are
67
+ considered.
68
+
69
+ returns:
70
+ filter statement returning all object ids that are
71
+ mapped to the given appstc and allowed to present to the user.
72
+
73
+ usage:
74
+ conditions = [
75
+ MtArt.generate_selfilter_stc_query(appstc_ids=[stc.id]),
76
+ ]
77
+
78
+ """
79
+ if (stcmxntable is None) == (stcrefcol is None):
80
+ raise RuntimeError("Either mxn or refcol need to be supplied")
81
+
82
+ main_table = entity
83
+ tbl = main_table.__tablename__
84
+
85
+ # build up where statement
86
+ conditions: List[ColumnElement[bool]] = [
87
+ AppStc_Paths.id_path.bool_op("&&")(
88
+ cast(array(appstc_ids), ARRAY(BigInteger))
89
+ ),
90
+ ]
91
+
92
+ if inherit:
93
+ subquery: ScalarSelect[bool] = (
94
+ select(
95
+ func.bool_or(
96
+ AppStc_Paths.id_path.op("&&")(func.array(AppStc_Paths.id))
97
+ )
98
+ )
99
+ .filter(AppStc_Paths.id.in_(appstc_ids))
100
+ .scalar_subquery()
101
+ )
102
+
103
+ conditions.append(subquery)
104
+
105
+ if stcmxntable:
106
+ mxn_table = entity.metadata.tables[stcmxntable.__tablename__]
107
+
108
+ column_names = mxn_table.c[f"{tbl}_id"]
109
+
110
+ join_on = [or_(AppStc_Paths.id == mxn_table.c["appstc_id"])]
111
+
112
+ for flag in mxnflags:
113
+ join_on.append(mxn_table.c[flag])
114
+
115
+ j = join(
116
+ mxn_table,
117
+ AppStc_Paths,
118
+ and_(*join_on),
119
+ )
120
+ elif stcrefcol:
121
+ column_names = getattr(main_table, "id")
122
+ j = join(
123
+ tbl,
124
+ AppStc_Paths,
125
+ AppStc_Paths.id == getattr(main_table, stcrefcol),
126
+ )
127
+
128
+ query = select(column_names).select_from(j).where(or_(*conditions))
129
+ return query
130
+
131
+ @staticmethod
132
+ def generate_selfilter_open():
133
+ """
134
+ this implements a open selfilter (everything is accessible)
135
+ the sqlalchemy way.
136
+ """
137
+ return literal(True)
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: perfact-api-app
3
+ Version: 0.6
4
+ Summary: PerFact API - SQLAlchemy models for the app namespace
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: sqlalchemy
13
+ Requires-Dist: perfact-api-base-model
14
+
15
+ # perfact-api-app
16
+
17
+ SQLAlchemy models for the `app` namespace of the PerFact API.
18
+
19
+ ## Models
20
+
21
+ Provides ORM models under `perfact.api.app.model`:
22
+
23
+ - `AppUser` / `AppUserKey` / `AppUserLogin` — user accounts, API keys, login sessions
24
+ - `AppStc` — org areas (Strukturknoten), hierarchical
25
+ - `AppPerm` / `AppPermXGroup` / `AppPermXStc` — permissions and their assignments
26
+ - `AppGroup` / `AppUserXPerm` / `AppUserXStc` — group and user-permission mappings
27
+ - `AppTblCleanup` — table cleanup job model
28
+
29
+ ## Installation
30
+
31
+ ```sh
32
+ pip install perfact-api-app
33
+ ```
34
+
35
+ ## Development
36
+
37
+ ```sh
38
+ pip install -e ../perfact-api-base-model/
39
+ pip install -e .
40
+ tox
41
+ ```
42
+
43
+ ## Maintainers
44
+
45
+ - Viktor Dick <viktor.dick@perfact.de>
@@ -0,0 +1,22 @@
1
+ README.md
2
+ pyproject.toml
3
+ tox.ini
4
+ src/perfact/api/app/model/__init__.py
5
+ src/perfact/api/app/model/appgroup.py
6
+ src/perfact/api/app/model/appperm.py
7
+ src/perfact/api/app/model/apppermxgroup.py
8
+ src/perfact/api/app/model/apppermxstc.py
9
+ src/perfact/api/app/model/appstc.py
10
+ src/perfact/api/app/model/apptblcleanup.py
11
+ src/perfact/api/app/model/appuser.py
12
+ src/perfact/api/app/model/appuserkey.py
13
+ src/perfact/api/app/model/appuserlogin.py
14
+ src/perfact/api/app/model/appuserxperm.py
15
+ src/perfact/api/app/model/appuserxstc.py
16
+ src/perfact/api/app/model/authinfo.py
17
+ src/perfact/api/app/model/selfilterable.py
18
+ src/perfact_api_app.egg-info/PKG-INFO
19
+ src/perfact_api_app.egg-info/SOURCES.txt
20
+ src/perfact_api_app.egg-info/dependency_links.txt
21
+ src/perfact_api_app.egg-info/requires.txt
22
+ src/perfact_api_app.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ sqlalchemy
2
+ perfact-api-base-model
@@ -0,0 +1,31 @@
1
+ [tox]
2
+ envlist = py3
3
+ isolated_build = true
4
+
5
+ [pytest]
6
+
7
+ [testenv]
8
+ passenv = SSH_AUTH_SOCK, PYTHONPATH, HTTP_PROXY, HTTPS_PROXY
9
+ setenv =
10
+ GIT_SSH_VARIANT=ssh
11
+ GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
12
+ LOCALDBMODE=pgctl
13
+
14
+ deps =
15
+ ruff
16
+ pytest
17
+ coverage
18
+ psycopg[binary]
19
+ pytest-postgresql
20
+ pytest-cov
21
+ pytest-typing
22
+ bandit
23
+ mypy
24
+ perfact-api-base-model
25
+
26
+ commands =
27
+ ruff format --check
28
+ ruff check
29
+ bandit --configfile {toxinidir}/../bandit.yml -r src
30
+ mypy src
31
+ # pytest --doctest-modules --cov-branch --cov=src --cov-report=term-missing {posargs:src}