elport 0.1.3__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.
- elport-0.1.3/.github/workflows/ci.yml +48 -0
- elport-0.1.3/.github/workflows/release.yml +33 -0
- elport-0.1.3/.gitignore +37 -0
- elport-0.1.3/.pre-commit-config.yaml +27 -0
- elport-0.1.3/AGENTS.md +66 -0
- elport-0.1.3/LICENSE +21 -0
- elport-0.1.3/PKG-INFO +249 -0
- elport-0.1.3/README.md +227 -0
- elport-0.1.3/README_JA.md +238 -0
- elport-0.1.3/docs/DESIGN.md +197 -0
- elport-0.1.3/docs/ELABFTW-API.md +242 -0
- elport-0.1.3/docs/adr/0001-git-like-and-gh-like-command-vocabularies.md +38 -0
- elport-0.1.3/docs/adr/0002-remote-base-from-the-patch-response.md +30 -0
- elport-0.1.3/pyproject.toml +45 -0
- elport-0.1.3/skills/elport/SKILL.md +88 -0
- elport-0.1.3/src/elport/__init__.py +1 -0
- elport-0.1.3/src/elport/browse.py +54 -0
- elport-0.1.3/src/elport/cli.py +566 -0
- elport-0.1.3/src/elport/client.py +129 -0
- elport-0.1.3/src/elport/config.py +226 -0
- elport-0.1.3/src/elport/frontmatter.py +133 -0
- elport-0.1.3/src/elport/state.py +43 -0
- elport-0.1.3/src/elport/sync.py +1160 -0
- elport-0.1.3/src/elport/transclude.py +515 -0
- elport-0.1.3/tests/_sync_harness.py +93 -0
- elport-0.1.3/tests/conftest.py +20 -0
- elport-0.1.3/tests/test_browse.py +242 -0
- elport-0.1.3/tests/test_cli.py +1159 -0
- elport-0.1.3/tests/test_client.py +229 -0
- elport-0.1.3/tests/test_config.py +426 -0
- elport-0.1.3/tests/test_state.py +18 -0
- elport-0.1.3/tests/test_sync_attachments.py +140 -0
- elport-0.1.3/tests/test_sync_comments.py +96 -0
- elport-0.1.3/tests/test_sync_fetch.py +216 -0
- elport-0.1.3/tests/test_sync_merge.py +574 -0
- elport-0.1.3/tests/test_sync_permissions.py +597 -0
- elport-0.1.3/tests/test_sync_pull.py +1104 -0
- elport-0.1.3/tests/test_sync_push.py +1232 -0
- elport-0.1.3/tests/test_sync_status.py +233 -0
- elport-0.1.3/tests/test_transclude.py +1151 -0
- elport-0.1.3/uv.lock +803 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call: # release.yml gates publishing on these checks.
|
|
8
|
+
|
|
9
|
+
# Cancel superseded runs on the same ref.
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
test:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0 # hatch-vcs derives the version from tags.
|
|
28
|
+
|
|
29
|
+
# setup-uv stopped publishing major tags at v8; pin the full version.
|
|
30
|
+
# Caching defaults to `auto`, which is what we want here.
|
|
31
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
32
|
+
with:
|
|
33
|
+
python-version: ${{ matrix.python-version }}
|
|
34
|
+
|
|
35
|
+
# --locked fails the build if uv.lock has drifted from pyproject.toml.
|
|
36
|
+
- run: uv sync --locked --extra dev
|
|
37
|
+
|
|
38
|
+
- name: ruff check
|
|
39
|
+
run: uv run ruff check
|
|
40
|
+
|
|
41
|
+
- name: ruff format
|
|
42
|
+
run: uv run ruff format --check
|
|
43
|
+
|
|
44
|
+
- name: ty check
|
|
45
|
+
run: uv run ty check src
|
|
46
|
+
|
|
47
|
+
- name: pytest
|
|
48
|
+
run: uv run pytest
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
ci:
|
|
12
|
+
uses: ./.github/workflows/ci.yml
|
|
13
|
+
|
|
14
|
+
publish:
|
|
15
|
+
needs: ci
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
environment: pypi
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
id-token: write
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0 # hatch-vcs derives the version from tags.
|
|
25
|
+
|
|
26
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
27
|
+
|
|
28
|
+
- run: uv build
|
|
29
|
+
|
|
30
|
+
- name: Check the wheel's version matches the tag
|
|
31
|
+
run: test "$(uvx --from dist/*.whl elport --version)" = "elport ${GITHUB_REF_NAME#v}"
|
|
32
|
+
|
|
33
|
+
- run: uv publish --trusted-publishing always
|
elport-0.1.3/.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# macOS
|
|
2
|
+
.DS_Store
|
|
3
|
+
.AppleDouble
|
|
4
|
+
.LSOverride
|
|
5
|
+
._*
|
|
6
|
+
.Spotlight-V100
|
|
7
|
+
.Trashes
|
|
8
|
+
|
|
9
|
+
# Windows
|
|
10
|
+
Thumbs.db
|
|
11
|
+
Thumbs.db:encryptable
|
|
12
|
+
ehthumbs.db
|
|
13
|
+
Desktop.ini
|
|
14
|
+
$RECYCLE.BIN/
|
|
15
|
+
*.lnk
|
|
16
|
+
|
|
17
|
+
# Linux
|
|
18
|
+
*~
|
|
19
|
+
.directory
|
|
20
|
+
.Trash-*
|
|
21
|
+
.fuse_hidden*
|
|
22
|
+
.nfs*
|
|
23
|
+
|
|
24
|
+
# Python
|
|
25
|
+
.venv/
|
|
26
|
+
__pycache__/
|
|
27
|
+
*.py[oc]
|
|
28
|
+
.ruff_cache/
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
build/
|
|
31
|
+
dist/
|
|
32
|
+
wheels/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
|
|
35
|
+
# Secrets / local config
|
|
36
|
+
.env
|
|
37
|
+
.elport.toml
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: ruff-check
|
|
5
|
+
name: ruff check
|
|
6
|
+
entry: uv run ruff check --fix
|
|
7
|
+
language: system
|
|
8
|
+
types_or: [python, pyi]
|
|
9
|
+
|
|
10
|
+
- id: ruff-format
|
|
11
|
+
name: ruff format
|
|
12
|
+
entry: uv run ruff format
|
|
13
|
+
language: system
|
|
14
|
+
types_or: [python, pyi]
|
|
15
|
+
|
|
16
|
+
- id: ty-check
|
|
17
|
+
name: ty check
|
|
18
|
+
entry: uv run ty check src
|
|
19
|
+
language: system
|
|
20
|
+
pass_filenames: false
|
|
21
|
+
|
|
22
|
+
- id: pytest
|
|
23
|
+
name: pytest
|
|
24
|
+
entry: uv run pytest
|
|
25
|
+
language: system
|
|
26
|
+
pass_filenames: false
|
|
27
|
+
files: ^tests/
|
elport-0.1.3/AGENTS.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# elport: project guide for AI agents
|
|
2
|
+
|
|
3
|
+
`elport` is a **git-like sync CLI for eLabFTW**. It push/pulls locally-written lab notes (Markdown/HTML + referenced
|
|
4
|
+
files) to/from eLabFTW entities. **Local is the source of truth.**
|
|
5
|
+
|
|
6
|
+
## Documentation map (what is authoritative for what)
|
|
7
|
+
|
|
8
|
+
**Authority differs by role:**
|
|
9
|
+
|
|
10
|
+
- **Behavioral contract (authoritative) = the tests** (`tests/`). The core engine (reference scan/replace, path
|
|
11
|
+
safety, reverse transclusion) and the sync semantics (push/pull/status/diff, conflict detection, the two-form
|
|
12
|
+
base) are owned solely by the tests. **To change behavior, update the tests first**, then implement. Do not
|
|
13
|
+
create prose restatements of the contract (they become a drift source).
|
|
14
|
+
- **Why it is built this way = [docs/DESIGN.md](docs/DESIGN.md)** (frozen: background, scope, settled decisions).
|
|
15
|
+
Decisions made *after* those design sessions live in **[docs/adr/](docs/adr/)** and take precedence over DESIGN
|
|
16
|
+
where they overlap; DESIGN is not edited to match, it only gains a pointer.
|
|
17
|
+
- **Measured eLabFTW API facts = [docs/ELABFTW-API.md](docs/ELABFTW-API.md)** (observation log of the external
|
|
18
|
+
world; update only when the demo's version changes).
|
|
19
|
+
- **User-facing usage = [README.md](README.md)** (commands, front matter, config).
|
|
20
|
+
- **Driving elport from an agent = [skills/elport/SKILL.md](skills/elport/SKILL.md)** (for the downstream operating
|
|
21
|
+
context).
|
|
22
|
+
|
|
23
|
+
Design principles (DESIGN §2.1): single engine / markdown-native / explicit-and-simple / secret isolation. Keep
|
|
24
|
+
added features **thin** (YAGNI); prefer read-only, single-endpoint features.
|
|
25
|
+
|
|
26
|
+
## Invariants you must not break (detailed authority is the tests, rationale is DESIGN)
|
|
27
|
+
|
|
28
|
+
- **Local is authoritative.** push is a full-body overwrite with conflict detection. Do not silently add two-way
|
|
29
|
+
auto-sync.
|
|
30
|
+
- push transclusion runs on the **outgoing copy** and **never rewrites the local original**.
|
|
31
|
+
- **base is two-form**: remote-base (the stored body returned by the push `PATCH`, not the sent bytes;
|
|
32
|
+
[ADR-0002](docs/adr/0002-remote-base-from-the-patch-response.md)) and local-base
|
|
33
|
+
(the local original at push/pull time). **Always compare same-form** (remote↔remote-base, local↔local-base). With
|
|
34
|
+
only one form, server normalization (non-convergent) yields a false conflict / permanent-dirty every time. See
|
|
35
|
+
the "Body normalization" section of ELABFTW-API.
|
|
36
|
+
- **Do not embed out-of-sync data (comments, steps, revision bodies) in the body.** Use a sidecar or terminal
|
|
37
|
+
output (do not pollute the source).
|
|
38
|
+
- Upload targets are **only files under the document directory.** Exclude schemed URLs, absolute paths, `..`
|
|
39
|
+
escapes, and **hidden files/dirs (any dot-prefixed path component, e.g. `.env`, `.git/config`)**. A pulled
|
|
40
|
+
remote body must not make the next push exfiltrate local secrets; **never parse inside code fences, inline code,
|
|
41
|
+
or HTML comments.**
|
|
42
|
+
- Attachment identity uses the **server-returned hash first, else basename + size** (same-name entries can coexist,
|
|
43
|
+
so check against all of them). **Never auto-delete attachments during push.**
|
|
44
|
+
- **Keep secrets (API keys) out of the project tree.** OS keyring + home-dir config only. **Never log the key.**
|
|
45
|
+
|
|
46
|
+
## Non-obvious eLabFTW API traps (highlights; details in ELABFTW-API.md)
|
|
47
|
+
|
|
48
|
+
- **`content_type:2` (markdown) is settable via PATCH and respected by recent eLabFTW** (the create-time body bug
|
|
49
|
+
#6416 is fixed; very old instances may ignore it). **Verify `content_type == 2` after push** and abort otherwise.
|
|
50
|
+
- **`download.php?...` URLs use browser Cookie-session auth** and **do not work with the API key.** Embed them in
|
|
51
|
+
the body for humans, but the tool's own download (pull) uses the API
|
|
52
|
+
`GET /api/v2/{entity}/{id}/uploads/{id}?format=binary`.
|
|
53
|
+
- An upload's `long_name` **contains `/`**. When building download-URL queries, **percent-encode is mandatory.**
|
|
54
|
+
- **The body is normalized on every save and re-sending the normalized form does not converge** (even in md mode).
|
|
55
|
+
**HTML-unescape** URLs in the body before parsing them.
|
|
56
|
+
- **Revisions are readable** (`GET .../revisions[/{id}]`), but nothing ties a version to "my last push." Keep them
|
|
57
|
+
**read-only**; do not use them to auto-restore the 3-way merge ancestor base.
|
|
58
|
+
|
|
59
|
+
## Testing and verification
|
|
60
|
+
|
|
61
|
+
- The core engine (reference scan/replace, path safety, reverse transclusion) and the sync semantics are
|
|
62
|
+
unit-tested **without network** (the API layer is mocked). These tests are the authority for behavior, so
|
|
63
|
+
**changes go test-first.**
|
|
64
|
+
- When live API behavior needs confirming, use **only the public demo <https://demo.elabftw.net>** (public,
|
|
65
|
+
periodically reset, disposable). Never use any other instance. Record the demo version a fact was measured on;
|
|
66
|
+
version-dependent behavior is confirmed on the demo's current version only.
|
elport-0.1.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 youthesame
|
|
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.
|
elport-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: elport
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Git-like sync CLI for eLabFTW (local-first push/pull of Markdown/HTML lab notes)
|
|
5
|
+
Project-URL: Repository, https://github.com/youthesame/elport
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: keyring>=25
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Requires-Dist: requests>=2.31
|
|
12
|
+
Requires-Dist: tomli-w>=1.0
|
|
13
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pre-commit>=4.6.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-xdist>=3.8; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.16; extra == 'dev'
|
|
19
|
+
Requires-Dist: tomli>=2.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ty>=0.0.69; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# elport: a git-like sync CLI for eLabFTW
|
|
24
|
+
|
|
25
|
+
[English](https://github.com/youthesame/elport/blob/main/README.md) | [日本語](https://github.com/youthesame/elport/blob/main/README_JA.md)
|
|
26
|
+
|
|
27
|
+
Write lab notes locally in Markdown, with inline HTML like `<figure>` where you want it. Then `push`/`pull` them,
|
|
28
|
+
along with every figure and data file the body references, to eLabFTW entities. **Local is the source of truth.**
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Python 3.10+, installed as a `uv` tool (provides the `elport` command):
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
uv tool install elport # upgrade: uv tool upgrade elport
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or with pip:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pip install elport # upgrade: pip install -U elport
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## The core idea
|
|
45
|
+
|
|
46
|
+
One local file maps to one eLabFTW entry: Markdown with a YAML front matter block, inline HTML allowed.
|
|
47
|
+
|
|
48
|
+
On `push`, elport uploads every real local file the body references, in any notation, and swaps each path for its
|
|
49
|
+
real eLabFTW URL. The body goes up as raw Markdown, so `<figure>` and `$...$` render as-is. To link another entry
|
|
50
|
+
instead of uploading, write its eLabFTW URL. Code fences, inline code, and HTML comments are never parsed.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
elport login labA # store base_url (config) + api_key (OS keyring), interactively
|
|
56
|
+
elport list # what do I already have on the server? (read-only)
|
|
57
|
+
elport new "CRISPR titration" # create the entity, scaffold report.md with id
|
|
58
|
+
# ...or start from an entity made in the Web UI: elport clone <id>
|
|
59
|
+
# ...edit report.md, drop fig1.png / data.csv next to it...
|
|
60
|
+
elport status # what would sync? (read-only)
|
|
61
|
+
elport push # upload references + push the body
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
Commands that act on a local document take an optional `[<doc>]`, defaulting to `report.md`. Any name works.
|
|
67
|
+
Exit code `0` on success, `1` on failure. Run `elport <command> --help` for the full flags.
|
|
68
|
+
|
|
69
|
+
### Sync
|
|
70
|
+
|
|
71
|
+
| Command | Summary |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `push` | Upload the body and its referenced files to one entity. |
|
|
74
|
+
| `pull` | Fetch the body and metadata, and download referenced files. |
|
|
75
|
+
| `fetch` | Download every attachment, referenced or not. Read-only. |
|
|
76
|
+
| `status` | Report what would sync, and in which mode. Read-only. |
|
|
77
|
+
| `diff` | Diff local against remote, or against the last push with `--base`. |
|
|
78
|
+
| `merge` | Merge `.base.md` and `.remote.md` into `<doc>` after a conflict. Requires `git`. |
|
|
79
|
+
|
|
80
|
+
`push` creates the entity and writes `id` back to the front matter when `id` is unset, and it runs the mode and
|
|
81
|
+
conflict checks first. `merge` is local-only: it runs `git merge-file`, so `git` must be installed, but the folder
|
|
82
|
+
need not be a git repository.
|
|
83
|
+
|
|
84
|
+
### Entities
|
|
85
|
+
|
|
86
|
+
| Command | Summary |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `new "<title>"` | Create an entity and scaffold front matter. |
|
|
89
|
+
| `clone <id>` | Start `<doc>` from an entity that already exists, then pull. |
|
|
90
|
+
| `list` | List remote entities as `id date title`. Read-only. |
|
|
91
|
+
| `view <id>` | Print one remote body to stdout, exactly as stored. |
|
|
92
|
+
| `comments` | Print the remote comment thread. |
|
|
93
|
+
| `comment "<text>"` | Post one comment. Use the Web UI to edit or delete. |
|
|
94
|
+
|
|
95
|
+
`new` and `clone` write to `report.md` unless you pass `-o <doc>`. `list` defaults to your own entities
|
|
96
|
+
(`--scope self`) and returns 50 at a time, with `-q <term>`, `--limit`, `--offset`, and `--json` to narrow it.
|
|
97
|
+
`comments` prints to the terminal only, never into the body.
|
|
98
|
+
|
|
99
|
+
### Setup
|
|
100
|
+
|
|
101
|
+
| Command | Summary |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `login [<profile>]` | Store base_url in `config.toml`, api_key in the OS keyring. |
|
|
104
|
+
| `logout [<profile>]` | Remove the stored api_key. Keeps base_url. |
|
|
105
|
+
| `profile [use <name>]` | List profiles, or set the default one. |
|
|
106
|
+
| `whoami` | Check auth: user, team, role, key permissions, server version, scopes. |
|
|
107
|
+
|
|
108
|
+
### Shared options
|
|
109
|
+
|
|
110
|
+
- `-n/--dry-run` rehearses a push without sending.
|
|
111
|
+
- `-f/--force` pushes over a changed remote, losing the Web-side change.
|
|
112
|
+
- `-y/--yes` skips the two confirmations: widening `read`/`write` beyond your team, and new uploads over 25 MiB.
|
|
113
|
+
A non-interactive run needs `-y` for either.
|
|
114
|
+
- `--profile <name>` and `--entity {experiments,items}` pick the destination.
|
|
115
|
+
|
|
116
|
+
### pull and fetch
|
|
117
|
+
|
|
118
|
+
Neither command is a superset of the other.
|
|
119
|
+
|
|
120
|
+
- `pull` writes referenced files back by **basename only**. A subdirectory path like `assets/fig.png` lands as
|
|
121
|
+
`fig.png`.
|
|
122
|
+
- `pull` only downloads files the body links to. Attachments that sit on the entity without being embedded, like
|
|
123
|
+
raw data or spectra, stay on the server.
|
|
124
|
+
- `fetch` brings those down. It never parses the body, touches the base, or feeds the push manifest, so a fetched
|
|
125
|
+
file is not uploaded again unless you link it in the body yourself.
|
|
126
|
+
- If a local file differs, `fetch` leaves it in place and writes the remote copy beside it as `<name>.remote`.
|
|
127
|
+
|
|
128
|
+
## Document format (front matter)
|
|
129
|
+
|
|
130
|
+
A YAML block at the top, generated or completed on push if absent:
|
|
131
|
+
|
|
132
|
+
```markdown
|
|
133
|
+
---
|
|
134
|
+
id: 42 # entity ID (auto-filled on creation)
|
|
135
|
+
entity: experiments # experiments | items (default experiments)
|
|
136
|
+
title: "experiment title" # optional; defaults to the file name on creation
|
|
137
|
+
tags: [CRISPR, PCR] # optional; add-only (remove tags in the Web UI)
|
|
138
|
+
category: Molecular Biology # optional; ID or existing category name (elport never creates categories)
|
|
139
|
+
status: Running # optional; ID or existing status name (elport never creates statuses)
|
|
140
|
+
profile: labA # optional; destination profile
|
|
141
|
+
read: team # optional; owner | owner+admin | team | account | public
|
|
142
|
+
write: owner # optional; same scale
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
# Body. Markdown, inline HTML allowed ...
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The block holds only `id`, human metadata, and an optional `profile`. The base and the hashes live in state. elport
|
|
149
|
+
strips the front matter before sending the body.
|
|
150
|
+
|
|
151
|
+
`title`, `category`, `status`, `read`, and `write` are reflected **only when present**. Omit a key and elport leaves
|
|
152
|
+
that remote value untouched. `pull` writes them back from the server, so a key you never wrote may appear after a
|
|
153
|
+
pull, while a key you edited locally but have not pushed is kept. `tags` are add-only on both sides, so pull keeps
|
|
154
|
+
the union.
|
|
155
|
+
|
|
156
|
+
`read`/`write` set the eLabFTW base visibility only, so individual Web-UI grants survive. Widening beyond your team
|
|
157
|
+
asks for confirmation.
|
|
158
|
+
|
|
159
|
+
If the front matter and the CLI disagree on profile, entity, or id, elport **stops** rather than guessing.
|
|
160
|
+
|
|
161
|
+
## Conflicts
|
|
162
|
+
|
|
163
|
+
Since you may also edit the body in the Web UI, `push` compares the current remote against the stored base first.
|
|
164
|
+
|
|
165
|
+
- **unchanged** proceeds.
|
|
166
|
+
- **changed** aborts. elport writes `<name>.base.md` (the ancestor) and `<name>.remote.md`. Run `elport merge` to
|
|
167
|
+
3-way them into your file, resolve any `<<<<<<<`/`>>>>>>>` markers, then push. A body that still has markers is
|
|
168
|
+
refused.
|
|
169
|
+
- **no base on this machine** aborts. Run `elport pull` first, or `--force` to overwrite blind.
|
|
170
|
+
|
|
171
|
+
`--force` discards the Web-side change, so use it deliberately. eLabFTW keeps server-side history recoverable from
|
|
172
|
+
the Web UI as a safety net, but elport keeps no local history of its own. Want per-edit history? `git init` your
|
|
173
|
+
notes folder, since the files are plain Markdown.
|
|
174
|
+
|
|
175
|
+
## Configuration & auth
|
|
176
|
+
|
|
177
|
+
**No credentials in the project.**
|
|
178
|
+
|
|
179
|
+
### Where the key lives
|
|
180
|
+
|
|
181
|
+
`elport login` stores the API key in your OS keyring: Keychain, Credential Manager, or Secret Service. Everything
|
|
182
|
+
that is not a secret, base_url included, goes in `~/.config/elport/config.toml` with mode `600`. elport never
|
|
183
|
+
prints the key.
|
|
184
|
+
|
|
185
|
+
```toml
|
|
186
|
+
# ~/.config/elport/config.toml
|
|
187
|
+
default_profile = "labA"
|
|
188
|
+
|
|
189
|
+
[profiles.labA]
|
|
190
|
+
base_url = "https://lab-a.example.org"
|
|
191
|
+
verify_ssl = true
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Which credentials win
|
|
195
|
+
|
|
196
|
+
elport checks three sources, in order, and stops at the first hit.
|
|
197
|
+
|
|
198
|
+
1. `ELABFTW_BASE_URL` + `ELABFTW_API_KEY` in the environment. This is how CI authenticates.
|
|
199
|
+
2. The keyring and config pair from `elport login`. The normal default.
|
|
200
|
+
3. A plaintext key in `~/.config/elport/config.toml`. A last resort, used with a warning, and only when no keyring backend exists (e.g. WSL, headless servers).
|
|
201
|
+
|
|
202
|
+
### Profiles
|
|
203
|
+
|
|
204
|
+
Profiles layer like settings.json, one per team:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
~/.config/elport/config.toml → <project>/.elport.toml → <dir>/.elport.toml
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Your first `elport login` becomes the default. `elport profile use <name>` switches it, or set `profile:` in a
|
|
211
|
+
note's front matter to pin that one file.
|
|
212
|
+
|
|
213
|
+
### .elportignore
|
|
214
|
+
|
|
215
|
+
`.elportignore` excludes referenced files, `.gitignore`-style, so they are never uploaded. It is additive across
|
|
216
|
+
the same layers as profiles, plus every `.elportignore` from the project root down to the document directory. Each
|
|
217
|
+
pattern is anchored at its own directory.
|
|
218
|
+
|
|
219
|
+
## Learn more
|
|
220
|
+
|
|
221
|
+
- **Why it works this way** → [docs/DESIGN.md](https://github.com/youthesame/elport/blob/main/docs/DESIGN.md)
|
|
222
|
+
- **How eLabFTW's API actually behaves** → [docs/ELABFTW-API.md](https://github.com/youthesame/elport/blob/main/docs/ELABFTW-API.md)
|
|
223
|
+
- **The behavioral contract** → the test suite (`tests/`) is authoritative
|
|
224
|
+
- **Driving elport from an AI agent** → [skills/elport/SKILL.md](https://github.com/youthesame/elport/blob/main/skills/elport/SKILL.md)
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
| Module | Role |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `cli.py` | Argument parsing and dispatch |
|
|
231
|
+
| `sync.py` | push, pull, status, diff |
|
|
232
|
+
| `transclude.py` | Path ↔ URL rewriting, both directions |
|
|
233
|
+
| `client.py` | eLabFTW API wrapper |
|
|
234
|
+
| `state.py` | The stored base and its hashes |
|
|
235
|
+
| `config.py` | Profiles and credential resolution |
|
|
236
|
+
| `frontmatter.py` | The YAML block |
|
|
237
|
+
| `browse.py` | list and view |
|
|
238
|
+
|
|
239
|
+
**The tests are the authoritative behavioral contract, so change tests first.** You can check live API behavior
|
|
240
|
+
against <https://demo.elabftw.net>.
|
|
241
|
+
|
|
242
|
+
## Acknowledgments
|
|
243
|
+
|
|
244
|
+
- [elab-doc-sync](https://github.com/Kosaku-Noba/elab-doc-sync)
|
|
245
|
+
- [elAPI](https://github.com/uhd-urz/elAPI)
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|