ariadne-x 0.5.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 (45) hide show
  1. ariadne_x-0.5.0/.github/workflows/ci.yml +29 -0
  2. ariadne_x-0.5.0/.github/workflows/publish.yml +47 -0
  3. ariadne_x-0.5.0/.gitignore +13 -0
  4. ariadne_x-0.5.0/LICENSE +22 -0
  5. ariadne_x-0.5.0/PKG-INFO +216 -0
  6. ariadne_x-0.5.0/README.md +185 -0
  7. ariadne_x-0.5.0/RELEASING.md +24 -0
  8. ariadne_x-0.5.0/docs/API.md +216 -0
  9. ariadne_x-0.5.0/docs/DUMPS.md +144 -0
  10. ariadne_x-0.5.0/docs/HYPERPLEX.md +39 -0
  11. ariadne_x-0.5.0/docs/RAFT.md +34 -0
  12. ariadne_x-0.5.0/docs/SCHEMA.md +75 -0
  13. ariadne_x-0.5.0/docs/SOURCES.md +73 -0
  14. ariadne_x-0.5.0/examples/fixture_archive/data/account.js +10 -0
  15. ariadne_x-0.5.0/examples/fixture_archive/data/tweets.js +19 -0
  16. ariadne_x-0.5.0/examples/generic.jsonl +3 -0
  17. ariadne_x-0.5.0/pyproject.toml +68 -0
  18. ariadne_x-0.5.0/src/ariadne/__init__.py +89 -0
  19. ariadne_x-0.5.0/src/ariadne/__main__.py +6 -0
  20. ariadne_x-0.5.0/src/ariadne/_types.py +9 -0
  21. ariadne_x-0.5.0/src/ariadne/api.py +1001 -0
  22. ariadne_x-0.5.0/src/ariadne/archive.py +247 -0
  23. ariadne_x-0.5.0/src/ariadne/bluesky.py +196 -0
  24. ariadne_x-0.5.0/src/ariadne/cli.py +987 -0
  25. ariadne_x-0.5.0/src/ariadne/dumps.py +1182 -0
  26. ariadne_x-0.5.0/src/ariadne/errors.py +41 -0
  27. ariadne_x-0.5.0/src/ariadne/fetch.py +365 -0
  28. ariadne_x-0.5.0/src/ariadne/hx.py +169 -0
  29. ariadne_x-0.5.0/src/ariadne/ids.py +66 -0
  30. ariadne_x-0.5.0/src/ariadne/models.py +155 -0
  31. ariadne_x-0.5.0/src/ariadne/providers.py +284 -0
  32. ariadne_x-0.5.0/src/ariadne/py.typed +0 -0
  33. ariadne_x-0.5.0/src/ariadne/reconstruct.py +198 -0
  34. ariadne_x-0.5.0/src/ariadne/render.py +238 -0
  35. ariadne_x-0.5.0/src/ariadne/sources.py +254 -0
  36. ariadne_x-0.5.0/src/ariadne/store.py +126 -0
  37. ariadne_x-0.5.0/src/ariadne/timeutil.py +65 -0
  38. ariadne_x-0.5.0/src/ariadne/unofficial.py +204 -0
  39. ariadne_x-0.5.0/tests/conftest.py +14 -0
  40. ariadne_x-0.5.0/tests/test_api.py +269 -0
  41. ariadne_x-0.5.0/tests/test_ariadne.py +514 -0
  42. ariadne_x-0.5.0/tests/test_dumps.py +627 -0
  43. ariadne_x-0.5.0/tests/test_dumps_cli.py +242 -0
  44. ariadne_x-0.5.0/tests/test_sources.py +188 -0
  45. ariadne_x-0.5.0/uv.lock +210 -0
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+
22
+ - name: Install package with dev dependencies
23
+ run: python -m pip install -e ".[dev]"
24
+
25
+ - name: Lint
26
+ run: ruff check .
27
+
28
+ - name: Test
29
+ run: pytest
@@ -0,0 +1,47 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.x"
16
+
17
+ - name: Install package with dev dependencies
18
+ run: python -m pip install -e ".[dev]"
19
+
20
+ - name: Lint
21
+ run: ruff check .
22
+
23
+ - name: Test
24
+ run: pytest
25
+
26
+ publish:
27
+ needs: test
28
+ runs-on: ubuntu-latest
29
+ environment: pypi
30
+ permissions:
31
+ contents: read
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.x"
39
+
40
+ - name: Install build tools
41
+ run: python -m pip install --upgrade build
42
+
43
+ - name: Build package
44
+ run: python -m build
45
+
46
+ - name: Publish to PyPI
47
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ .tweet-threader-cache.json
2
+ .ariadne-cache.json
3
+ nic
4
+ .venv/
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ dist/
11
+ build/
12
+
13
+ .ruff_cache/
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Claudio Brandolino
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.
22
+
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.5
2
+ Name: ariadne-x
3
+ Version: 0.5.0
4
+ Summary: Reconstruct reply branches from X/Twitter archives or IDs and render them as LLM chat inputs.
5
+ Project-URL: Homepage, https://ariadne.hyperplex.org
6
+ Project-URL: Repository, https://github.com/lumpenspace/ariadne
7
+ Project-URL: Issues, https://github.com/lumpenspace/ariadne/issues
8
+ Author: Claudio Brandolino
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Text Processing
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: rich>=13.7
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2; extra == 'dev'
25
+ Requires-Dist: duckdb>=1.1; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.8; extra == 'dev'
28
+ Provides-Extra: parquet
29
+ Requires-Dist: duckdb>=1.1; extra == 'parquet'
30
+ Description-Content-Type: text/markdown
31
+
32
+ <p align="center">
33
+ <img src="site/public/ariadne-thread-v2.png" alt="A golden conversation thread crossing several archives" width="100%">
34
+ </p>
35
+
36
+ <h1 align="center">⌇ ariadne</h1>
37
+
38
+ <p align="center">
39
+ <strong>Find the conversation.</strong><br>
40
+ Turn scattered X/Twitter archives and tweet datasets into readable, attributable reply branches.
41
+ </p>
42
+
43
+ <p align="center">
44
+ <a href="https://ariadne.hyperplex.org">documentation</a> ·
45
+ <a href="https://pypi.org/project/ariadne-x/">PyPI</a> ·
46
+ <a href="docs/API.md">Python API</a> ·
47
+ <a href="LICENSE">MIT</a>
48
+ </p>
49
+
50
+ ---
51
+
52
+ A social export remembers posts. The conversation around them is often somewhere
53
+ else: a parent in another archive, a quote in a community dataset, an older post in
54
+ the cache.
55
+
56
+ Ariadne merges those sources, selects the posts you care about, follows every known
57
+ reply-parent chain toward its root, attaches quote context, and renders the result
58
+ root → target.
59
+
60
+ It does not pretend sparse data is complete. Missing posts stay visible as
61
+ placeholders and warnings unless you ask for `--strict`.
62
+
63
+ ## Start here
64
+
65
+ Requires Python 3.11 or newer. The distribution is `ariadne-x`; the command and
66
+ import are both `ariadne`.
67
+
68
+ ```bash
69
+ uv tool install ariadne-x
70
+ ariadne interactive
71
+ ```
72
+
73
+ Or build directly from a personal archive:
74
+
75
+ ```bash
76
+ ariadne build \
77
+ --archive ~/Downloads/twitter-archive.zip \
78
+ --for-user alice \
79
+ --since 2024-01-01 \
80
+ --format markdown \
81
+ --output conversations.md
82
+ ```
83
+
84
+ That is the whole basic loop:
85
+
86
+ ```text
87
+ archives + dumps + cache
88
+ ↓
89
+ choose targets
90
+ ↓
91
+ follow known parent IDs
92
+ ↓
93
+ quotes + root-to-target branches
94
+ ```
95
+
96
+ ## Choose your path
97
+
98
+ | You have… | Use… |
99
+ | --- | --- |
100
+ | One X/Twitter export | `ariadne build --archive PATH …` |
101
+ | CSV, JSON, JSONL, or NDJSON | `ariadne build --tweets-file PATH …` |
102
+ | Tweet IDs or X URLs | Pass them after `ariadne build` |
103
+ | Archives you will reuse | `ariadne dumps import PATH` |
104
+ | A public X account | `ariadne build --target-user USER …` |
105
+ | A Bluesky handle | `ariadne bluesky HANDLE …` |
106
+
107
+ Imported archives form a local, searchable library:
108
+
109
+ ```bash
110
+ ariadne dumps import ~/Downloads/twitter-archive.zip --name personal
111
+ ariadne dumps search "remembered phrase" --user alice
112
+ ariadne dumps show https://x.com/alice/status/1234567890123456789
113
+
114
+ # Imported dumps join ordinary builds automatically.
115
+ ariadne build --for-user alice --since 2024-01-01 --format raft -o alice.jsonl
116
+ ```
117
+
118
+ Each import becomes a self-contained SQLite database under `~/.ariadne/dumps`.
119
+ The source is never modified, and removing an import never removes the source.
120
+ Parquet imports additionally need DuckDB:
121
+
122
+ ```bash
123
+ uv tool install 'ariadne-x[parquet]'
124
+ ```
125
+
126
+ [Read the archive library guide →](docs/DUMPS.md)
127
+
128
+ ## From Python
129
+
130
+ The CLI is a thin front end over a typed synchronous API:
131
+
132
+ ```python
133
+ from pathlib import Path
134
+ import ariadne
135
+
136
+ options = ariadne.BuildOptions(
137
+ archive=Path.home() / "Downloads" / "twitter-archive.zip",
138
+ for_user="alice",
139
+ since="2024-01-01",
140
+ )
141
+
142
+ result = ariadne.build(options)
143
+
144
+ for conversation in result:
145
+ print(conversation.target_id)
146
+
147
+ documents = result.raft_documents()
148
+ result.save("out/branches.jsonl", "raft")
149
+ ```
150
+
151
+ Use `no_dumps=True` when a build must ignore the persistent archive library.
152
+ Named failures derive from `AriadneError`, including `ConfigurationError`,
153
+ `NoTargetsError`, `ReconstructionError`, and `SourceError`.
154
+
155
+ [Read the Python API reference →](docs/API.md)
156
+
157
+ ## Pick an output
158
+
159
+ | Format | Shape | Good for |
160
+ | --- | --- | --- |
161
+ | `messages` | enriched JSON conversations | chat-like data with tweet metadata; the CLI default |
162
+ | `openai` | reduced JSON conversations | nested `role`, `name`, and `content` messages |
163
+ | `json` | normalized graph + tweets | analysis, provenance, and custom rendering |
164
+ | `markdown` | text | humans, notebooks, and review |
165
+ | `raft` | one JSON object per line | retrieval, chunking, and embedding |
166
+
167
+ The `openai` renderer keeps Ariadne's conversation envelope; consumers extract
168
+ `conversations[i].messages`. Role names describe position in the branch, not the
169
+ speaker's intent.
170
+
171
+ [Inspect the schemas →](docs/SCHEMA.md)
172
+
173
+ ## What Ariadne follows
174
+
175
+ - One target's ancestor path back to its root—not sibling replies or a whole tree.
176
+ - Older parents even when `--since` limits the starting targets.
177
+ - Reply and quote edges across different imported dumps.
178
+ - Quote context, with root quote-tweets spliced onto their quoted post by default.
179
+
180
+ Ordinary archive builds stay local. `--target-user` is the convenience exception: it
181
+ tries unofficial RSS and oEmbed unless disabled. Those sources can recover recent text
182
+ but usually cannot prove reply edges. X API reads are separately opt-in through
183
+ `--fetch` and `--fetch-user-timeline` and may be billable.
184
+
185
+ [Read the source and network policy →](docs/SOURCES.md)
186
+
187
+ ## A few useful commands
188
+
189
+ ```bash
190
+ ariadne inspect-archive ~/Downloads/twitter-archive.zip
191
+ ariadne dumps interactive
192
+ ariadne build --help
193
+
194
+ # Bluesky uses its public API and the same renderers.
195
+ ariadne bluesky alice.bsky.social --since 2024-01-01 --format raft
196
+ ```
197
+
198
+ ## Reference
199
+
200
+ - [Documentation site](https://ariadne.hyperplex.org)
201
+ - [Persistent archive library](docs/DUMPS.md)
202
+ - [Python API](docs/API.md)
203
+ - [Source behavior](docs/SOURCES.md)
204
+ - [Output schemas](docs/SCHEMA.md)
205
+ - [Raft handoff](docs/RAFT.md)
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ uv sync --extra dev --extra parquet
211
+ uv run pytest
212
+ uv run ruff check .
213
+ uv run mypy src
214
+ ```
215
+
216
+ MIT licensed. The thread was there all along.
@@ -0,0 +1,185 @@
1
+ <p align="center">
2
+ <img src="site/public/ariadne-thread-v2.png" alt="A golden conversation thread crossing several archives" width="100%">
3
+ </p>
4
+
5
+ <h1 align="center">⌇ ariadne</h1>
6
+
7
+ <p align="center">
8
+ <strong>Find the conversation.</strong><br>
9
+ Turn scattered X/Twitter archives and tweet datasets into readable, attributable reply branches.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://ariadne.hyperplex.org">documentation</a> ·
14
+ <a href="https://pypi.org/project/ariadne-x/">PyPI</a> ·
15
+ <a href="docs/API.md">Python API</a> ·
16
+ <a href="LICENSE">MIT</a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ A social export remembers posts. The conversation around them is often somewhere
22
+ else: a parent in another archive, a quote in a community dataset, an older post in
23
+ the cache.
24
+
25
+ Ariadne merges those sources, selects the posts you care about, follows every known
26
+ reply-parent chain toward its root, attaches quote context, and renders the result
27
+ root → target.
28
+
29
+ It does not pretend sparse data is complete. Missing posts stay visible as
30
+ placeholders and warnings unless you ask for `--strict`.
31
+
32
+ ## Start here
33
+
34
+ Requires Python 3.11 or newer. The distribution is `ariadne-x`; the command and
35
+ import are both `ariadne`.
36
+
37
+ ```bash
38
+ uv tool install ariadne-x
39
+ ariadne interactive
40
+ ```
41
+
42
+ Or build directly from a personal archive:
43
+
44
+ ```bash
45
+ ariadne build \
46
+ --archive ~/Downloads/twitter-archive.zip \
47
+ --for-user alice \
48
+ --since 2024-01-01 \
49
+ --format markdown \
50
+ --output conversations.md
51
+ ```
52
+
53
+ That is the whole basic loop:
54
+
55
+ ```text
56
+ archives + dumps + cache
57
+ ↓
58
+ choose targets
59
+ ↓
60
+ follow known parent IDs
61
+ ↓
62
+ quotes + root-to-target branches
63
+ ```
64
+
65
+ ## Choose your path
66
+
67
+ | You have… | Use… |
68
+ | --- | --- |
69
+ | One X/Twitter export | `ariadne build --archive PATH …` |
70
+ | CSV, JSON, JSONL, or NDJSON | `ariadne build --tweets-file PATH …` |
71
+ | Tweet IDs or X URLs | Pass them after `ariadne build` |
72
+ | Archives you will reuse | `ariadne dumps import PATH` |
73
+ | A public X account | `ariadne build --target-user USER …` |
74
+ | A Bluesky handle | `ariadne bluesky HANDLE …` |
75
+
76
+ Imported archives form a local, searchable library:
77
+
78
+ ```bash
79
+ ariadne dumps import ~/Downloads/twitter-archive.zip --name personal
80
+ ariadne dumps search "remembered phrase" --user alice
81
+ ariadne dumps show https://x.com/alice/status/1234567890123456789
82
+
83
+ # Imported dumps join ordinary builds automatically.
84
+ ariadne build --for-user alice --since 2024-01-01 --format raft -o alice.jsonl
85
+ ```
86
+
87
+ Each import becomes a self-contained SQLite database under `~/.ariadne/dumps`.
88
+ The source is never modified, and removing an import never removes the source.
89
+ Parquet imports additionally need DuckDB:
90
+
91
+ ```bash
92
+ uv tool install 'ariadne-x[parquet]'
93
+ ```
94
+
95
+ [Read the archive library guide →](docs/DUMPS.md)
96
+
97
+ ## From Python
98
+
99
+ The CLI is a thin front end over a typed synchronous API:
100
+
101
+ ```python
102
+ from pathlib import Path
103
+ import ariadne
104
+
105
+ options = ariadne.BuildOptions(
106
+ archive=Path.home() / "Downloads" / "twitter-archive.zip",
107
+ for_user="alice",
108
+ since="2024-01-01",
109
+ )
110
+
111
+ result = ariadne.build(options)
112
+
113
+ for conversation in result:
114
+ print(conversation.target_id)
115
+
116
+ documents = result.raft_documents()
117
+ result.save("out/branches.jsonl", "raft")
118
+ ```
119
+
120
+ Use `no_dumps=True` when a build must ignore the persistent archive library.
121
+ Named failures derive from `AriadneError`, including `ConfigurationError`,
122
+ `NoTargetsError`, `ReconstructionError`, and `SourceError`.
123
+
124
+ [Read the Python API reference →](docs/API.md)
125
+
126
+ ## Pick an output
127
+
128
+ | Format | Shape | Good for |
129
+ | --- | --- | --- |
130
+ | `messages` | enriched JSON conversations | chat-like data with tweet metadata; the CLI default |
131
+ | `openai` | reduced JSON conversations | nested `role`, `name`, and `content` messages |
132
+ | `json` | normalized graph + tweets | analysis, provenance, and custom rendering |
133
+ | `markdown` | text | humans, notebooks, and review |
134
+ | `raft` | one JSON object per line | retrieval, chunking, and embedding |
135
+
136
+ The `openai` renderer keeps Ariadne's conversation envelope; consumers extract
137
+ `conversations[i].messages`. Role names describe position in the branch, not the
138
+ speaker's intent.
139
+
140
+ [Inspect the schemas →](docs/SCHEMA.md)
141
+
142
+ ## What Ariadne follows
143
+
144
+ - One target's ancestor path back to its root—not sibling replies or a whole tree.
145
+ - Older parents even when `--since` limits the starting targets.
146
+ - Reply and quote edges across different imported dumps.
147
+ - Quote context, with root quote-tweets spliced onto their quoted post by default.
148
+
149
+ Ordinary archive builds stay local. `--target-user` is the convenience exception: it
150
+ tries unofficial RSS and oEmbed unless disabled. Those sources can recover recent text
151
+ but usually cannot prove reply edges. X API reads are separately opt-in through
152
+ `--fetch` and `--fetch-user-timeline` and may be billable.
153
+
154
+ [Read the source and network policy →](docs/SOURCES.md)
155
+
156
+ ## A few useful commands
157
+
158
+ ```bash
159
+ ariadne inspect-archive ~/Downloads/twitter-archive.zip
160
+ ariadne dumps interactive
161
+ ariadne build --help
162
+
163
+ # Bluesky uses its public API and the same renderers.
164
+ ariadne bluesky alice.bsky.social --since 2024-01-01 --format raft
165
+ ```
166
+
167
+ ## Reference
168
+
169
+ - [Documentation site](https://ariadne.hyperplex.org)
170
+ - [Persistent archive library](docs/DUMPS.md)
171
+ - [Python API](docs/API.md)
172
+ - [Source behavior](docs/SOURCES.md)
173
+ - [Output schemas](docs/SCHEMA.md)
174
+ - [Raft handoff](docs/RAFT.md)
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ uv sync --extra dev --extra parquet
180
+ uv run pytest
181
+ uv run ruff check .
182
+ uv run mypy src
183
+ ```
184
+
185
+ MIT licensed. The thread was there all along.
@@ -0,0 +1,24 @@
1
+ # Releasing
2
+
3
+ The PyPI distribution is `ariadne-x`; the Python import package and CLI
4
+ command remain `ariadne`. The package version is sourced from `__version__` in
5
+ `src/ariadne/__init__.py`, which Hatch reads through `pyproject.toml`.
6
+
7
+ ## GitHub Release → PyPI Trusted Publishing
8
+
9
+ The one-time pending trusted publisher on PyPI uses:
10
+
11
+ - Project: `ariadne-x`
12
+ - Owner/repository: `lumpenspace/ariadne`
13
+ - Workflow: `publish.yml`
14
+ - Environment: `pypi`
15
+
16
+ For each release:
17
+
18
+ 1. Bump `__version__` in `src/ariadne/__init__.py` and run `uv lock`.
19
+ 2. Merge the release commit to `main` and wait for CI.
20
+ 3. Publish a GitHub Release with a matching `vX.Y.Z` tag.
21
+
22
+ `.github/workflows/publish.yml` runs lint and tests, builds the sdist and wheel,
23
+ then uploads them to PyPI through OIDC. No PyPI password or API token is stored
24
+ in GitHub.