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.
Files changed (94) hide show
  1. paperus-0.0.8/PKG-INFO +57 -0
  2. paperus-0.0.8/README.md +19 -0
  3. paperus-0.0.8/pyproject.toml +87 -0
  4. paperus-0.0.8/pyproject.toml.orig +69 -0
  5. paperus-0.0.8/src/paperus/__init__.py +5 -0
  6. paperus-0.0.8/src/paperus/access.py +202 -0
  7. paperus-0.0.8/src/paperus/api/__init__.py +1 -0
  8. paperus-0.0.8/src/paperus/api/admin.py +364 -0
  9. paperus-0.0.8/src/paperus/api/auth.py +297 -0
  10. paperus-0.0.8/src/paperus/api/avatars.py +60 -0
  11. paperus-0.0.8/src/paperus/api/backups.py +91 -0
  12. paperus-0.0.8/src/paperus/api/citations.py +92 -0
  13. paperus-0.0.8/src/paperus/api/deps.py +77 -0
  14. paperus-0.0.8/src/paperus/api/duplicates.py +61 -0
  15. paperus-0.0.8/src/paperus/api/files.py +350 -0
  16. paperus-0.0.8/src/paperus/api/groups.py +491 -0
  17. paperus-0.0.8/src/paperus/api/health.py +22 -0
  18. paperus-0.0.8/src/paperus/api/imports.py +120 -0
  19. paperus-0.0.8/src/paperus/api/items.py +749 -0
  20. paperus-0.0.8/src/paperus/api/notes.py +423 -0
  21. paperus-0.0.8/src/paperus/api/organize.py +649 -0
  22. paperus-0.0.8/src/paperus/api/search.py +272 -0
  23. paperus-0.0.8/src/paperus/api/setup.py +120 -0
  24. paperus-0.0.8/src/paperus/api/tokens.py +101 -0
  25. paperus-0.0.8/src/paperus/api/trash.py +43 -0
  26. paperus-0.0.8/src/paperus/api/users.py +217 -0
  27. paperus-0.0.8/src/paperus/app.py +176 -0
  28. paperus-0.0.8/src/paperus/auth.py +134 -0
  29. paperus-0.0.8/src/paperus/avatars.py +44 -0
  30. paperus-0.0.8/src/paperus/backup.py +496 -0
  31. paperus-0.0.8/src/paperus/changes.py +104 -0
  32. paperus-0.0.8/src/paperus/citations.py +337 -0
  33. paperus-0.0.8/src/paperus/cli.py +275 -0
  34. paperus-0.0.8/src/paperus/config.py +59 -0
  35. paperus-0.0.8/src/paperus/db.py +18 -0
  36. paperus-0.0.8/src/paperus/duplicates.py +183 -0
  37. paperus-0.0.8/src/paperus/embedding.py +116 -0
  38. paperus-0.0.8/src/paperus/handover.py +119 -0
  39. paperus-0.0.8/src/paperus/identifiers.py +118 -0
  40. paperus-0.0.8/src/paperus/identify.py +243 -0
  41. paperus-0.0.8/src/paperus/importing/__init__.py +53 -0
  42. paperus-0.0.8/src/paperus/importing/apply.py +444 -0
  43. paperus-0.0.8/src/paperus/importing/bibtex.py +303 -0
  44. paperus-0.0.8/src/paperus/importing/common.py +67 -0
  45. paperus-0.0.8/src/paperus/importing/csljson.py +55 -0
  46. paperus-0.0.8/src/paperus/importing/ris.py +157 -0
  47. paperus-0.0.8/src/paperus/importing/run.py +205 -0
  48. paperus-0.0.8/src/paperus/importing/zotero.py +441 -0
  49. paperus-0.0.8/src/paperus/integrity.py +123 -0
  50. paperus-0.0.8/src/paperus/jobs.py +480 -0
  51. paperus-0.0.8/src/paperus/library.py +247 -0
  52. paperus-0.0.8/src/paperus/mcp_server.py +914 -0
  53. paperus-0.0.8/src/paperus/merge.py +296 -0
  54. paperus-0.0.8/src/paperus/metadata.py +110 -0
  55. paperus-0.0.8/src/paperus/metrics.py +228 -0
  56. paperus-0.0.8/src/paperus/migrate.py +23 -0
  57. paperus-0.0.8/src/paperus/migrations/env.py +53 -0
  58. paperus-0.0.8/src/paperus/migrations/procrastinate_3.10.0.sql +599 -0
  59. paperus-0.0.8/src/paperus/migrations/script.py.mako +20 -0
  60. paperus-0.0.8/src/paperus/migrations/versions/0001_initial_schema.py +294 -0
  61. paperus-0.0.8/src/paperus/migrations/versions/0002_add_system_settings.py +32 -0
  62. paperus-0.0.8/src/paperus/migrations/versions/0003_mark_reference_lists.py +28 -0
  63. paperus-0.0.8/src/paperus/migrations/versions/0004_add_tags_and_collections.py +115 -0
  64. paperus-0.0.8/src/paperus/migrations/versions/0005_add_tag_details.py +22 -0
  65. paperus-0.0.8/src/paperus/migrations/versions/0006_add_groups_and_libraries.py +149 -0
  66. paperus-0.0.8/src/paperus/migrations/versions/0007_items_in_several_libraries.py +49 -0
  67. paperus-0.0.8/src/paperus/migrations/versions/0008_one_owner_per_item.py +53 -0
  68. paperus-0.0.8/src/paperus/migrations/versions/0009_add_seen_hints.py +29 -0
  69. paperus-0.0.8/src/paperus/migrations/versions/0010_recent_activity.py +44 -0
  70. paperus-0.0.8/src/paperus/migrations/versions/0011_user_login_and_deactivation.py +26 -0
  71. paperus-0.0.8/src/paperus/migrations/versions/0012_add_access_tokens.py +45 -0
  72. paperus-0.0.8/src/paperus/migrations/versions/0013_add_notes.py +150 -0
  73. paperus-0.0.8/src/paperus/migrations/versions/0014_add_imports.py +47 -0
  74. paperus-0.0.8/src/paperus/migrations/versions/0015_keep_embedding_model.py +34 -0
  75. paperus-0.0.8/src/paperus/migrations/versions/0016_add_not_duplicates.py +39 -0
  76. paperus-0.0.8/src/paperus/migrations/versions/0017_add_favourites_and_pinned_collections.py +63 -0
  77. paperus-0.0.8/src/paperus/migrations/versions/0018_add_oidc_subject.py +22 -0
  78. paperus-0.0.8/src/paperus/migrations/versions/0019_add_avatars.py +24 -0
  79. paperus-0.0.8/src/paperus/ml.py +205 -0
  80. paperus-0.0.8/src/paperus/models.py +769 -0
  81. paperus-0.0.8/src/paperus/notes.py +111 -0
  82. paperus-0.0.8/src/paperus/oidc.py +296 -0
  83. paperus-0.0.8/src/paperus/openapi.py +11 -0
  84. paperus-0.0.8/src/paperus/organize.py +158 -0
  85. paperus-0.0.8/src/paperus/processing.py +161 -0
  86. paperus-0.0.8/src/paperus/py.typed +0 -0
  87. paperus-0.0.8/src/paperus/readable.py +337 -0
  88. paperus-0.0.8/src/paperus/storage.py +235 -0
  89. paperus-0.0.8/src/paperus/system.py +217 -0
  90. paperus-0.0.8/src/paperus/text.py +134 -0
  91. paperus-0.0.8/src/paperus/tokens.py +151 -0
  92. paperus-0.0.8/src/paperus/trash.py +121 -0
  93. paperus-0.0.8/src/paperus/usage.py +89 -0
  94. 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.
@@ -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,5 @@
1
+ """paperus: a self-hosted research library for groups."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("paperus")
@@ -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."""