gistfs 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,32 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ *.egg
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ *.whl
10
+
11
+ # Virtual environments
12
+ .venv/
13
+ venv/
14
+
15
+ # Environment
16
+ .env
17
+
18
+ # IDE
19
+ .idea/
20
+ .vscode/
21
+ *.swp
22
+
23
+ # Testing
24
+ .pytest_cache/
25
+ .coverage
26
+ htmlcov/
27
+
28
+ # macOS
29
+ .DS_Store
30
+
31
+ # Project
32
+ junk/
gistfs-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,38 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project overview
6
+
7
+ gistfs is a Python library that uses GitHub Gists as a persistent key-value filesystem, designed for AI agent memory. It provides a layered architecture:
8
+
9
+ - **GistFS** (`gistfs/core.py`) — Low-level filesystem abstraction over a single gist. Handles CRUD on gist files via the GitHub API, with local caching and a context manager interface. Also provides `GistFile`, a `io.StringIO` subclass for file-like read/write/append.
10
+ - **GistMemory** (`gistfs/memory.py`) — Higher-level key-value store built on GistFS. Supports collections (each collection = one JSON file in the gist).
11
+ - **Integrations** (`gistfs/integrations/`) — Adapters that implement third-party store interfaces:
12
+ - `GistKVStore` (LlamaIndex `BaseKVStore`) — delegates to GistMemory
13
+ - `GistStore` (LangGraph `BaseStore`) — uses GistFS directly, maps namespace tuples to filenames (`("user", "prefs")` → `user__prefs.json`)
14
+
15
+ ## Commands
16
+
17
+ ```bash
18
+ # Install (uses uv, hatchling build backend)
19
+ uv pip install -e ".[dev,all]"
20
+
21
+ # Run all tests (requires GITHUB_TOKEN — tests hit the live GitHub API)
22
+ uv run pytest
23
+
24
+ # Run a single test file or test
25
+ uv run pytest tests/test_core.py
26
+ uv run pytest tests/test_core.py::test_write_and_read -v
27
+ ```
28
+
29
+ ## Testing
30
+
31
+ Tests are **live integration tests** — they create a real gist, run operations, then delete it. The `test_gist` session-scoped fixture in `tests/conftest.py` handles gist lifecycle. A `GITHUB_TOKEN` env var with `gist` scope is required; tests skip if it's missing. A `.env` file is loaded via `python-dotenv`.
32
+
33
+ ## Key design decisions
34
+
35
+ - All gist file content is JSON-serialized. Non-JSON content is stored/returned as raw strings.
36
+ - GistFS uses an in-memory `_cache` dict synced from the API. The cache is populated on `sync()` / context manager entry and cleared on exit.
37
+ - Write operations (`write`, `delete`) hit the GitHub API immediately then update the local cache — there is no batching or deferred flush.
38
+ - `GistFile.close()` flushes writes to the gist; the context manager calls `close()` automatically.
gistfs-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hervé Mignot
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.
gistfs-0.1.0/Makefile ADDED
@@ -0,0 +1,17 @@
1
+ .PHONY: test build clean publish-test publish
2
+
3
+ test:
4
+ uv run pytest
5
+
6
+ build: clean
7
+ uv build
8
+
9
+ clean:
10
+ rm -rf dist/
11
+
12
+ publish-test: build
13
+ UV_PUBLISH_TOKEN=$(UV_PUBLISH_TOKEN_TEST)
14
+ uv publish --publish-url https://test.pypi.org/legacy/
15
+
16
+ publish: build
17
+ uv publish
gistfs-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: gistfs
3
+ Version: 0.1.0
4
+ Summary: Use GitHub Gists as a persistent key-value filesystem — ideal for AI agent memory
5
+ Project-URL: Homepage, https://github.com/HerveMignot/gistfs
6
+ Project-URL: Repository, https://github.com/HerveMignot/gistfs
7
+ Project-URL: Issues, https://github.com/HerveMignot/gistfs/issues
8
+ Author: Hervé Mignot
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,ai,filesystem,gist,github,llm,memory
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: requests>=2.28
24
+ Provides-Extra: all
25
+ Requires-Dist: langgraph-checkpoint>=2.0; extra == 'all'
26
+ Requires-Dist: llama-index-core>=0.11; extra == 'all'
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: python-dotenv>=1.0; extra == 'dev'
31
+ Provides-Extra: langgraph
32
+ Requires-Dist: langgraph-checkpoint>=2.0; extra == 'langgraph'
33
+ Provides-Extra: llamaindex
34
+ Requires-Dist: llama-index-core>=0.11; extra == 'llamaindex'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # gistfs
38
+
39
+ Use GitHub Gists as a persistent key-value filesystem — ideal for AI agent memory.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install gistfs
45
+ ```
46
+
47
+ With optional integrations:
48
+
49
+ ```bash
50
+ pip install gistfs[llamaindex] # LlamaIndex KVStore
51
+ pip install gistfs[langgraph] # LangGraph BaseStore
52
+ pip install gistfs[all] # everything
53
+ ```
54
+
55
+ ## Quick start
56
+
57
+ ### Creating a new gist
58
+
59
+ No need to create a gist manually — bootstrap one from code:
60
+
61
+ ```python
62
+ from gistfs import GistFS
63
+
64
+ gfs = GistFS.create(description="my agent memory")
65
+ print(gfs.gist_id) # save this for later
66
+
67
+ # Or with GistMemory:
68
+ from gistfs import GistMemory
69
+ mem = GistMemory.create(description="my agent memory")
70
+ ```
71
+
72
+ ### As a filesystem (context manager)
73
+
74
+ ```python
75
+ from gistfs import GistFS
76
+
77
+ with GistFS(gist_id="your_gist_id") as gfs:
78
+ gfs.write("config.json", {"model": "gpt-4", "temperature": 0.7})
79
+ config = gfs.read("config.json")
80
+ print(gfs.list_files())
81
+ gfs.delete("config.json")
82
+ ```
83
+
84
+ ### File-like interface
85
+
86
+ ```python
87
+ with GistFS(gist_id="your_gist_id") as gfs:
88
+ with gfs.open("notes.txt", "w") as f:
89
+ f.write("hello world")
90
+
91
+ with gfs.open("notes.txt", "r") as f:
92
+ content = f.read()
93
+
94
+ with gfs.open("notes.txt", "a") as f:
95
+ f.write("\nappended line")
96
+ ```
97
+
98
+ ### As AI agent memory
99
+
100
+ ```python
101
+ from gistfs import GistMemory
102
+
103
+ with GistMemory(gist_id="your_gist_id") as mem:
104
+ mem.put("conversation_1", {"messages": [{"role": "user", "content": "hi"}]})
105
+ history = mem.get("conversation_1")
106
+ all_data = mem.get_all()
107
+ mem.delete("conversation_1")
108
+ ```
109
+
110
+ ### LlamaIndex integration
111
+
112
+ ```python
113
+ from gistfs.integrations.llamaindex import GistKVStore
114
+
115
+ store = GistKVStore(gist_id="your_gist_id")
116
+ store.put("doc1", {"text": "hello world"}, collection="docstore")
117
+ doc = store.get("doc1", collection="docstore")
118
+ ```
119
+
120
+ ### LangGraph integration
121
+
122
+ ```python
123
+ from gistfs.integrations.langgraph import GistStore
124
+
125
+ store = GistStore(gist_id="your_gist_id")
126
+ store.put(("user", "prefs"), "theme", {"value": "dark"})
127
+ item = store.get(("user", "prefs"), "theme")
128
+ ```
129
+
130
+ ## Authentication
131
+
132
+ Set the `GITHUB_TOKEN` environment variable with a GitHub personal access token that has `gist` scope. Read-only operations on public gists work without a token.
133
+
134
+ ```bash
135
+ export GITHUB_TOKEN=ghp_your_token_here
136
+ ```
137
+
138
+ Or pass it directly:
139
+
140
+ ```python
141
+ gfs = GistFS(gist_id="abc123", token="ghp_...")
142
+ ```
gistfs-0.1.0/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # gistfs
2
+
3
+ Use GitHub Gists as a persistent key-value filesystem — ideal for AI agent memory.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install gistfs
9
+ ```
10
+
11
+ With optional integrations:
12
+
13
+ ```bash
14
+ pip install gistfs[llamaindex] # LlamaIndex KVStore
15
+ pip install gistfs[langgraph] # LangGraph BaseStore
16
+ pip install gistfs[all] # everything
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ### Creating a new gist
22
+
23
+ No need to create a gist manually — bootstrap one from code:
24
+
25
+ ```python
26
+ from gistfs import GistFS
27
+
28
+ gfs = GistFS.create(description="my agent memory")
29
+ print(gfs.gist_id) # save this for later
30
+
31
+ # Or with GistMemory:
32
+ from gistfs import GistMemory
33
+ mem = GistMemory.create(description="my agent memory")
34
+ ```
35
+
36
+ ### As a filesystem (context manager)
37
+
38
+ ```python
39
+ from gistfs import GistFS
40
+
41
+ with GistFS(gist_id="your_gist_id") as gfs:
42
+ gfs.write("config.json", {"model": "gpt-4", "temperature": 0.7})
43
+ config = gfs.read("config.json")
44
+ print(gfs.list_files())
45
+ gfs.delete("config.json")
46
+ ```
47
+
48
+ ### File-like interface
49
+
50
+ ```python
51
+ with GistFS(gist_id="your_gist_id") as gfs:
52
+ with gfs.open("notes.txt", "w") as f:
53
+ f.write("hello world")
54
+
55
+ with gfs.open("notes.txt", "r") as f:
56
+ content = f.read()
57
+
58
+ with gfs.open("notes.txt", "a") as f:
59
+ f.write("\nappended line")
60
+ ```
61
+
62
+ ### As AI agent memory
63
+
64
+ ```python
65
+ from gistfs import GistMemory
66
+
67
+ with GistMemory(gist_id="your_gist_id") as mem:
68
+ mem.put("conversation_1", {"messages": [{"role": "user", "content": "hi"}]})
69
+ history = mem.get("conversation_1")
70
+ all_data = mem.get_all()
71
+ mem.delete("conversation_1")
72
+ ```
73
+
74
+ ### LlamaIndex integration
75
+
76
+ ```python
77
+ from gistfs.integrations.llamaindex import GistKVStore
78
+
79
+ store = GistKVStore(gist_id="your_gist_id")
80
+ store.put("doc1", {"text": "hello world"}, collection="docstore")
81
+ doc = store.get("doc1", collection="docstore")
82
+ ```
83
+
84
+ ### LangGraph integration
85
+
86
+ ```python
87
+ from gistfs.integrations.langgraph import GistStore
88
+
89
+ store = GistStore(gist_id="your_gist_id")
90
+ store.put(("user", "prefs"), "theme", {"value": "dark"})
91
+ item = store.get(("user", "prefs"), "theme")
92
+ ```
93
+
94
+ ## Authentication
95
+
96
+ Set the `GITHUB_TOKEN` environment variable with a GitHub personal access token that has `gist` scope. Read-only operations on public gists work without a token.
97
+
98
+ ```bash
99
+ export GITHUB_TOKEN=ghp_your_token_here
100
+ ```
101
+
102
+ Or pass it directly:
103
+
104
+ ```python
105
+ gfs = GistFS(gist_id="abc123", token="ghp_...")
106
+ ```
@@ -0,0 +1,7 @@
1
+ """gistfs — Use GitHub Gists as a persistent key-value filesystem for AI agents."""
2
+
3
+ from .core import GistFS, GistFile
4
+ from .memory import GistMemory
5
+
6
+ __all__ = ["GistFS", "GistFile", "GistMemory"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,330 @@
1
+ """Core GistFS class — use GitHub Gists as a persistent key-value filesystem."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import json
7
+ import os
8
+ from typing import Any
9
+
10
+ import requests
11
+
12
+
13
+ GITHUB_API_GISTS = "https://api.github.com/gists/{gist_id}"
14
+ GITHUB_API_GISTS_BASE = "https://api.github.com/gists"
15
+
16
+
17
+ class GistFS:
18
+ """A filesystem-like interface backed by a single GitHub Gist.
19
+
20
+ Each "file" in the gist stores a JSON-serialized value, giving you a
21
+ persistent key-value store accessible from anywhere with an internet
22
+ connection.
23
+
24
+ Usage::
25
+
26
+ with GistFS(gist_id="abc123") as gfs:
27
+ gfs.write("state.json", {"counter": 1})
28
+ data = gfs.read("state.json")
29
+ print(gfs.list_files())
30
+ gfs.delete("state.json")
31
+
32
+ The token is read from the ``GITHUB_TOKEN`` environment variable by
33
+ default, or can be passed explicitly. Read-only operations on public
34
+ gists work without a token.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ gist_id: str,
40
+ token: str | None = None,
41
+ *,
42
+ auto_sync: bool = True,
43
+ ) -> None:
44
+ self.gist_id = gist_id
45
+ self.token = token or os.environ.get("GITHUB_TOKEN", "")
46
+ self.auto_sync = auto_sync
47
+ self._url = GITHUB_API_GISTS.format(gist_id=gist_id)
48
+ self._cache: dict[str, Any] | None = None
49
+
50
+ # ── factory ───────────────────────────────────────────────────────
51
+
52
+ @classmethod
53
+ def create(
54
+ cls,
55
+ description: str = "",
56
+ *,
57
+ public: bool = False,
58
+ token: str | None = None,
59
+ init_files: dict[str, Any] | None = None,
60
+ ) -> GistFS:
61
+ """Create a new GitHub Gist and return a :class:`GistFS` bound to it.
62
+
63
+ Parameters
64
+ ----------
65
+ description:
66
+ Gist description shown on GitHub.
67
+ public:
68
+ If ``True`` the gist is public; otherwise it is secret (default).
69
+ token:
70
+ GitHub personal access token. Falls back to ``GITHUB_TOKEN``.
71
+ init_files:
72
+ Optional mapping of ``{filename: content}`` to seed the gist with.
73
+ If *None*, a single placeholder file ``_gistfs.json`` is created.
74
+
75
+ Returns
76
+ -------
77
+ GistFS
78
+ A new instance already synced with the freshly created gist.
79
+ """
80
+ resolved_token = token or os.environ.get("GITHUB_TOKEN", "")
81
+ if not resolved_token:
82
+ raise ValueError(
83
+ "A GitHub token is required to create a gist. "
84
+ "Set GITHUB_TOKEN or pass token=."
85
+ )
86
+
87
+ if init_files:
88
+ files_payload = {
89
+ fname: {"content": json.dumps(data, ensure_ascii=False, default=str)}
90
+ for fname, data in init_files.items()
91
+ }
92
+ else:
93
+ files_payload = {"_gistfs.json": {"content": "{}"}}
94
+
95
+ payload: dict[str, Any] = {
96
+ "description": description,
97
+ "public": public,
98
+ "files": files_payload,
99
+ }
100
+ headers = {
101
+ "Accept": "application/vnd.github+json",
102
+ "Authorization": f"Bearer {resolved_token}",
103
+ }
104
+ resp = requests.post(GITHUB_API_GISTS_BASE, headers=headers, json=payload)
105
+ resp.raise_for_status()
106
+
107
+ gist_id = resp.json()["id"]
108
+ instance = cls(gist_id, token=resolved_token)
109
+ instance.sync()
110
+ return instance
111
+
112
+ # ── context manager ──────────────────────────────────────────────
113
+
114
+ def __enter__(self) -> GistFS:
115
+ self.sync()
116
+ return self
117
+
118
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: ANN001
119
+ self._cache = None
120
+ return None
121
+
122
+ # ── public API ───────────────────────────────────────────────────
123
+
124
+ def sync(self) -> None:
125
+ """Fetch the latest gist state from GitHub."""
126
+ resp = self._get_request()
127
+ resp.raise_for_status()
128
+ gist = resp.json()
129
+ self._cache = {}
130
+ for fname, fdata in gist.get("files", {}).items():
131
+ try:
132
+ self._cache[fname] = json.loads(fdata["content"])
133
+ except (json.JSONDecodeError, TypeError):
134
+ self._cache[fname] = fdata["content"]
135
+
136
+ def read(self, filename: str) -> Any:
137
+ """Read and return the parsed content of *filename*."""
138
+ self._ensure_synced()
139
+ assert self._cache is not None
140
+ if filename not in self._cache:
141
+ raise FileNotFoundError(f"{filename!r} not found in gist {self.gist_id}")
142
+ return self._cache[filename]
143
+
144
+ def write(self, filename: str, data: Any) -> None:
145
+ """Write *data* (JSON-serializable) to *filename* in the gist."""
146
+ content = json.dumps(data, ensure_ascii=False, default=str)
147
+ payload = {"files": {filename: {"content": content}}}
148
+ resp = self._patch_request(payload)
149
+ resp.raise_for_status()
150
+ # Update local cache
151
+ self._ensure_synced()
152
+ assert self._cache is not None
153
+ self._cache[filename] = data
154
+
155
+ def delete(self, filename: str) -> bool:
156
+ """Delete *filename* from the gist. Returns True if it existed."""
157
+ self._ensure_synced()
158
+ assert self._cache is not None
159
+ if filename not in self._cache:
160
+ return False
161
+ # GitHub API: setting content to empty string with null deletes the file
162
+ payload = {"files": {filename: None}}
163
+ resp = self._patch_request(payload)
164
+ resp.raise_for_status()
165
+ self._cache.pop(filename, None)
166
+ return True
167
+
168
+ def list_files(self) -> list[str]:
169
+ """Return a list of filenames in the gist."""
170
+ self._ensure_synced()
171
+ assert self._cache is not None
172
+ return list(self._cache.keys())
173
+
174
+ def exists(self, filename: str) -> bool:
175
+ """Check whether *filename* exists in the gist."""
176
+ self._ensure_synced()
177
+ assert self._cache is not None
178
+ return filename in self._cache
179
+
180
+ def read_all(self) -> dict[str, Any]:
181
+ """Return all files as a ``{filename: content}`` dict."""
182
+ self._ensure_synced()
183
+ assert self._cache is not None
184
+ return dict(self._cache)
185
+
186
+ def open(self, filename: str, mode: str = "r") -> GistFile:
187
+ """Open a file in the gist, returning a file-like object.
188
+
189
+ Supported modes: ``"r"`` (read), ``"w"`` (write), ``"a"`` (append),
190
+ ``"r+"`` (read and write).
191
+
192
+ Usage::
193
+
194
+ with gfs.open("notes.txt", "w") as f:
195
+ f.write("hello world")
196
+
197
+ with gfs.open("notes.txt", "r") as f:
198
+ content = f.read()
199
+ """
200
+ if mode not in ("r", "w", "a", "r+"):
201
+ raise ValueError(f"Unsupported mode {mode!r} — use 'r', 'w', 'a', or 'r+'")
202
+ return GistFile(self, filename, mode)
203
+
204
+ # ── helpers ──────────────────────────────────────────────────────
205
+
206
+ def _ensure_synced(self) -> None:
207
+ if self._cache is None:
208
+ if self.auto_sync:
209
+ self.sync()
210
+ else:
211
+ raise RuntimeError("GistFS not synced — call .sync() first")
212
+
213
+ def _headers(self, *, auth: bool = True) -> dict[str, str]:
214
+ headers = {"Accept": "application/vnd.github+json"}
215
+ if auth and self.token:
216
+ headers["Authorization"] = f"Bearer {self.token}"
217
+ return headers
218
+
219
+ def _get_request(self) -> requests.Response:
220
+ return requests.get(self._url, headers=self._headers(auth=bool(self.token)))
221
+
222
+ def _patch_request(self, payload: dict) -> requests.Response:
223
+ if not self.token:
224
+ raise ValueError(
225
+ "A GitHub token is required for write operations. "
226
+ "Set GITHUB_TOKEN or pass token= to GistFS()."
227
+ )
228
+ return requests.patch(self._url, headers=self._headers(), json=payload)
229
+
230
+ def __repr__(self) -> str:
231
+ return f"GistFS(gist_id={self.gist_id!r})"
232
+
233
+
234
+ class GistFile(io.StringIO):
235
+ """File-like object for a single file in a gist.
236
+
237
+ Wraps :class:`io.StringIO` so that standard file operations
238
+ (``read``, ``write``, ``seek``, ``tell``, iteration) work as expected.
239
+ On :meth:`close` (or exiting the context manager), any writes are
240
+ flushed back to the gist.
241
+
242
+ Do not instantiate directly — use :meth:`GistFS.open` instead.
243
+ """
244
+
245
+ def __init__(self, gfs: GistFS, filename: str, mode: str) -> None:
246
+ self._gfs = gfs
247
+ self._filename = filename
248
+ self._mode = mode
249
+ self._writable = mode in ("w", "a", "r+")
250
+
251
+ # Seed the buffer with existing content for read / append / r+
252
+ initial = ""
253
+ if mode in ("r", "a", "r+"):
254
+ try:
255
+ content = gfs.read(filename)
256
+ if isinstance(content, str):
257
+ initial = content
258
+ else:
259
+ initial = json.dumps(content, ensure_ascii=False, default=str)
260
+ except FileNotFoundError:
261
+ if mode == "r":
262
+ raise
263
+ # "a" and "r+" start empty if file doesn't exist
264
+
265
+ super().__init__(initial)
266
+
267
+ # Position cursor at end for append, at start for read/r+
268
+ if mode == "a":
269
+ self.seek(0, io.SEEK_END)
270
+ else:
271
+ self.seek(0)
272
+
273
+ @property
274
+ def name(self) -> str:
275
+ return self._filename
276
+
277
+ @property
278
+ def mode(self) -> str:
279
+ return self._mode
280
+
281
+ def writable(self) -> bool:
282
+ return self._writable
283
+
284
+ def readable(self) -> bool:
285
+ return self._mode in ("r", "r+")
286
+
287
+ def write(self, s: str) -> int:
288
+ if not self._writable:
289
+ raise io.UnsupportedOperation("not writable")
290
+ return super().write(s)
291
+
292
+ def read(self, size: int = -1) -> str:
293
+ if self._mode == "w":
294
+ raise io.UnsupportedOperation("not readable")
295
+ return super().read(size)
296
+
297
+ def readline(self, size: int = -1) -> str:
298
+ if self._mode == "w":
299
+ raise io.UnsupportedOperation("not readable")
300
+ return super().readline(size)
301
+
302
+ def readlines(self, hint: int = -1) -> list[str]:
303
+ if self._mode == "w":
304
+ raise io.UnsupportedOperation("not readable")
305
+ return super().readlines(hint)
306
+
307
+ def close(self) -> None:
308
+ if not self.closed and self._writable:
309
+ self._flush_to_gist()
310
+ super().close()
311
+
312
+ def _flush_to_gist(self) -> None:
313
+ """Push the buffer content to the gist as a raw string."""
314
+ content = self.getvalue()
315
+ if not content:
316
+ return
317
+ payload = {"files": {self._filename: {"content": content}}}
318
+ resp = self._gfs._patch_request(payload)
319
+ resp.raise_for_status()
320
+ # Update cache
321
+ self._gfs._ensure_synced()
322
+ assert self._gfs._cache is not None
323
+ try:
324
+ self._gfs._cache[self._filename] = json.loads(content)
325
+ except (json.JSONDecodeError, ValueError):
326
+ self._gfs._cache[self._filename] = content
327
+
328
+ def __repr__(self) -> str:
329
+ state = "closed" if self.closed else self._mode
330
+ return f"GistFile({self._filename!r}, {state!r})"
File without changes