joppylib 0.1.0a12__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.
- joppylib-0.1.0a12/.gitignore +207 -0
- joppylib-0.1.0a12/.python-version +1 -0
- joppylib-0.1.0a12/CHANGELOG.md +30 -0
- joppylib-0.1.0a12/LICENSE +21 -0
- joppylib-0.1.0a12/PKG-INFO +58 -0
- joppylib-0.1.0a12/README.md +37 -0
- joppylib-0.1.0a12/docs/dev/improvements_report_2026-03.md +149 -0
- joppylib-0.1.0a12/docs/dev/semantic-versioning-in-practice.md +342 -0
- joppylib-0.1.0a12/docs/dev/testing_guide.md +138 -0
- joppylib-0.1.0a12/docs/howto/python_testing_setup.md +378 -0
- joppylib-0.1.0a12/docs/howto/testing_concepts.md +223 -0
- joppylib-0.1.0a12/pyproject.toml +40 -0
- joppylib-0.1.0a12/src/joppylib/__init__.py +25 -0
- joppylib-0.1.0a12/src/joppylib/api_client.py +781 -0
- joppylib-0.1.0a12/src/joppylib/config.py +25 -0
- joppylib-0.1.0a12/src/joppylib/connection.py +90 -0
- joppylib-0.1.0a12/src/joppylib/exceptions.py +8 -0
- joppylib-0.1.0a12/src/joppylib/joplin_client.py +429 -0
- joppylib-0.1.0a12/src/joppylib/py.typed +0 -0
- joppylib-0.1.0a12/tests/conftest.py +42 -0
- joppylib-0.1.0a12/tests/test_api_client.py +341 -0
- joppylib-0.1.0a12/tests/test_config.py +42 -0
- joppylib-0.1.0a12/tests/test_connection.py +113 -0
- joppylib-0.1.0a12/tests/test_joplin_client.py +106 -0
- joppylib-0.1.0a12/uv.lock +340 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
#dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
#poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
#pdm.lock
|
|
116
|
+
#pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
#pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Environments
|
|
138
|
+
.env
|
|
139
|
+
.envrc
|
|
140
|
+
.venv
|
|
141
|
+
env/
|
|
142
|
+
venv/
|
|
143
|
+
ENV/
|
|
144
|
+
env.bak/
|
|
145
|
+
venv.bak/
|
|
146
|
+
|
|
147
|
+
# Spyder project settings
|
|
148
|
+
.spyderproject
|
|
149
|
+
.spyproject
|
|
150
|
+
|
|
151
|
+
# Rope project settings
|
|
152
|
+
.ropeproject
|
|
153
|
+
|
|
154
|
+
# mkdocs documentation
|
|
155
|
+
/site
|
|
156
|
+
|
|
157
|
+
# mypy
|
|
158
|
+
.mypy_cache/
|
|
159
|
+
.dmypy.json
|
|
160
|
+
dmypy.json
|
|
161
|
+
|
|
162
|
+
# Pyre type checker
|
|
163
|
+
.pyre/
|
|
164
|
+
|
|
165
|
+
# pytype static type analyzer
|
|
166
|
+
.pytype/
|
|
167
|
+
|
|
168
|
+
# Cython debug symbols
|
|
169
|
+
cython_debug/
|
|
170
|
+
|
|
171
|
+
# PyCharm
|
|
172
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
173
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
174
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
175
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
176
|
+
#.idea/
|
|
177
|
+
|
|
178
|
+
# Abstra
|
|
179
|
+
# Abstra is an AI-powered process automation framework.
|
|
180
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
181
|
+
# Learn more at https://abstra.io/docs
|
|
182
|
+
.abstra/
|
|
183
|
+
|
|
184
|
+
# Visual Studio Code
|
|
185
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
186
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
188
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
189
|
+
# .vscode/
|
|
190
|
+
|
|
191
|
+
# Ruff stuff:
|
|
192
|
+
.ruff_cache/
|
|
193
|
+
|
|
194
|
+
# PyPI configuration file
|
|
195
|
+
.pypirc
|
|
196
|
+
|
|
197
|
+
# Cursor
|
|
198
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
199
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
200
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
201
|
+
.cursorignore
|
|
202
|
+
.cursorindexingignore
|
|
203
|
+
|
|
204
|
+
# Marimo
|
|
205
|
+
marimo/_static/
|
|
206
|
+
marimo/_lsp/
|
|
207
|
+
__marimo__/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0a12] - 2026-04-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Runtime `__version__` attribute derived from package metadata.
|
|
14
|
+
- CHANGELOG.md following Keep a Changelog format.
|
|
15
|
+
- PyPI metadata: license, keywords, classifiers, URLS.
|
|
16
|
+
|
|
17
|
+
## [0.1.0a11] - 2026-03-31
|
|
18
|
+
|
|
19
|
+
Alpha development phase, consolidated from releases 0.1.0a1 through 0.1.0a11.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Note management: create, read, update, delete, search, and list notes.
|
|
23
|
+
- Tag management: full CRUD, add/remove tags from notes, get all tags for a note.
|
|
24
|
+
- Folder management: full CRUD operations.
|
|
25
|
+
- Support for events, resources, and revisions.
|
|
26
|
+
- Full-text search using Joplin's search syntax with field filtering and ordering.
|
|
27
|
+
- Batch listing with optional field filtering and ordering parameters.
|
|
28
|
+
- Flexible authentication: interactive (browser-based) and API key-based.
|
|
29
|
+
- Debug mode for including raw API responses.
|
|
30
|
+
- Custom exceptions and connection validation.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jeroen Kroesen
|
|
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,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: joppylib
|
|
3
|
+
Version: 0.1.0a12
|
|
4
|
+
Summary: A thin abstraction layer over the Joplin Data API
|
|
5
|
+
Project-URL: Homepage, https://github.com/jeroenkroesen/joppylib
|
|
6
|
+
Project-URL: Repository, https://github.com/jeroenkroesen/joppylib
|
|
7
|
+
Author-email: Jeroen Kroesen <jeroen@kroesen.nu>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: joplin,joplin-api,notes
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: pydantic-settings>=2.10.1
|
|
18
|
+
Requires-Dist: pydantic>=2.11.7
|
|
19
|
+
Requires-Dist: requests>=2.32.4
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# JopPyLib
|
|
23
|
+
Python abstraction over the Joplin data api
|
|
24
|
+
|
|
25
|
+
==WARNING==
|
|
26
|
+
JopPyLib is under heavy development. I've only just started. I will update this readme when a first usable version is released.
|
|
27
|
+
|
|
28
|
+
JopPyLib is part of the [Artumis project](https://jeroenkroesen.github.io/artumis_site/). It is meant to be a thin layer to enable programmatic manipulation of Joplin notes via a higher level interface than the data API.
|
|
29
|
+
|
|
30
|
+
JopPyLib is highly opinionated because it is designed to enable a higher level library call [JopBrainLib](https://github.com/jeroenkroesen/jopbrainlib) that enables second brain functionality in Joplin.
|
|
31
|
+
|
|
32
|
+
An [example Jupyter notebook interface](https://github.com/jeroenkroesen/joppylib-notebook) is available for interacting with JopPyLib.
|
|
33
|
+
***
|
|
34
|
+
|
|
35
|
+
## Status
|
|
36
|
+
Current release: joppylib-0.1.0a11
|
|
37
|
+
Very alpha testing release.
|
|
38
|
+
***
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Roadmap
|
|
42
|
+
- [x] Clean up and document old personal codebase
|
|
43
|
+
- [ ] Complete api layer (`api_client.py`)
|
|
44
|
+
- [x] Generate packages on github to allow pip install when testing.
|
|
45
|
+
- [x] Implement PUT
|
|
46
|
+
- [x] Implement better tag functions
|
|
47
|
+
- [x] Delete tag from note
|
|
48
|
+
- [x] Get all tags attached to a note
|
|
49
|
+
- [ ] Test API functionality
|
|
50
|
+
- [x] Demonstrate low-level interface in [notebook](https://github.com/jeroenkroesen/joppylib-notebook)
|
|
51
|
+
- [x] Design higher level interface
|
|
52
|
+
- [x] Implement higher level interface
|
|
53
|
+
|
|
54
|
+
***
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Reference
|
|
58
|
+
[Joplin Data API documentation](https://joplinapp.org/help/api/references/rest_api/)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# JopPyLib
|
|
2
|
+
Python abstraction over the Joplin data api
|
|
3
|
+
|
|
4
|
+
==WARNING==
|
|
5
|
+
JopPyLib is under heavy development. I've only just started. I will update this readme when a first usable version is released.
|
|
6
|
+
|
|
7
|
+
JopPyLib is part of the [Artumis project](https://jeroenkroesen.github.io/artumis_site/). It is meant to be a thin layer to enable programmatic manipulation of Joplin notes via a higher level interface than the data API.
|
|
8
|
+
|
|
9
|
+
JopPyLib is highly opinionated because it is designed to enable a higher level library call [JopBrainLib](https://github.com/jeroenkroesen/jopbrainlib) that enables second brain functionality in Joplin.
|
|
10
|
+
|
|
11
|
+
An [example Jupyter notebook interface](https://github.com/jeroenkroesen/joppylib-notebook) is available for interacting with JopPyLib.
|
|
12
|
+
***
|
|
13
|
+
|
|
14
|
+
## Status
|
|
15
|
+
Current release: joppylib-0.1.0a11
|
|
16
|
+
Very alpha testing release.
|
|
17
|
+
***
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Roadmap
|
|
21
|
+
- [x] Clean up and document old personal codebase
|
|
22
|
+
- [ ] Complete api layer (`api_client.py`)
|
|
23
|
+
- [x] Generate packages on github to allow pip install when testing.
|
|
24
|
+
- [x] Implement PUT
|
|
25
|
+
- [x] Implement better tag functions
|
|
26
|
+
- [x] Delete tag from note
|
|
27
|
+
- [x] Get all tags attached to a note
|
|
28
|
+
- [ ] Test API functionality
|
|
29
|
+
- [x] Demonstrate low-level interface in [notebook](https://github.com/jeroenkroesen/joppylib-notebook)
|
|
30
|
+
- [x] Design higher level interface
|
|
31
|
+
- [x] Implement higher level interface
|
|
32
|
+
|
|
33
|
+
***
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Reference
|
|
37
|
+
[Joplin Data API documentation](https://joplinapp.org/help/api/references/rest_api/)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# JopPyLib — Critical Examination Report
|
|
2
|
+
**Date:** 2026-03-24
|
|
3
|
+
**Scope:** All source code in `src/joppylib/`
|
|
4
|
+
**Context:** All API traffic targets the Joplin Data API running on the user's localhost. Joplin Server is out-of-scope.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Input Sanitization
|
|
9
|
+
|
|
10
|
+
### ~~No input sanitization on `query` in `search()` — Correctness bug~~ RESOLVED
|
|
11
|
+
~~`api_client.py:107` interpolates the query directly into the URL string:~~
|
|
12
|
+
```python
|
|
13
|
+
params = f'?query={query}'
|
|
14
|
+
```
|
|
15
|
+
~~A query containing `&`, `=`, `#`, `+`, or spaces will break the URL or inject additional parameters. For example, searching for `C++` results in Joplin receiving `C ` (plus decoded as space), and searching for `foo&limit=1` would override the pagination limit.~~
|
|
16
|
+
|
|
17
|
+
**Fixed:** `search()` now uses a `params` dict passed to `requests.get()`, which handles URL encoding automatically. Verified end-to-end against a live Joplin instance with `C++`, `C#`, `foo&bar`, and `key=value` queries.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Bugs & Correctness Issues
|
|
22
|
+
|
|
23
|
+
### ~~Copy-paste docstring errors~~ RESOLVED
|
|
24
|
+
~~- `delete()` docstring says "Get an entity instance by ID" (`api_client.py:385`, `joplin_client.py:246`).~~
|
|
25
|
+
~~- `update()` docstring says "data for the object to create" (`api_client.py:359`, `joplin_client.py:221`).~~
|
|
26
|
+
|
|
27
|
+
**Fixed:** Corrected `delete()` docstrings to say "Delete an entity instance by ID" and `update()` parameter descriptions to say "data for the object to update" in both `api_client.py` and `joplin_client.py`.
|
|
28
|
+
|
|
29
|
+
### ~~`check_connection` ignores non-200 responses (`connection.py:30-33`)~~ RESOLVED
|
|
30
|
+
```python
|
|
31
|
+
try:
|
|
32
|
+
requests.get(url_ping)
|
|
33
|
+
return True
|
|
34
|
+
except requests.exceptions.ConnectionError:
|
|
35
|
+
return False
|
|
36
|
+
```
|
|
37
|
+
~~Any HTTP response (including 500) returns `True`. Only a connection failure returns `False`. A Joplin instance that is up but malfunctioning would be reported as connected.~~
|
|
38
|
+
|
|
39
|
+
**Fixed:** `check_connection` now uses `resp.ok` (True only for 2xx) and catches the broad `RequestException` base class.
|
|
40
|
+
|
|
41
|
+
### ~~Inconsistent return types across methods~~ ACCEPTED (by design)
|
|
42
|
+
- `get()`, `create()`, `update()`, `delete()` return raw `requests.Response`.
|
|
43
|
+
- `search()`, `get_multi()`, `get_all_tags()` return `Dict[str, Any]` with a `success`/`data`/`error` wrapper.
|
|
44
|
+
|
|
45
|
+
~~Consumers must handle two completely different response patterns depending on which method they call.~~
|
|
46
|
+
|
|
47
|
+
**Accepted:** The split maps to a real architectural difference — single-request methods return the raw Response (full access to status, headers, body), while paginated methods aggregate multiple responses into a Dict wrapper. Unifying would either lose information (Response direction) or add unnecessary wrapping (Dict direction). Downstream consumer JopBrainLib already handles both patterns. Type hints document the contract per method.
|
|
48
|
+
|
|
49
|
+
### ~~`name` class attribute collision in `joplin_client`~~ RESOLVED
|
|
50
|
+
~~`joplin_client.Note` has class attribute `name: str = 'note'`, but its inherited `__init__` takes `name` as a parameter and overwrites it via `self.name = name`. The class attribute is dead code. Same for `Tag`.~~
|
|
51
|
+
|
|
52
|
+
**Fixed:** Removed dead `name` class attributes from `Note` and `Tag` in `joplin_client.py`. Also corrected `Tag` docstring from "Interact with items" to "Interact with tags".
|
|
53
|
+
|
|
54
|
+
### ~~Unused `ITEM_TYPE` constant~~ RESOLVED
|
|
55
|
+
~~`defaults.py` defines `ITEM_TYPE = 'note'` but it is never referenced anywhere in the codebase.~~
|
|
56
|
+
|
|
57
|
+
**Fixed:** Removed `ITEM_TYPE = 'note'` from `defaults.py`.
|
|
58
|
+
|
|
59
|
+
### Mutable default class attributes (`api_client.py:28-29`)
|
|
60
|
+
```python
|
|
61
|
+
fields: List[str] = []
|
|
62
|
+
fields_create_required: List[str] = []
|
|
63
|
+
```
|
|
64
|
+
Class-level mutable defaults. Not currently mutated, but a latent footgun if any code ever appends to them on an instance.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. Robustness Issues
|
|
69
|
+
|
|
70
|
+
### ~~Auth polling loop has no timeout and no delay (`connection.py:77-81`)~~ RESOLVED
|
|
71
|
+
~~Two problems:~~
|
|
72
|
+
1. ~~If the user never responds to the Joplin auth dialog, this loops forever.~~
|
|
73
|
+
2. ~~There is no `time.sleep()` between polls — it hammers the local Joplin API as fast as Python can loop, which can make the Joplin UI sluggish.~~
|
|
74
|
+
|
|
75
|
+
**Fixed:** Auth polling now has a configurable timeout and a delay between polls.
|
|
76
|
+
|
|
77
|
+
### `get_auth_token` doesn't handle HTTP errors (`connection.py:73-74`)
|
|
78
|
+
```python
|
|
79
|
+
resp_init = requests.post(url_init)
|
|
80
|
+
init_token = resp_init.json()['auth_token']
|
|
81
|
+
```
|
|
82
|
+
If the POST returns a non-200 or non-JSON response, this crashes with an unhelpful `KeyError` or `JSONDecodeError`.
|
|
83
|
+
|
|
84
|
+
### ~~No timeout on any HTTP request~~ RESOLVED
|
|
85
|
+
~~Every `requests.get/post/put/delete` call has no `timeout` parameter. If Joplin freezes (not uncommon for an Electron app under load), the calling Python code hangs indefinitely.~~
|
|
86
|
+
|
|
87
|
+
**Fixed:** All HTTP requests now have a configurable timeout.
|
|
88
|
+
|
|
89
|
+
### No retry on paginated operations
|
|
90
|
+
If page 3 of 5 fails, the entire operation fails and all data (including successfully fetched pages 1-2) is discarded.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. Design Issues
|
|
95
|
+
|
|
96
|
+
### ~~Pagination loop duplicated 3 times~~ RESOLVED
|
|
97
|
+
~~The same ~20-line pagination pattern is copy-pasted in `search()` (lines 131-157), `get_multi()` (lines 230-256), and `Note.get_all_tags()` (lines 513-539). They differ only in the initial URL. This has already caused bugs historically (per commits "Fixed: reponse indexes" and "Fixed: added req_index to fix indexing").~~
|
|
98
|
+
|
|
99
|
+
**Fixed:** Extracted a shared `_paginate()` method on `Item`. All three callers now delegate to it. Also migrated `get_multi()` and `get_all_tags()` from string interpolation to `params=` dict.
|
|
100
|
+
|
|
101
|
+
### The high-level layer is mostly boilerplate
|
|
102
|
+
Every method in `joplin_client.Item` is a pass-through that prepends `self._api_key` and `self.settings`. ~270 lines exist to hide two parameters. A `requests.Session`-style approach, `functools.partial`, or `__getattr__` delegation could achieve the same in a fraction of the code.
|
|
103
|
+
|
|
104
|
+
### No connection reuse or shared client instance
|
|
105
|
+
Every API call opens a new TCP connection. There is no shared client instance for connection reuse, central timeout config, or auth handling.
|
|
106
|
+
|
|
107
|
+
**Recommendation:** Migrate from `requests` to `httpx`. The `httpx.Client` (sync) provides connection pooling, base URL support, and shared config out of the box. Additionally, `httpx.AsyncClient` offers an almost identical async API, enabling a future async interface without a library swap. This is low priority — the library targets localhost so the performance impact is minimal — but it removes a design issue and opens the door for async support.
|
|
108
|
+
|
|
109
|
+
### `create()` accepts `Dict | str` but `str` bypasses validation (`api_client.py:331-338`)
|
|
110
|
+
When `data` is a string, required field validation is skipped entirely. The user gets a raw HTTP error with no library-level context if it fails.
|
|
111
|
+
|
|
112
|
+
### `JoplinClient` hard-codes entity specialization (`joplin_client.py:405-410`)
|
|
113
|
+
`folder`, `event`, `resource`, and `revision` use the base `Item` class. This means `resource` can't handle file uploads (per the TODO at `api_client.py:729`), and there's no way for a consumer to know this without reading the source.
|
|
114
|
+
|
|
115
|
+
### `id` shadows the Python builtin
|
|
116
|
+
Used as a parameter name throughout both layers. Convention is `item_id` or `id_`.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 5. Testing & Quality
|
|
121
|
+
|
|
122
|
+
### ~~Zero tests~~ RESOLVED
|
|
123
|
+
~~This is the single biggest risk. The git history shows multiple pagination and indexing bugs that were caught manually. The triplicated pagination loop is especially prone to regressions.~~
|
|
124
|
+
|
|
125
|
+
**Fixed:** 52 tests added across all modules (`test_config.py`, `test_connection.py`, `test_api_client.py`, `test_joplin_client.py`) using `pytest` and `responses` for HTTP mocking. Covers CRUD operations, pagination, auth flows, validation, and delegation.
|
|
126
|
+
|
|
127
|
+
### No CI/CD pipeline
|
|
128
|
+
No GitHub Actions, no linting, no type checking configured. The `py.typed` marker exists but `mypy`/`pyright` is not configured to run.
|
|
129
|
+
|
|
130
|
+
### `dist/` directory is checked into git
|
|
131
|
+
11 versions of wheels and tarballs bloat the repository. These should be built and published via CI or GitHub Releases.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Summary
|
|
136
|
+
|
|
137
|
+
| Priority | Issue | Why it matters |
|
|
138
|
+
|----------|-------|----------------|
|
|
139
|
+
| ~~**High**~~ | ~~No request timeouts~~ | ~~Hangs if Joplin freezes~~ **RESOLVED** |
|
|
140
|
+
| ~~**High**~~ | ~~Auth polling: no timeout, no delay~~ | ~~Infinite loop, pegs CPU, makes Joplin sluggish~~ **RESOLVED** |
|
|
141
|
+
| ~~**High**~~ | ~~URL query not encoded~~ | ~~Searches with special characters break silently~~ **RESOLVED** |
|
|
142
|
+
| ~~**High**~~ | ~~No tests~~ | ~~Every change risks regressions (proven by history)~~ **RESOLVED** |
|
|
143
|
+
| ~~**Medium**~~ | ~~Pagination loop duplicated 3x~~ | ~~Already caused bugs; will again~~ **RESOLVED** |
|
|
144
|
+
| ~~**Medium**~~ | ~~Inconsistent return types (Response vs Dict)~~ | ~~Confusing API for consumers~~ **ACCEPTED (by design)** |
|
|
145
|
+
| ~~**Low**~~ | ~~No shared client / consider httpx migration~~ | ~~No connection reuse; httpx would also enable future async support~~ **ACCEPTED (deferred)** |
|
|
146
|
+
| ~~**Medium**~~ | ~~`check_connection` ignores status codes~~ | ~~False positives~~ **RESOLVED** |
|
|
147
|
+
| ~~**Low**~~ | ~~Copy-paste docstring errors~~ | ~~Misleading docs~~ **RESOLVED** |
|
|
148
|
+
| ~~**Low**~~ | ~~`dist/` in git~~ | ~~Repository bloat~~ **ACCEPTED (deferred)** |
|
|
149
|
+
| ~~**Low**~~ | ~~Unused `ITEM_TYPE`, dead `name` class attrs~~ | ~~Dead code~~ **RESOLVED** |
|