annotheca 0.1.0__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 (38) hide show
  1. annotheca-0.1.0/.github/workflows/tests.yml +19 -0
  2. annotheca-0.1.0/.gitignore +6 -0
  3. annotheca-0.1.0/LICENSE +21 -0
  4. annotheca-0.1.0/PKG-INFO +203 -0
  5. annotheca-0.1.0/README.md +181 -0
  6. annotheca-0.1.0/docs/library-dark.png +0 -0
  7. annotheca-0.1.0/docs/library.png +0 -0
  8. annotheca-0.1.0/docs/reader.png +0 -0
  9. annotheca-0.1.0/pyproject.toml +38 -0
  10. annotheca-0.1.0/src/annotheca/__init__.py +3 -0
  11. annotheca-0.1.0/src/annotheca/__main__.py +5 -0
  12. annotheca-0.1.0/src/annotheca/api.py +194 -0
  13. annotheca-0.1.0/src/annotheca/bibtex.py +193 -0
  14. annotheca-0.1.0/src/annotheca/bundle.py +152 -0
  15. annotheca-0.1.0/src/annotheca/cli.py +122 -0
  16. annotheca-0.1.0/src/annotheca/library.py +409 -0
  17. annotheca-0.1.0/src/annotheca/mcp_server.py +246 -0
  18. annotheca-0.1.0/src/annotheca/models.py +23 -0
  19. annotheca-0.1.0/src/annotheca/sources.py +190 -0
  20. annotheca-0.1.0/src/annotheca/static/app.css +1123 -0
  21. annotheca-0.1.0/src/annotheca/static/favicon.svg +8 -0
  22. annotheca-0.1.0/src/annotheca/static/index.html +39 -0
  23. annotheca-0.1.0/src/annotheca/static/js/api.js +58 -0
  24. annotheca-0.1.0/src/annotheca/static/js/i18n.js +226 -0
  25. annotheca-0.1.0/src/annotheca/static/js/main.js +563 -0
  26. annotheca-0.1.0/src/annotheca/static/js/storage.js +30 -0
  27. annotheca-0.1.0/src/annotheca/static/js/ui.js +163 -0
  28. annotheca-0.1.0/src/annotheca/static/js/viewer.js +591 -0
  29. annotheca-0.1.0/src/annotheca/static/vendor/pdfjs/LICENSE +177 -0
  30. annotheca-0.1.0/src/annotheca/static/vendor/pdfjs/pdf.min.mjs +28 -0
  31. annotheca-0.1.0/src/annotheca/static/vendor/pdfjs/pdf.worker.min.mjs +28 -0
  32. annotheca-0.1.0/tests/test_api.py +104 -0
  33. annotheca-0.1.0/tests/test_bibtex.py +76 -0
  34. annotheca-0.1.0/tests/test_bundle.py +87 -0
  35. annotheca-0.1.0/tests/test_library.py +132 -0
  36. annotheca-0.1.0/tests/test_mcp_server.py +110 -0
  37. annotheca-0.1.0/tests/test_sources.py +56 -0
  38. annotheca-0.1.0/uv.lock +536 -0
