paperus 0.0.8__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.
- paperus-0.0.8/PKG-INFO +57 -0
- paperus-0.0.8/README.md +19 -0
- paperus-0.0.8/pyproject.toml +87 -0
- paperus-0.0.8/pyproject.toml.orig +69 -0
- paperus-0.0.8/src/paperus/__init__.py +5 -0
- paperus-0.0.8/src/paperus/access.py +202 -0
- paperus-0.0.8/src/paperus/api/__init__.py +1 -0
- paperus-0.0.8/src/paperus/api/admin.py +364 -0
- paperus-0.0.8/src/paperus/api/auth.py +297 -0
- paperus-0.0.8/src/paperus/api/avatars.py +60 -0
- paperus-0.0.8/src/paperus/api/backups.py +91 -0
- paperus-0.0.8/src/paperus/api/citations.py +92 -0
- paperus-0.0.8/src/paperus/api/deps.py +77 -0
- paperus-0.0.8/src/paperus/api/duplicates.py +61 -0
- paperus-0.0.8/src/paperus/api/files.py +350 -0
- paperus-0.0.8/src/paperus/api/groups.py +491 -0
- paperus-0.0.8/src/paperus/api/health.py +22 -0
- paperus-0.0.8/src/paperus/api/imports.py +120 -0
- paperus-0.0.8/src/paperus/api/items.py +749 -0
- paperus-0.0.8/src/paperus/api/notes.py +423 -0
- paperus-0.0.8/src/paperus/api/organize.py +649 -0
- paperus-0.0.8/src/paperus/api/search.py +272 -0
- paperus-0.0.8/src/paperus/api/setup.py +120 -0
- paperus-0.0.8/src/paperus/api/tokens.py +101 -0
- paperus-0.0.8/src/paperus/api/trash.py +43 -0
- paperus-0.0.8/src/paperus/api/users.py +217 -0
- paperus-0.0.8/src/paperus/app.py +176 -0
- paperus-0.0.8/src/paperus/auth.py +134 -0
- paperus-0.0.8/src/paperus/avatars.py +44 -0
- paperus-0.0.8/src/paperus/backup.py +496 -0
- paperus-0.0.8/src/paperus/changes.py +104 -0
- paperus-0.0.8/src/paperus/citations.py +337 -0
- paperus-0.0.8/src/paperus/cli.py +275 -0
- paperus-0.0.8/src/paperus/config.py +59 -0
- paperus-0.0.8/src/paperus/db.py +18 -0
- paperus-0.0.8/src/paperus/duplicates.py +183 -0
- paperus-0.0.8/src/paperus/embedding.py +116 -0
- paperus-0.0.8/src/paperus/handover.py +119 -0
- paperus-0.0.8/src/paperus/identifiers.py +118 -0
- paperus-0.0.8/src/paperus/identify.py +243 -0
- paperus-0.0.8/src/paperus/importing/__init__.py +53 -0
- paperus-0.0.8/src/paperus/importing/apply.py +444 -0
- paperus-0.0.8/src/paperus/importing/bibtex.py +303 -0
- paperus-0.0.8/src/paperus/importing/common.py +67 -0
- paperus-0.0.8/src/paperus/importing/csljson.py +55 -0
- paperus-0.0.8/src/paperus/importing/ris.py +157 -0
- paperus-0.0.8/src/paperus/importing/run.py +205 -0
- paperus-0.0.8/src/paperus/importing/zotero.py +441 -0
- paperus-0.0.8/src/paperus/integrity.py +123 -0
- paperus-0.0.8/src/paperus/jobs.py +480 -0
- paperus-0.0.8/src/paperus/library.py +247 -0
- paperus-0.0.8/src/paperus/mcp_server.py +914 -0
- paperus-0.0.8/src/paperus/merge.py +296 -0
- paperus-0.0.8/src/paperus/metadata.py +110 -0
- paperus-0.0.8/src/paperus/metrics.py +228 -0
- paperus-0.0.8/src/paperus/migrate.py +23 -0
- paperus-0.0.8/src/paperus/migrations/env.py +53 -0
- paperus-0.0.8/src/paperus/migrations/procrastinate_3.10.0.sql +599 -0
- paperus-0.0.8/src/paperus/migrations/script.py.mako +20 -0
- paperus-0.0.8/src/paperus/migrations/versions/0001_initial_schema.py +294 -0
- paperus-0.0.8/src/paperus/migrations/versions/0002_add_system_settings.py +32 -0
- paperus-0.0.8/src/paperus/migrations/versions/0003_mark_reference_lists.py +28 -0
- paperus-0.0.8/src/paperus/migrations/versions/0004_add_tags_and_collections.py +115 -0
- paperus-0.0.8/src/paperus/migrations/versions/0005_add_tag_details.py +22 -0
- paperus-0.0.8/src/paperus/migrations/versions/0006_add_groups_and_libraries.py +149 -0
- paperus-0.0.8/src/paperus/migrations/versions/0007_items_in_several_libraries.py +49 -0
- paperus-0.0.8/src/paperus/migrations/versions/0008_one_owner_per_item.py +53 -0
- paperus-0.0.8/src/paperus/migrations/versions/0009_add_seen_hints.py +29 -0
- paperus-0.0.8/src/paperus/migrations/versions/0010_recent_activity.py +44 -0
- paperus-0.0.8/src/paperus/migrations/versions/0011_user_login_and_deactivation.py +26 -0
- paperus-0.0.8/src/paperus/migrations/versions/0012_add_access_tokens.py +45 -0
- paperus-0.0.8/src/paperus/migrations/versions/0013_add_notes.py +150 -0
- paperus-0.0.8/src/paperus/migrations/versions/0014_add_imports.py +47 -0
- paperus-0.0.8/src/paperus/migrations/versions/0015_keep_embedding_model.py +34 -0
- paperus-0.0.8/src/paperus/migrations/versions/0016_add_not_duplicates.py +39 -0
- paperus-0.0.8/src/paperus/migrations/versions/0017_add_favourites_and_pinned_collections.py +63 -0
- paperus-0.0.8/src/paperus/migrations/versions/0018_add_oidc_subject.py +22 -0
- paperus-0.0.8/src/paperus/migrations/versions/0019_add_avatars.py +24 -0
- paperus-0.0.8/src/paperus/ml.py +205 -0
- paperus-0.0.8/src/paperus/models.py +769 -0
- paperus-0.0.8/src/paperus/notes.py +111 -0
- paperus-0.0.8/src/paperus/oidc.py +296 -0
- paperus-0.0.8/src/paperus/openapi.py +11 -0
- paperus-0.0.8/src/paperus/organize.py +158 -0
- paperus-0.0.8/src/paperus/processing.py +161 -0
- paperus-0.0.8/src/paperus/py.typed +0 -0
- paperus-0.0.8/src/paperus/readable.py +337 -0
- paperus-0.0.8/src/paperus/storage.py +235 -0
- paperus-0.0.8/src/paperus/system.py +217 -0
- paperus-0.0.8/src/paperus/text.py +134 -0
- paperus-0.0.8/src/paperus/tokens.py +151 -0
- paperus-0.0.8/src/paperus/trash.py +121 -0
- paperus-0.0.8/src/paperus/usage.py +89 -0
- paperus-0.0.8/src/paperus/web.py +25 -0
paperus-0.0.8/PKG-INFO
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paperus
|
|
3
|
+
Version: 0.0.8
|
|
4
|
+
Summary: Self-hosted research library for groups
|
|
5
|
+
Keywords: research,library,papers,semantic search,self-hosted
|
|
6
|
+
Author: paperus contributors
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Framework :: FastAPI
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Dist: alembic>=1.20.1
|
|
14
|
+
Requires-Dist: argon2-cffi>=25.1.0
|
|
15
|
+
Requires-Dist: boto3>=1.43.124
|
|
16
|
+
Requires-Dist: fastapi>=0.162.0
|
|
17
|
+
Requires-Dist: httpx2>=2.16.0
|
|
18
|
+
Requires-Dist: markdownify>=1.2.3
|
|
19
|
+
Requires-Dist: mcp>=2.3.0,<3
|
|
20
|
+
Requires-Dist: pgvector>=0.5.1
|
|
21
|
+
Requires-Dist: procrastinate>=3.10,<3.11
|
|
22
|
+
Requires-Dist: prometheus-client>=0.26.0
|
|
23
|
+
Requires-Dist: psycopg[binary]>=3.3.8
|
|
24
|
+
Requires-Dist: pybtex>=0.26.1
|
|
25
|
+
Requires-Dist: pydantic-settings>=2.16.2
|
|
26
|
+
Requires-Dist: pylatexenc>=2.11
|
|
27
|
+
Requires-Dist: pypdfium2>=5.14.0
|
|
28
|
+
Requires-Dist: python-multipart>=0.0.33
|
|
29
|
+
Requires-Dist: rdflib>=7.6.0
|
|
30
|
+
Requires-Dist: rispy>=0.10.0
|
|
31
|
+
Requires-Dist: sqlalchemy>=2.1.8
|
|
32
|
+
Requires-Dist: uvicorn[standard]>=0.54.0
|
|
33
|
+
Requires-Python: >=3.13
|
|
34
|
+
Project-URL: Homepage, https://github.com/qcode-uni-a/paperus
|
|
35
|
+
Project-URL: Source, https://github.com/qcode-uni-a/paperus
|
|
36
|
+
Project-URL: Issues, https://github.com/qcode-uni-a/paperus/issues
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# paperus
|
|
40
|
+
|
|
41
|
+
Server of [paperus](https://github.com/qcode-uni-a/paperus), a self-hosted
|
|
42
|
+
research library for groups: papers and notes in one shared library,
|
|
43
|
+
searchable by words and by meaning, with your own AI assistant connected.
|
|
44
|
+
|
|
45
|
+
This package contains the API, the background worker and the `paperus`
|
|
46
|
+
command. Most installations use the container images instead; see the
|
|
47
|
+
[installation guide](https://github.com/qcode-uni-a/paperus#installation).
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pip install paperus
|
|
51
|
+
paperus migrate # needs PAPERUS_DATABASE_URL (Postgres + pgvector)
|
|
52
|
+
paperus user create alice --admin
|
|
53
|
+
paperus serve # API on port 8000
|
|
54
|
+
paperus worker # background jobs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Status: early version (0.0.x) for a pilot; expect breaking changes.
|
paperus-0.0.8/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# paperus
|
|
2
|
+
|
|
3
|
+
Server of [paperus](https://github.com/qcode-uni-a/paperus), a self-hosted
|
|
4
|
+
research library for groups: papers and notes in one shared library,
|
|
5
|
+
searchable by words and by meaning, with your own AI assistant connected.
|
|
6
|
+
|
|
7
|
+
This package contains the API, the background worker and the `paperus`
|
|
8
|
+
command. Most installations use the container images instead; see the
|
|
9
|
+
[installation guide](https://github.com/qcode-uni-a/paperus#installation).
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install paperus
|
|
13
|
+
paperus migrate # needs PAPERUS_DATABASE_URL (Postgres + pgvector)
|
|
14
|
+
paperus user create alice --admin
|
|
15
|
+
paperus serve # API on port 8000
|
|
16
|
+
paperus worker # background jobs
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Status: early version (0.0.x) for a pilot; expect breaking changes.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "paperus"
|
|
3
|
+
version = "0.0.8"
|
|
4
|
+
description = "Self-hosted research library for groups"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
keywords = [
|
|
8
|
+
"research",
|
|
9
|
+
"library",
|
|
10
|
+
"papers",
|
|
11
|
+
"semantic search",
|
|
12
|
+
"self-hosted",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
16
|
+
"Framework :: FastAPI",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.13"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"alembic>=1.20.1",
|
|
24
|
+
"argon2-cffi>=25.1.0",
|
|
25
|
+
"boto3>=1.43.124",
|
|
26
|
+
"fastapi>=0.162.0",
|
|
27
|
+
"httpx2>=2.16.0",
|
|
28
|
+
"markdownify>=1.2.3",
|
|
29
|
+
"mcp>=2.3.0,<3",
|
|
30
|
+
"pgvector>=0.5.1",
|
|
31
|
+
"procrastinate>=3.10,<3.11",
|
|
32
|
+
"prometheus-client>=0.26.0",
|
|
33
|
+
"psycopg[binary]>=3.3.8",
|
|
34
|
+
"pybtex>=0.26.1",
|
|
35
|
+
"pydantic-settings>=2.16.2",
|
|
36
|
+
"pylatexenc>=2.11",
|
|
37
|
+
"pypdfium2>=5.14.0",
|
|
38
|
+
"python-multipart>=0.0.33",
|
|
39
|
+
"rdflib>=7.6.0",
|
|
40
|
+
"rispy>=0.10.0",
|
|
41
|
+
"sqlalchemy>=2.1.8",
|
|
42
|
+
"uvicorn[standard]>=0.54.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[[project.authors]]
|
|
46
|
+
name = "paperus contributors"
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/qcode-uni-a/paperus"
|
|
50
|
+
Source = "https://github.com/qcode-uni-a/paperus"
|
|
51
|
+
Issues = "https://github.com/qcode-uni-a/paperus/issues"
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
paperus = "paperus.cli:main"
|
|
55
|
+
|
|
56
|
+
[build-system]
|
|
57
|
+
requires = ["uv_build>=0.13,<0.14"]
|
|
58
|
+
build-backend = "uv_build"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 88
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = [
|
|
65
|
+
"E",
|
|
66
|
+
"F",
|
|
67
|
+
"W",
|
|
68
|
+
"I",
|
|
69
|
+
"B",
|
|
70
|
+
"UP",
|
|
71
|
+
"SIM",
|
|
72
|
+
"RUF",
|
|
73
|
+
"ANN",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
filterwarnings = ["error"]
|
|
79
|
+
|
|
80
|
+
[dependency-groups]
|
|
81
|
+
dev = [
|
|
82
|
+
"moto[s3]>=5.2.5",
|
|
83
|
+
"pytest>=9.2.0",
|
|
84
|
+
"ruff>=0.17.3",
|
|
85
|
+
"ty>=0.0.97",
|
|
86
|
+
"types-boto3[s3]>=1.43.123",
|
|
87
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "paperus"
|
|
3
|
+
version = "0.0.8"
|
|
4
|
+
description = "Self-hosted research library for groups"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
authors = [{ name = "paperus contributors" }]
|
|
8
|
+
keywords = ["research", "library", "papers", "semantic search", "self-hosted"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
11
|
+
"Framework :: FastAPI",
|
|
12
|
+
"Intended Audience :: Science/Research",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Topic :: Scientific/Engineering",
|
|
15
|
+
]
|
|
16
|
+
requires-python = ">=3.13"
|
|
17
|
+
dependencies = [
|
|
18
|
+
"alembic>=1.20.1",
|
|
19
|
+
"argon2-cffi>=25.1.0",
|
|
20
|
+
"boto3>=1.43.124",
|
|
21
|
+
"fastapi>=0.162.0",
|
|
22
|
+
"httpx2>=2.16.0",
|
|
23
|
+
"markdownify>=1.2.3",
|
|
24
|
+
"mcp>=2.3.0,<3",
|
|
25
|
+
"pgvector>=0.5.1",
|
|
26
|
+
"procrastinate>=3.10,<3.11", # schema frozen in src/paperus/migrations/
|
|
27
|
+
"prometheus-client>=0.26.0",
|
|
28
|
+
"psycopg[binary]>=3.3.8",
|
|
29
|
+
"pybtex>=0.26.1",
|
|
30
|
+
"pydantic-settings>=2.16.2",
|
|
31
|
+
"pylatexenc>=2.11",
|
|
32
|
+
"pypdfium2>=5.14.0",
|
|
33
|
+
"python-multipart>=0.0.33",
|
|
34
|
+
"rdflib>=7.6.0",
|
|
35
|
+
"rispy>=0.10.0",
|
|
36
|
+
"sqlalchemy>=2.1.8",
|
|
37
|
+
"uvicorn[standard]>=0.54.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/qcode-uni-a/paperus"
|
|
42
|
+
Source = "https://github.com/qcode-uni-a/paperus"
|
|
43
|
+
Issues = "https://github.com/qcode-uni-a/paperus/issues"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
paperus = "paperus.cli:main"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["uv_build>=0.13,<0.14"]
|
|
50
|
+
build-backend = "uv_build"
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 88
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "ANN"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
filterwarnings = ["error"]
|
|
61
|
+
|
|
62
|
+
[dependency-groups]
|
|
63
|
+
dev = [
|
|
64
|
+
"moto[s3]>=5.2.5",
|
|
65
|
+
"pytest>=9.2.0",
|
|
66
|
+
"ruff>=0.17.3",
|
|
67
|
+
"ty>=0.0.97",
|
|
68
|
+
"types-boto3[s3]>=1.43.123",
|
|
69
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Who may read and change which library.
|
|
2
|
+
|
|
3
|
+
A person reads their own library and those of the groups they belong to,
|
|
4
|
+
including every group inside those (membership is inherited downwards, and a
|
|
5
|
+
subgroup can only raise a role). Site administrators manage users and groups
|
|
6
|
+
but read no library because of it. All of this is worked out once per
|
|
7
|
+
request (``Access.of``) from the group tree, which is small, and the person's
|
|
8
|
+
memberships; lists and searches then filter by library ID in SQL.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
from sqlalchemy import ColumnElement, select
|
|
15
|
+
from sqlalchemy.orm import Session
|
|
16
|
+
|
|
17
|
+
from paperus.models import (
|
|
18
|
+
ROLE_RANK,
|
|
19
|
+
Group,
|
|
20
|
+
Item,
|
|
21
|
+
Library,
|
|
22
|
+
Membership,
|
|
23
|
+
Note,
|
|
24
|
+
Role,
|
|
25
|
+
User,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Forbidden(Exception):
|
|
30
|
+
"""The person may see this but not change it (HTTP 403)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NotVisible(Exception):
|
|
34
|
+
"""The person may not see this; answered like a missing object (404)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Access:
|
|
39
|
+
user: User
|
|
40
|
+
own_library: uuid.UUID
|
|
41
|
+
roles: dict[uuid.UUID, Role] = field(default_factory=dict)
|
|
42
|
+
"""Effective role per group (direct or inherited)."""
|
|
43
|
+
group_libraries: dict[uuid.UUID, uuid.UUID] = field(default_factory=dict)
|
|
44
|
+
"""Library of each group the person belongs to."""
|
|
45
|
+
own_allowed: bool = True
|
|
46
|
+
"""False for an access token limited to some groups' libraries."""
|
|
47
|
+
site_admin: bool = False
|
|
48
|
+
"""A site administrator acting in person (never through a token, which
|
|
49
|
+
gets no administration)."""
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def of(
|
|
53
|
+
cls,
|
|
54
|
+
db: Session,
|
|
55
|
+
user: User,
|
|
56
|
+
only: list[uuid.UUID] | None = None,
|
|
57
|
+
*,
|
|
58
|
+
token: bool = False,
|
|
59
|
+
) -> "Access":
|
|
60
|
+
"""What ``user`` may do; with ``only`` (an access token's owner limit),
|
|
61
|
+
nothing outside these libraries; ``token``: through an access token."""
|
|
62
|
+
own = db.scalar(select(Library.id).where(Library.user_id == user.id))
|
|
63
|
+
if own is None: # users from before libraries existed get one now
|
|
64
|
+
library = Library(user_id=user.id)
|
|
65
|
+
db.add(library)
|
|
66
|
+
db.flush()
|
|
67
|
+
own = library.id
|
|
68
|
+
parents: dict[uuid.UUID, uuid.UUID | None] = {
|
|
69
|
+
group_id: parent
|
|
70
|
+
for group_id, parent in db.execute(select(Group.id, Group.parent_id))
|
|
71
|
+
}
|
|
72
|
+
direct = {
|
|
73
|
+
group_id: Role(role)
|
|
74
|
+
for group_id, role in db.execute(
|
|
75
|
+
select(Membership.group_id, Membership.role).where(
|
|
76
|
+
Membership.user_id == user.id
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
roles: dict[uuid.UUID, Role] = {}
|
|
81
|
+
for group_id in parents:
|
|
82
|
+
# The highest role on the way up to the top.
|
|
83
|
+
best: Role | None = None
|
|
84
|
+
current: uuid.UUID | None = group_id
|
|
85
|
+
while current is not None:
|
|
86
|
+
role = direct.get(current)
|
|
87
|
+
if role and (best is None or ROLE_RANK[role] > ROLE_RANK[best]):
|
|
88
|
+
best = role
|
|
89
|
+
current = parents.get(current)
|
|
90
|
+
if best is not None:
|
|
91
|
+
roles[group_id] = best
|
|
92
|
+
libraries = {
|
|
93
|
+
group_id: library_id
|
|
94
|
+
for group_id, library_id in db.execute(
|
|
95
|
+
select(Library.group_id, Library.id).where(Library.group_id.in_(roles))
|
|
96
|
+
)
|
|
97
|
+
if group_id is not None
|
|
98
|
+
}
|
|
99
|
+
site_admin = user.is_admin and not token
|
|
100
|
+
if only is None:
|
|
101
|
+
return cls(user, own, roles, libraries, site_admin=site_admin)
|
|
102
|
+
allowed = set(only)
|
|
103
|
+
libraries = {g: lib for g, lib in libraries.items() if lib in allowed}
|
|
104
|
+
roles = {g: r for g, r in roles.items() if g in libraries}
|
|
105
|
+
return cls(
|
|
106
|
+
user,
|
|
107
|
+
own,
|
|
108
|
+
roles,
|
|
109
|
+
libraries,
|
|
110
|
+
own_allowed=own in allowed,
|
|
111
|
+
site_admin=site_admin,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def readable(self) -> list[uuid.UUID]:
|
|
116
|
+
"""Libraries this person may read: their own and their groups'."""
|
|
117
|
+
own = [self.own_library] if self.own_allowed else []
|
|
118
|
+
return [*own, *self.group_libraries.values()]
|
|
119
|
+
|
|
120
|
+
def role_in_library(self, library_id: uuid.UUID) -> Role | None:
|
|
121
|
+
for group_id, group_library in self.group_libraries.items():
|
|
122
|
+
if group_library == library_id:
|
|
123
|
+
return self.roles[group_id]
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def can_read(self, library_id: uuid.UUID) -> bool:
|
|
127
|
+
"""The person's own library (unless a token is limited to others) or
|
|
128
|
+
one of their groups' (only those a limited token allows)."""
|
|
129
|
+
if library_id == self.own_library:
|
|
130
|
+
return self.own_allowed
|
|
131
|
+
return library_id in self.group_libraries.values()
|
|
132
|
+
|
|
133
|
+
def can_write(self, library_id: uuid.UUID) -> bool:
|
|
134
|
+
"""Add, change and delete items, tags and collections."""
|
|
135
|
+
if library_id == self.own_library:
|
|
136
|
+
return self.own_allowed
|
|
137
|
+
role = self.role_in_library(library_id)
|
|
138
|
+
return role is not None and ROLE_RANK[role] >= ROLE_RANK[Role.MEMBER]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def writable(self) -> list[uuid.UUID]:
|
|
142
|
+
return [library for library in self.readable if self.can_write(library)]
|
|
143
|
+
|
|
144
|
+
def is_group_admin(self, group_id: uuid.UUID) -> bool:
|
|
145
|
+
return self.roles.get(group_id) == Role.ADMIN
|
|
146
|
+
|
|
147
|
+
def require_write(self, library_id: uuid.UUID) -> None:
|
|
148
|
+
if not self.can_write(library_id):
|
|
149
|
+
raise Forbidden("readers cannot change this library")
|
|
150
|
+
|
|
151
|
+
# Items have one owner (``Item.library_id``): a person or a group.
|
|
152
|
+
|
|
153
|
+
def can_read_item(self, item: Item) -> bool:
|
|
154
|
+
return self.can_read(item.library_id)
|
|
155
|
+
|
|
156
|
+
def can_write_item(self, item: Item) -> bool:
|
|
157
|
+
return self.can_write(item.library_id)
|
|
158
|
+
|
|
159
|
+
def require_write_item(self, item: Item) -> None:
|
|
160
|
+
if not self.can_write_item(item):
|
|
161
|
+
raise Forbidden("readers cannot change this item")
|
|
162
|
+
|
|
163
|
+
def item_filter(
|
|
164
|
+
self, libraries: list[uuid.UUID] | None = None
|
|
165
|
+
) -> ColumnElement[bool]:
|
|
166
|
+
"""Items owned by these (by default: everything the person reads)."""
|
|
167
|
+
return Item.library_id.in_(self.readable if libraries is None else libraries)
|
|
168
|
+
|
|
169
|
+
def may_tag(self, library_id: uuid.UUID, item: Item) -> bool:
|
|
170
|
+
"""Whether the person may put a tag of ``library_id`` on (or take it
|
|
171
|
+
off) the item: their private tags on any item they see; a group's
|
|
172
|
+
tags (they read) on any item they may change, e.g. their own papers
|
|
173
|
+
with the group's vocabulary."""
|
|
174
|
+
if library_id == self.own_library:
|
|
175
|
+
return self.own_allowed and self.can_read_item(item)
|
|
176
|
+
return self.can_read(library_id) and self.can_write_item(item)
|
|
177
|
+
|
|
178
|
+
def may_hold(self, library_id: uuid.UUID, item: Item | Note) -> bool:
|
|
179
|
+
"""Whether a collection of ``library_id`` may hold the item (or note):
|
|
180
|
+
a group's only the group's own, a person's private ones anything the
|
|
181
|
+
person can read."""
|
|
182
|
+
if library_id == self.own_library:
|
|
183
|
+
return self.own_allowed and self.can_read(item.library_id)
|
|
184
|
+
return library_id == item.library_id
|
|
185
|
+
|
|
186
|
+
def target_library(self, requested: uuid.UUID | None) -> uuid.UUID:
|
|
187
|
+
"""Where something new goes: the library asked for (if the person may
|
|
188
|
+
add to it), else their default library, else their own."""
|
|
189
|
+
if requested is not None:
|
|
190
|
+
if not self.can_read(requested):
|
|
191
|
+
raise NotVisible("library not found")
|
|
192
|
+
self.require_write(requested)
|
|
193
|
+
return requested
|
|
194
|
+
default = self.user.default_library_id
|
|
195
|
+
if default is not None and self.can_write(default):
|
|
196
|
+
return default
|
|
197
|
+
if self.own_allowed:
|
|
198
|
+
return self.own_library
|
|
199
|
+
writable = self.writable
|
|
200
|
+
if not writable:
|
|
201
|
+
raise Forbidden("this token may not add anything")
|
|
202
|
+
return writable[0]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP API routes."""
|