evernote2unbent 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.
@@ -0,0 +1,61 @@
1
+ name: Publish
2
+
3
+ # Publishing is driven by tags, so a release is `git tag v0.1.0 && git push --tags`
4
+ # rather than a checklist someone has to remember at the end of a long day.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+ # Manual runs go to TestPyPI, so the publishing path itself can be exercised without
9
+ # burning a real version number — PyPI refuses to reuse one, even after a delete.
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ matrix:
17
+ python-version: ["3.10", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+ - run: uv python install ${{ matrix.python-version }}
24
+ - run: uv sync --group test --group dev
25
+ - run: uv run ruff check src tests
26
+ - run: uv run ruff format --check src tests
27
+ - run: uv run pytest tests/ -q
28
+
29
+ publish:
30
+ # Gated on the tests: a tag that fails them should not become a release anyone can
31
+ # install, and PyPI versions cannot be replaced once published.
32
+ needs: test
33
+ runs-on: ubuntu-latest
34
+ environment: release
35
+ permissions:
36
+ # Trusted Publishing: PyPI verifies this workflow's identity through OIDC, so there
37
+ # is no API token to store, rotate, or leak.
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: astral-sh/setup-uv@v5
42
+ - run: uv build
43
+
44
+ - name: Check the tag matches the version in pyproject.toml
45
+ if: startsWith(github.ref, 'refs/tags/')
46
+ run: |
47
+ TAG="${GITHUB_REF#refs/tags/v}"
48
+ VERSION=$(uv run python -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
49
+ if [ "$TAG" != "$VERSION" ]; then
50
+ echo "Tag v$TAG does not match pyproject version $VERSION." >&2
51
+ echo "Bump the version, commit, then tag." >&2
52
+ exit 1
53
+ fi
54
+
55
+ - name: Publish to TestPyPI
56
+ if: github.event_name == 'workflow_dispatch'
57
+ run: uv publish --publish-url https://test.pypi.org/legacy/
58
+
59
+ - name: Publish to PyPI
60
+ if: startsWith(github.ref, 'refs/tags/')
61
+ run: uv publish
@@ -0,0 +1,26 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ # The three platforms people migrate on. Windows is included because the token
15
+ # and config stores touch file modes, which behave differently there.
16
+ os: [ubuntu-latest, macos-latest, windows-latest]
17
+ python-version: ["3.10", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+ - run: uv python install ${{ matrix.python-version }}
24
+ - run: uv sync --group test --group dev
25
+ - run: uv run ruff check src tests
26
+ - run: uv run pytest tests/ -q
@@ -0,0 +1,10 @@
1
+ # Local Evernote sync database — data, routinely gigabytes.
2
+ en_backup.db
3
+ *.db
4
+
5
+ .venv/
6
+ dist/
7
+ __pycache__/
8
+ *.egg-info/
9
+ .pytest_cache/
10
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C Deutsch
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,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: evernote2unbent
3
+ Version: 0.1.0
4
+ Summary: Move your Evernote notes into Unbent, without an ENEX file in between.
5
+ Project-URL: Homepage, https://github.com/cdeutsch/evernote2unbent
6
+ Project-URL: Unbent, https://unbent.app
7
+ Project-URL: evernote-backup, https://github.com/vzhd1701/evernote-backup
8
+ Author: C Deutsch
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: enex,evernote,export,migration,notes,unbent
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: click>=8.1
20
+ Requires-Dist: evernote-backup>=1.14.0
21
+ Requires-Dist: requests>=2.31
22
+ Description-Content-Type: text/markdown
23
+
24
+ # evernote2unbent
25
+
26
+ Move your Evernote notes into [Unbent](https://unbent.app) — notes, notebooks, tags and
27
+ attachments — without exporting an ENEX file first.
28
+
29
+ Evernote has no export API, so every other route runs through the desktop client's
30
+ File → Export, which is manual, unreliable past a few thousand notes, and produces
31
+ multi-gigabyte files you then have to feed somewhere. This syncs your account with
32
+ [evernote-backup](https://github.com/vzhd1701/evernote-backup) and uploads straight from
33
+ its local database.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ uv tool install evernote2unbent
39
+ ```
40
+
41
+ Or with pipx:
42
+
43
+ ```bash
44
+ pipx install evernote2unbent
45
+ ```
46
+
47
+ ## Use
48
+
49
+ ```bash
50
+ evernote2unbent migrate
51
+ ```
52
+
53
+ That signs you in to Evernote, syncs your account to a local database, then uploads
54
+ everything to Unbent. It asks which Unbent instance to use the first time and remembers
55
+ the answer.
56
+
57
+ **Start with one notebook** to see the result before moving everything. The sync still
58
+ fetches your whole account — it is the upload that is scoped:
59
+
60
+ ```bash
61
+ evernote2unbent migrate -n "Some Notebook"
62
+ ```
63
+
64
+ Interrupted runs are safe to repeat: the sync is incremental, and notes that already
65
+ landed are skipped rather than duplicated.
66
+
67
+ ### Doing the steps separately
68
+
69
+ `migrate` runs [evernote-backup](https://github.com/vzhd1701/evernote-backup) for you. If
70
+ you already use it, or want the ENEX files too, run the halves yourself:
71
+
72
+ ```bash
73
+ evernote-backup init-db
74
+ evernote-backup sync
75
+ evernote2unbent upload
76
+ ```
77
+
78
+ ## Re-running is safe
79
+
80
+ Unbent keys each note on a hash of its title and body, so a note that already landed is
81
+ skipped rather than duplicated. That makes an interrupted upload resumable by simply
82
+ repeating the command — and a note whose attachments failed partway gets only the missing
83
+ files on the next run.
84
+
85
+ Nothing here deletes or modifies anything in Evernote.
86
+
87
+ ## Options
88
+
89
+ | Flag | Meaning |
90
+ | --- | --- |
91
+ | `-d, --database` | The database `evernote-backup sync` wrote (default `en_backup.db`) |
92
+ | `-n, --notebook` | Only this notebook. Repeatable |
93
+ | `-t, --tag` | Only notes with this tag. Repeatable |
94
+ | `--include-trash` | Include notes from Evernote's trash |
95
+ | `--url` | Instance for one command, without storing it |
96
+
97
+ ## Signing in
98
+
99
+ `login` prints a short code and a link. Approving it in a browser you are already signed in
100
+ to grants an ordinary session — no password is typed into the terminal, and no long-lived
101
+ key is pasted from a settings page.
102
+
103
+ The token is stored in `~/.config/unbent/credentials.json` at mode `0600`, keyed by
104
+ instance so a token for one server is never sent to another. `logout` forgets it locally;
105
+ signing out everywhere in Unbent revokes it for real.
106
+
107
+ ## Which instance
108
+
109
+ There is deliberately no default. A guessed instance means a forgotten flag can send a
110
+ migration to a server you did not choose, and that failure is silent, because uploading to
111
+ the wrong Unbent still succeeds.
112
+
113
+ The answer is stored in `~/.config/unbent/config.json` — plain JSON, mode `0644`, separate
114
+ from the credentials file so changing a URL never means opening one with a token in it.
115
+
116
+ ```bash
117
+ evernote2unbent set-url https://unbent.app
118
+ evernote2unbent show-config
119
+ ```
120
+
121
+ `$UNBENT_URL` overrides it for a shell session, `--url` for a single command.
122
+
123
+ ## What gets converted
124
+
125
+ Notes arrive as Markdown. The conversion happens server-side in Unbent, which is why this
126
+ package does not implement one: Unbent's own importer already has a tested ENML converter,
127
+ and a second implementation here would drift from it.
128
+
129
+ Attachments upload separately as raw bytes and are linked from the note body by their
130
+ content hash, which is how an inline image ends up in the right place.
131
+
132
+ ## Credit
133
+
134
+ The hard part — Evernote authentication, the Thrift API client, and incremental sync — is
135
+ [evernote-backup](https://github.com/vzhd1701/evernote-backup) by
136
+ [vzhd1701](https://github.com/vzhd1701), used here as a dependency rather than forked. This
137
+ package adds an uploader and a CLI on top of it and modifies none of it.
138
+
139
+ If you want ENEX files rather than a migration, use evernote-backup directly; it does that
140
+ well and this is not a replacement for it.
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,121 @@
1
+ # evernote2unbent
2
+
3
+ Move your Evernote notes into [Unbent](https://unbent.app) — notes, notebooks, tags and
4
+ attachments — without exporting an ENEX file first.
5
+
6
+ Evernote has no export API, so every other route runs through the desktop client's
7
+ File → Export, which is manual, unreliable past a few thousand notes, and produces
8
+ multi-gigabyte files you then have to feed somewhere. This syncs your account with
9
+ [evernote-backup](https://github.com/vzhd1701/evernote-backup) and uploads straight from
10
+ its local database.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ uv tool install evernote2unbent
16
+ ```
17
+
18
+ Or with pipx:
19
+
20
+ ```bash
21
+ pipx install evernote2unbent
22
+ ```
23
+
24
+ ## Use
25
+
26
+ ```bash
27
+ evernote2unbent migrate
28
+ ```
29
+
30
+ That signs you in to Evernote, syncs your account to a local database, then uploads
31
+ everything to Unbent. It asks which Unbent instance to use the first time and remembers
32
+ the answer.
33
+
34
+ **Start with one notebook** to see the result before moving everything. The sync still
35
+ fetches your whole account — it is the upload that is scoped:
36
+
37
+ ```bash
38
+ evernote2unbent migrate -n "Some Notebook"
39
+ ```
40
+
41
+ Interrupted runs are safe to repeat: the sync is incremental, and notes that already
42
+ landed are skipped rather than duplicated.
43
+
44
+ ### Doing the steps separately
45
+
46
+ `migrate` runs [evernote-backup](https://github.com/vzhd1701/evernote-backup) for you. If
47
+ you already use it, or want the ENEX files too, run the halves yourself:
48
+
49
+ ```bash
50
+ evernote-backup init-db
51
+ evernote-backup sync
52
+ evernote2unbent upload
53
+ ```
54
+
55
+ ## Re-running is safe
56
+
57
+ Unbent keys each note on a hash of its title and body, so a note that already landed is
58
+ skipped rather than duplicated. That makes an interrupted upload resumable by simply
59
+ repeating the command — and a note whose attachments failed partway gets only the missing
60
+ files on the next run.
61
+
62
+ Nothing here deletes or modifies anything in Evernote.
63
+
64
+ ## Options
65
+
66
+ | Flag | Meaning |
67
+ | --- | --- |
68
+ | `-d, --database` | The database `evernote-backup sync` wrote (default `en_backup.db`) |
69
+ | `-n, --notebook` | Only this notebook. Repeatable |
70
+ | `-t, --tag` | Only notes with this tag. Repeatable |
71
+ | `--include-trash` | Include notes from Evernote's trash |
72
+ | `--url` | Instance for one command, without storing it |
73
+
74
+ ## Signing in
75
+
76
+ `login` prints a short code and a link. Approving it in a browser you are already signed in
77
+ to grants an ordinary session — no password is typed into the terminal, and no long-lived
78
+ key is pasted from a settings page.
79
+
80
+ The token is stored in `~/.config/unbent/credentials.json` at mode `0600`, keyed by
81
+ instance so a token for one server is never sent to another. `logout` forgets it locally;
82
+ signing out everywhere in Unbent revokes it for real.
83
+
84
+ ## Which instance
85
+
86
+ There is deliberately no default. A guessed instance means a forgotten flag can send a
87
+ migration to a server you did not choose, and that failure is silent, because uploading to
88
+ the wrong Unbent still succeeds.
89
+
90
+ The answer is stored in `~/.config/unbent/config.json` — plain JSON, mode `0644`, separate
91
+ from the credentials file so changing a URL never means opening one with a token in it.
92
+
93
+ ```bash
94
+ evernote2unbent set-url https://unbent.app
95
+ evernote2unbent show-config
96
+ ```
97
+
98
+ `$UNBENT_URL` overrides it for a shell session, `--url` for a single command.
99
+
100
+ ## What gets converted
101
+
102
+ Notes arrive as Markdown. The conversion happens server-side in Unbent, which is why this
103
+ package does not implement one: Unbent's own importer already has a tested ENML converter,
104
+ and a second implementation here would drift from it.
105
+
106
+ Attachments upload separately as raw bytes and are linked from the note body by their
107
+ content hash, which is how an inline image ends up in the right place.
108
+
109
+ ## Credit
110
+
111
+ The hard part — Evernote authentication, the Thrift API client, and incremental sync — is
112
+ [evernote-backup](https://github.com/vzhd1701/evernote-backup) by
113
+ [vzhd1701](https://github.com/vzhd1701), used here as a dependency rather than forked. This
114
+ package adds an uploader and a CLI on top of it and modifies none of it.
115
+
116
+ If you want ENEX files rather than a migration, use evernote-backup directly; it does that
117
+ well and this is not a replacement for it.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "evernote2unbent"
3
+ version = "0.1.0"
4
+ description = "Move your Evernote notes into Unbent, without an ENEX file in between."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "C Deutsch" }]
9
+ keywords = ["evernote", "unbent", "enex", "migration", "export", "notes"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: End Users/Desktop",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Utilities",
17
+ ]
18
+
19
+ dependencies = [
20
+ # The Evernote sync — account auth, incremental sync, and the local SQLite database
21
+ # this reads from. Depended on rather than forked: this package adds an uploader and a
22
+ # CLI and modifies none of it, so upstream fixes arrive with a version bump.
23
+ "evernote-backup>=1.14.0",
24
+ "click>=8.1",
25
+ "requests>=2.31",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/cdeutsch/evernote2unbent"
30
+ Unbent = "https://unbent.app"
31
+ "evernote-backup" = "https://github.com/vzhd1701/evernote-backup"
32
+
33
+ [project.scripts]
34
+ evernote2unbent = "evernote2unbent.cli:main"
35
+
36
+ [dependency-groups]
37
+ test = ["pytest", "pytest-mock", "requests-mock"]
38
+ dev = ["ruff", "ty"]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/evernote2unbent"]
46
+
47
+ [tool.ruff]
48
+ # 90, matching evernote-backup, whose prose comments this code was written alongside.
49
+ line-length = 90
50
+ src = ["src", "tests"]
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
54
+
55
+ [tool.ruff.lint.per-file-ignores]
56
+ "tests/*.py" = ["S", "PL"]
@@ -0,0 +1,3 @@
1
+ """Move Evernote notes into Unbent, without an ENEX file in between."""
2
+
3
+ __version__ = "0.1.0"