@@ -0,0 +1,19 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.10", "3.13"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python-version }}
18
+ - run: pip install -e '.[test]'
19
+ - run: pytest
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ dist/
6
+ build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Melissa Colin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.5
2
+ Name: annotheca
3
+ Version: 0.1.0
4
+ Summary: A local, bilingual reference manager for research projects: BibTeX, PDFs, highlights and sticky notes.
5
+ Project-URL: Homepage, https://github.com/melissa-colin/annotheca
6
+ Author: Melissa Colin
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: annotation,bibliography,bibtex,pdf,reference-manager,research
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: fastapi>=0.110
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: mcp<3,>=2
18
+ Requires-Dist: uvicorn>=0.29
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/src/annotheca/static/favicon.svg" width="72" alt="">
25
+ </p>
26
+
27
+ <h1 align="center">Annotheca</h1>
28
+
29
+ <p align="center">
30
+ <b>A calm, local home for the bibliography of your research projects.</b><br>
31
+ Paste a reference, get the BibTeX and the PDF, highlight, take notes, export one clean <code>.bib</code>.<br>
32
+ Let Claude and other AI assistants file the papers they find, with verified metadata.
33
+ </p>
34
+
35
+ <p align="center">
36
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library.png" alt="The library view of Annotheca" width="900">
37
+ </p>
38
+
39
+ ## Why
40
+
41
+ Research projects live in stages (state of the art, method, experiments,
42
+ writing) and the same paper often matters in several of them, for different
43
+ reasons. Annotheca organises references the way the project is organised:
44
+
45
+ - **Projects and stage folders.** Every project starts with a `SoA` (state of
46
+ the art) folder; add as many folders as your project has stages.
47
+ - **Paste anything.** BibTeX (one or many entries), DOIs, arXiv IDs or URLs,
48
+ or a formatted citation copied from a paper (APA, IEEE, ...). Annotheca
49
+ resolves it into clean BibTeX through doi.org, arXiv and Crossref.
50
+ - **PDFs found for you.** Open-access PDFs are downloaded automatically from
51
+ arXiv, Semantic Scholar and OpenAlex, following repository landing pages to
52
+ their PDF. Otherwise, drop the PDF on the paper.
53
+ - **Highlights, sticky notes and general notes.** Highlight passages in five
54
+ colours, pin draggable post-its on the page, and keep general notes on each
55
+ paper, next to it in the list or beside the PDF.
56
+ - **One paper, several folders, separate notes.** A paper filed in two folders
57
+ is stored once, but its highlights and notes are specific to each folder:
58
+ what you underline for the state of the art is not what you underline for
59
+ the method.
60
+ - **Share a project.** *Share* downloads the project, with its folders,
61
+ notes, highlights and PDFs, as one `.annotheca` file; whoever receives it
62
+ opens it with the import button of the sidebar.
63
+ - **Works with AI assistants.** Claude (or any MCP client) can file the papers
64
+ it finds into your projects, add notes, read yours and get the `.bib`. See
65
+ [AI assistants](#ai-assistants).
66
+ - **One clean `.bib`.** Export the whole project in one click; papers filed in
67
+ several folders appear only once, with stable, unique citation keys.
68
+ - **English and French**, light and dark themes, everything stored locally.
69
+
70
+ <p align="center">
71
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/reader.png" alt="Reading a paper with highlights and a sticky note" width="900">
72
+ </p>
73
+
74
+ ## Install
75
+
76
+ Annotheca needs Python 3.10 or newer. Install it as a command with
77
+ [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
78
+
79
+ ```sh
80
+ uv tool install annotheca
81
+ # or
82
+ pipx install annotheca
83
+ ```
84
+
85
+ Then, to let Claude use it (optional, see [AI assistants](#ai-assistants)):
86
+
87
+ ```sh
88
+ annotheca connect
89
+ ```
90
+
91
+ Update later with `uv tool upgrade annotheca` (or `pipx upgrade annotheca`).
92
+
93
+ ## Use
94
+
95
+ ```sh
96
+ annotheca
97
+ ```
98
+
99
+ The interface opens in your browser at <http://127.0.0.1:8737>. Running the
100
+ command again while Annotheca is running simply reopens it.
101
+
102
+ | Option or command | Effect |
103
+ | --- | --- |
104
+ | `--port PORT` | Serve on another local port (default `8737`). |
105
+ | `--data-dir DIR` | Keep the library somewhere else. |
106
+ | `--no-browser` | Do not open the browser. |
107
+ | `connect` | Make Annotheca available to Claude (see below). |
108
+ | `mcp` | Serve the tools for AI assistants over stdio (used by `connect`). |
109
+
110
+ Tips:
111
+
112
+ - `Ctrl+Enter` adds what you pasted.
113
+ - Click a citation key to copy `\cite{key}`.
114
+ - Drop a PDF file on a paper to attach it.
115
+ - In the reader, select text to highlight it; switch to *Sticky note* and
116
+ click on the page to pin a note; click a highlight to recolour or delete it;
117
+ `Esc` goes back.
118
+
119
+ ## AI assistants
120
+
121
+ When an assistant writes references itself, it can get a title, an author or a
122
+ year wrong. With Annotheca, the assistant only says *which* paper it means (a
123
+ DOI, an arXiv ID, a citation); Annotheca fetches the metadata from doi.org,
124
+ arXiv and Crossref, files the paper with its PDF, and answers with the exact
125
+ citation key, title and authors to use. Every paper an assistant adds is marked
126
+ with its name, so you can always see, and filter, what it relied on.
127
+
128
+ `annotheca connect` registers Annotheca as an
129
+ [MCP](https://modelcontextprotocol.io) server for **Claude Code** (in every
130
+ project) and **Claude Desktop** when they are installed, and prints the
131
+ configuration to paste into other MCP clients (Cursor, Codex, Gemini CLI...).
132
+ The assistant then just needs to be asked:
133
+
134
+ > Find recent papers on contact-implicit trajectory optimisation and add the
135
+ > three most relevant to the SoA of my PhysProx project, with a note on each.
136
+
137
+ > What did I highlight in posa2013direct? Write the related-work paragraph
138
+ > citing the papers of the Method folder.
139
+
140
+ | Tool | What the assistant can do |
141
+ | --- | --- |
142
+ | `list_projects` | See the projects and their folders. |
143
+ | `create_project`, `create_folder` | Create them. |
144
+ | `add_references` | File papers from DOIs, arXiv IDs, BibTeX or citations, with a note; get back verified metadata. |
145
+ | `list_papers` | List the papers of a project with your notes. |
146
+ | `read_paper` | Read a paper's BibTeX, your notes, highlights and sticky notes, and the path of its PDF. |
147
+ | `add_note` | Add a signed paragraph to a paper's notes. |
148
+ | `get_bibtex` | Get the project's `.bib`. |
149
+
150
+ Nothing else is needed: the server explains itself to the assistant. Papers it
151
+ adds appear in the interface as soon as you come back to the window.
152
+
153
+ ## Your data
154
+
155
+ Everything stays on your machine, in `~/.local/share/annotheca` (or
156
+ `$XDG_DATA_HOME/annotheca`, or `$ANNOTHECA_HOME`):
157
+
158
+ ```
159
+ library.db SQLite database: projects, folders, BibTeX, notes, annotations
160
+ pdfs/ one PDF per paper
161
+ ```
162
+
163
+ Back it up by copying that directory. The only network requests are the
164
+ lookups of references and PDFs, sent to doi.org, arXiv, Crossref, Semantic
165
+ Scholar, OpenAlex and the repositories they point to.
166
+
167
+ <p align="center">
168
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library-dark.png" alt="Dark theme" width="700">
169
+ </p>
170
+
171
+ ## Development
172
+
173
+ ```sh
174
+ git clone https://github.com/melissa-colin/annotheca
175
+ cd annotheca
176
+ uv venv && uv pip install -e '.[test]'
177
+ .venv/bin/pytest
178
+ .venv/bin/annotheca --data-dir /tmp/annotheca-dev
179
+ ```
180
+
181
+ The code follows the Google Python and JavaScript style guides. The backend is
182
+ a small [FastAPI](https://fastapi.tiangolo.com/) app over SQLite
183
+ (`src/annotheca`); the interface is dependency-free JavaScript modules with
184
+ [pdf.js](https://mozilla.github.io/pdf.js/) vendored, so there is no build
185
+ step.
186
+
187
+ ```
188
+ src/annotheca/
189
+ cli.py command line entry point
190
+ api.py HTTP API and static files
191
+ library.py storage: projects, folders, papers, placements, annotations
192
+ bundle.py .annotheca files to share a project
193
+ mcp_server.py tools for AI assistants (Model Context Protocol)
194
+ models.py validated shapes shared by the API and the bundles
195
+ bibtex.py BibTeX parsing, formatting, citation keys, identifiers
196
+ sources.py doi.org, arXiv, Crossref, Semantic Scholar, OpenAlex lookups
197
+ static/ web interface
198
+ ```
199
+
200
+ ## License
201
+
202
+ MIT. pdf.js is distributed under the Apache License 2.0
203
+ (`src/annotheca/static/vendor/pdfjs/LICENSE`).
@@ -0,0 +1,181 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/src/annotheca/static/favicon.svg" width="72" alt="">
3
+ </p>
4
+
5
+ <h1 align="center">Annotheca</h1>
6
+
7
+ <p align="center">
8
+ <b>A calm, local home for the bibliography of your research projects.</b><br>
9
+ Paste a reference, get the BibTeX and the PDF, highlight, take notes, export one clean <code>.bib</code>.<br>
10
+ Let Claude and other AI assistants file the papers they find, with verified metadata.
11
+ </p>
12
+
13
+ <p align="center">
14
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library.png" alt="The library view of Annotheca" width="900">
15
+ </p>
16
+
17
+ ## Why
18
+
19
+ Research projects live in stages (state of the art, method, experiments,
20
+ writing) and the same paper often matters in several of them, for different
21
+ reasons. Annotheca organises references the way the project is organised:
22
+
23
+ - **Projects and stage folders.** Every project starts with a `SoA` (state of
24
+ the art) folder; add as many folders as your project has stages.
25
+ - **Paste anything.** BibTeX (one or many entries), DOIs, arXiv IDs or URLs,
26
+ or a formatted citation copied from a paper (APA, IEEE, ...). Annotheca
27
+ resolves it into clean BibTeX through doi.org, arXiv and Crossref.
28
+ - **PDFs found for you.** Open-access PDFs are downloaded automatically from
29
+ arXiv, Semantic Scholar and OpenAlex, following repository landing pages to
30
+ their PDF. Otherwise, drop the PDF on the paper.
31
+ - **Highlights, sticky notes and general notes.** Highlight passages in five
32
+ colours, pin draggable post-its on the page, and keep general notes on each
33
+ paper, next to it in the list or beside the PDF.
34
+ - **One paper, several folders, separate notes.** A paper filed in two folders
35
+ is stored once, but its highlights and notes are specific to each folder:
36
+ what you underline for the state of the art is not what you underline for
37
+ the method.
38
+ - **Share a project.** *Share* downloads the project, with its folders,
39
+ notes, highlights and PDFs, as one `.annotheca` file; whoever receives it
40
+ opens it with the import button of the sidebar.
41
+ - **Works with AI assistants.** Claude (or any MCP client) can file the papers
42
+ it finds into your projects, add notes, read yours and get the `.bib`. See
43
+ [AI assistants](#ai-assistants).
44
+ - **One clean `.bib`.** Export the whole project in one click; papers filed in
45
+ several folders appear only once, with stable, unique citation keys.
46
+ - **English and French**, light and dark themes, everything stored locally.
47
+
48
+ <p align="center">
49
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/reader.png" alt="Reading a paper with highlights and a sticky note" width="900">
50
+ </p>
51
+
52
+ ## Install
53
+
54
+ Annotheca needs Python 3.10 or newer. Install it as a command with
55
+ [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
56
+
57
+ ```sh
58
+ uv tool install annotheca
59
+ # or
60
+ pipx install annotheca
61
+ ```
62
+
63
+ Then, to let Claude use it (optional, see [AI assistants](#ai-assistants)):
64
+
65
+ ```sh
66
+ annotheca connect
67
+ ```
68
+
69
+ Update later with `uv tool upgrade annotheca` (or `pipx upgrade annotheca`).
70
+
71
+ ## Use
72
+
73
+ ```sh
74
+ annotheca
75
+ ```
76
+
77
+ The interface opens in your browser at <http://127.0.0.1:8737>. Running the
78
+ command again while Annotheca is running simply reopens it.
79
+
80
+ | Option or command | Effect |
81
+ | --- | --- |
82
+ | `--port PORT` | Serve on another local port (default `8737`). |
83
+ | `--data-dir DIR` | Keep the library somewhere else. |
84
+ | `--no-browser` | Do not open the browser. |
85
+ | `connect` | Make Annotheca available to Claude (see below). |
86
+ | `mcp` | Serve the tools for AI assistants over stdio (used by `connect`). |
87
+
88
+ Tips:
89
+
90
+ - `Ctrl+Enter` adds what you pasted.
91
+ - Click a citation key to copy `\cite{key}`.
92
+ - Drop a PDF file on a paper to attach it.
93
+ - In the reader, select text to highlight it; switch to *Sticky note* and
94
+ click on the page to pin a note; click a highlight to recolour or delete it;
95
+ `Esc` goes back.
96
+
97
+ ## AI assistants
98
+
99
+ When an assistant writes references itself, it can get a title, an author or a
100
+ year wrong. With Annotheca, the assistant only says *which* paper it means (a
101
+ DOI, an arXiv ID, a citation); Annotheca fetches the metadata from doi.org,
102
+ arXiv and Crossref, files the paper with its PDF, and answers with the exact
103
+ citation key, title and authors to use. Every paper an assistant adds is marked
104
+ with its name, so you can always see, and filter, what it relied on.
105
+
106
+ `annotheca connect` registers Annotheca as an
107
+ [MCP](https://modelcontextprotocol.io) server for **Claude Code** (in every
108
+ project) and **Claude Desktop** when they are installed, and prints the
109
+ configuration to paste into other MCP clients (Cursor, Codex, Gemini CLI...).
110
+ The assistant then just needs to be asked:
111
+
112
+ > Find recent papers on contact-implicit trajectory optimisation and add the
113
+ > three most relevant to the SoA of my PhysProx project, with a note on each.
114
+
115
+ > What did I highlight in posa2013direct? Write the related-work paragraph
116
+ > citing the papers of the Method folder.
117
+
118
+ | Tool | What the assistant can do |
119
+ | --- | --- |
120
+ | `list_projects` | See the projects and their folders. |
121
+ | `create_project`, `create_folder` | Create them. |
122
+ | `add_references` | File papers from DOIs, arXiv IDs, BibTeX or citations, with a note; get back verified metadata. |
123
+ | `list_papers` | List the papers of a project with your notes. |
124
+ | `read_paper` | Read a paper's BibTeX, your notes, highlights and sticky notes, and the path of its PDF. |
125
+ | `add_note` | Add a signed paragraph to a paper's notes. |
126
+ | `get_bibtex` | Get the project's `.bib`. |
127
+
128
+ Nothing else is needed: the server explains itself to the assistant. Papers it
129
+ adds appear in the interface as soon as you come back to the window.
130
+
131
+ ## Your data
132
+
133
+ Everything stays on your machine, in `~/.local/share/annotheca` (or
134
+ `$XDG_DATA_HOME/annotheca`, or `$ANNOTHECA_HOME`):
135
+
136
+ ```
137
+ library.db SQLite database: projects, folders, BibTeX, notes, annotations
138
+ pdfs/ one PDF per paper
139
+ ```
140
+
141
+ Back it up by copying that directory. The only network requests are the
142
+ lookups of references and PDFs, sent to doi.org, arXiv, Crossref, Semantic
143
+ Scholar, OpenAlex and the repositories they point to.
144
+
145
+ <p align="center">
146
+ <img src="https://raw.githubusercontent.com/melissa-colin/annotheca/main/docs/library-dark.png" alt="Dark theme" width="700">
147
+ </p>
148
+
149
+ ## Development
150
+
151
+ ```sh
152
+ git clone https://github.com/melissa-colin/annotheca
153
+ cd annotheca
154
+ uv venv && uv pip install -e '.[test]'
155
+ .venv/bin/pytest
156
+ .venv/bin/annotheca --data-dir /tmp/annotheca-dev
157
+ ```
158
+
159
+ The code follows the Google Python and JavaScript style guides. The backend is
160
+ a small [FastAPI](https://fastapi.tiangolo.com/) app over SQLite
161
+ (`src/annotheca`); the interface is dependency-free JavaScript modules with
162
+ [pdf.js](https://mozilla.github.io/pdf.js/) vendored, so there is no build
163
+ step.
164
+
165
+ ```
166
+ src/annotheca/
167
+ cli.py command line entry point
168
+ api.py HTTP API and static files
169
+ library.py storage: projects, folders, papers, placements, annotations
170
+ bundle.py .annotheca files to share a project
171
+ mcp_server.py tools for AI assistants (Model Context Protocol)
172
+ models.py validated shapes shared by the API and the bundles
173
+ bibtex.py BibTeX parsing, formatting, citation keys, identifiers
174
+ sources.py doi.org, arXiv, Crossref, Semantic Scholar, OpenAlex lookups
175
+ static/ web interface
176
+ ```
177
+
178
+ ## License
179
+
180
+ MIT. pdf.js is distributed under the Apache License 2.0
181
+ (`src/annotheca/static/vendor/pdfjs/LICENSE`).
Binary file
Binary file
Binary file
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "annotheca"
7
+ version = "0.1.0"
8
+ description = "A local, bilingual reference manager for research projects: BibTeX, PDFs, highlights and sticky notes."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Melissa Colin" }]
13
+ requires-python = ">=3.10"
14
+ keywords = ["bibtex", "bibliography", "research", "pdf", "annotation", "reference-manager"]
15
+ classifiers = [
16
+ "Environment :: Web Environment",
17
+ "Intended Audience :: Science/Research",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Scientific/Engineering",
20
+ ]
21
+ dependencies = [
22
+ "fastapi>=0.110",
23
+ "httpx>=0.27",
24
+ "mcp>=2,<3",
25
+ "uvicorn>=0.29",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ test = ["pytest>=8"]
30
+
31
+ [project.scripts]
32
+ annotheca = "annotheca.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/melissa-colin/annotheca"
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """Annotheca: a local reference manager for research projects."""
2
+
3
+ __version__ = '0.1.0'
@@ -0,0 +1,5 @@
1
+ """Allows `python -m annotheca`."""
2
+
3
+ from annotheca import cli
4
+
5
+ cli.main()
@@ -0,0 +1,194 @@
1
+ """HTTP API and static web interface."""
2
+
3
+ import mimetypes
4
+ import pathlib
5
+ from typing import Any
6
+ from urllib import parse
7
+
8
+ import fastapi
9
+ import httpx
10
+ import pydantic
11
+ from fastapi import responses
12
+ from fastapi import staticfiles
13
+
14
+ from annotheca import bundle
15
+ from annotheca import library as library_lib
16
+ from annotheca import models
17
+ from annotheca import sources
18
+
19
+ _STATIC_DIR = pathlib.Path(__file__).parent / 'static'
20
+
21
+ # Exception types mapped to an HTTP status and the error code the
22
+ # interface translates for the user.
23
+ _ERRORS = {
24
+ library_lib.NotFoundError: (404, 'not_found'),
25
+ library_lib.ConflictError: (409, 'name_taken'),
26
+ library_lib.InvalidPdfError: (422, 'not_pdf'),
27
+ bundle.InvalidBundleError: (422, 'not_bundle'),
28
+ }
29
+
30
+
31
+ class NameBody(pydantic.BaseModel):
32
+ name: models.Name
33
+
34
+
35
+ class AddPapersBody(pydantic.BaseModel):
36
+ """Either pasted references or an already known paper to file."""
37
+ text: str | None = None
38
+ paper_id: int | None = None
39
+
40
+
41
+ class NotesBody(pydantic.BaseModel):
42
+ notes: str
43
+
44
+
45
+ class AnnotationChanges(pydantic.BaseModel):
46
+ color: models.Color | None = None
47
+ geometry: list[Any] | None = None
48
+ body: str | None = None
49
+
50
+
51
+ def create_app(library: library_lib.Library,
52
+ client: httpx.Client | None = None) -> fastapi.FastAPI:
53
+ """Builds the web application.
54
+
55
+ Args:
56
+ library: Where everything is stored.
57
+ client: HTTP client for the scholarly sources; a default one is
58
+ created when omitted.
59
+ """
60
+ client = client or sources.make_client()
61
+ app = fastapi.FastAPI(title='Annotheca', docs_url=None, redoc_url=None)
62
+ api = fastapi.APIRouter(prefix='/api')
63
+
64
+ for error_type, (status, code) in _ERRORS.items():
65
+ app.add_exception_handler(error_type, _error_handler(status, code))
66
+
67
+ @app.exception_handler(sources.ResolveError)
68
+ def _resolve_error(request, error):
69
+ del request # Unused.
70
+ return responses.JSONResponse({'detail': str(error)}, status_code=422)
71
+
72
+ @api.get('/projects')
73
+ def list_projects():
74
+ return library.list_projects()
75
+
76
+ @api.post('/projects', status_code=201)
77
+ def create_project(body: NameBody):
78
+ return library.create_project(body.name)
79
+
80
+ @api.get('/projects/{project_id}')
81
+ def get_project(project_id: int):
82
+ return library.get_project(project_id)
83
+
84
+ @api.get('/projects/{project_id}/bibtex')
85
+ def export_bibtex(project_id: int):
86
+ name, content = library.export_bibtex(project_id)
87
+ return _download(content, f'{name}.bib', 'application/x-bibtex')
88
+
89
+ @api.get('/projects/{project_id}/bundle')
90
+ def export_bundle(project_id: int):
91
+ """The project with its notes, annotations and PDFs, to share."""
92
+ name, content = bundle.export_project(library, project_id)
93
+ return _download(content, f'{name}.annotheca', 'application/zip')
94
+
95
+ @api.post('/projects/import', status_code=201)
96
+ async def import_bundle(request: fastapi.Request):
97
+ return bundle.import_project(library, await request.body())
98
+
99
+ @api.post('/projects/{project_id}/folders', status_code=201)
100
+ def create_folder(project_id: int, body: NameBody):
101
+ return library.create_folder(project_id, body.name)
102
+
103
+ @api.patch('/projects/{project_id}', status_code=204)
104
+ def rename_project(project_id: int, body: NameBody):
105
+ library.rename('project', project_id, body.name)
106
+
107
+ @api.delete('/projects/{project_id}', status_code=204)
108
+ def delete_project(project_id: int):
109
+ library.delete('project', project_id)
110
+
111
+ @api.patch('/folders/{folder_id}', status_code=204)
112
+ def rename_folder(folder_id: int, body: NameBody):
113
+ library.rename('folder', folder_id, body.name)
114
+
115
+ @api.delete('/folders/{folder_id}', status_code=204)
116
+ def delete_folder(folder_id: int):
117
+ library.delete('folder', folder_id)
118
+
119
+ @api.post('/folders/{folder_id}/papers')
120
+ def add_papers(folder_id: int, body: AddPapersBody):
121
+ """Files pasted references, or a known paper, in a folder."""
122
+ if body.paper_id is not None:
123
+ paper_ids = [body.paper_id]
124
+ elif body.text and body.text.strip():
125
+ entries = sources.resolve(body.text, client)
126
+ paper_ids = list(dict.fromkeys(map(library.add_paper, entries)))
127
+ else:
128
+ raise sources.ResolveError('unresolvable')
129
+ added = library.file_papers(folder_id, paper_ids)
130
+ return {'paper_ids': paper_ids, 'added': added,
131
+ 'already_there': len(paper_ids) - added}
132
+
133
+ @api.put('/placements/{placement_id}/notes', status_code=204)
134
+ def set_notes(placement_id: int, body: NotesBody):
135
+ library.set_notes(placement_id, body.notes)
136
+
137
+ @api.delete('/placements/{placement_id}', status_code=204)
138
+ def remove_placement(placement_id: int):
139
+ library.remove_placement(placement_id)
140
+
141
+ @api.get('/papers/{paper_id}/pdf')
142
+ def get_pdf(paper_id: int):
143
+ path = library.pdf_path(paper_id)
144
+ if not path.exists():
145
+ raise library_lib.NotFoundError
146
+ return responses.FileResponse(path, media_type='application/pdf')
147
+
148
+ @api.put('/papers/{paper_id}/pdf', status_code=204)
149
+ async def upload_pdf(paper_id: int, request: fastapi.Request):
150
+ library.save_pdf(paper_id, await request.body())
151
+
152
+ @api.post('/papers/{paper_id}/pdf/fetch')
153
+ def fetch_pdf(paper_id: int):
154
+ """Looks for an open-access PDF; reports whether one was found."""
155
+ pdf = sources.find_pdf(library.get_paper(paper_id), client)
156
+ if pdf:
157
+ library.save_pdf(paper_id, pdf)
158
+ return {'found': pdf is not None}
159
+
160
+ @api.get('/placements/{placement_id}/annotations')
161
+ def list_annotations(placement_id: int):
162
+ return library.list_annotations(placement_id)
163
+
164
+ @api.post('/placements/{placement_id}/annotations', status_code=201)
165
+ def create_annotation(placement_id: int, body: models.Annotation):
166
+ return library.create_annotation(placement_id, body.model_dump())
167
+
168
+ @api.patch('/annotations/{annotation_id}')
169
+ def update_annotation(annotation_id: int, body: AnnotationChanges):
170
+ return library.update_annotation(
171
+ annotation_id, body.model_dump(exclude_none=True))
172
+
173
+ @api.delete('/annotations/{annotation_id}', status_code=204)
174
+ def delete_annotation(annotation_id: int):
175
+ library.delete_annotation(annotation_id)
176
+
177
+ app.include_router(api)
178
+ mimetypes.add_type('text/javascript', '.mjs')
179
+ app.mount('/', staticfiles.StaticFiles(directory=_STATIC_DIR, html=True))
180
+ return app
181
+
182
+
183
+ def _download(content: str | bytes, filename: str,
184
+ media_type: str) -> responses.Response:
185
+ disposition = f"attachment; filename*=UTF-8''{parse.quote(filename)}"
186
+ return responses.Response(content, media_type=media_type,
187
+ headers={'Content-Disposition': disposition})
188
+
189
+
190
+ def _error_handler(status: int, code: str):
191
+ def handle(request: fastapi.Request, error: Exception):
192
+ del request, error # Unused.
193
+ return responses.JSONResponse({'detail': code}, status_code=status)
194
+ return handle