tai42-storage-github 0.2.2__tar.gz → 0.3.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.
- {tai42_storage_github-0.2.2/src/tai42_storage_github.egg-info → tai42_storage_github-0.3.0}/PKG-INFO +2 -2
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/pyproject.toml +2 -2
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/settings.py +7 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/storage.py +27 -11
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/tai-plugin.yml +1 -1
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0/src/tai42_storage_github.egg-info}/PKG-INFO +2 -2
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/requires.txt +1 -1
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/LICENSE +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/NOTICE +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/README.md +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/setup.cfg +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/__init__.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/client.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/py.typed +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/SOURCES.txt +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/dependency_links.txt +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/top_level.txt +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_binary.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_delete_dir.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_import_graph.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_plugin_spec.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_registration.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_security.py +0 -0
- {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_text.py +0 -0
{tai42_storage_github-0.2.2/src/tai42_storage_github.egg-info → tai42_storage_github-0.3.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tai42-storage-github
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: GitHub-backed Storage provider (text + binary/media) for the TAI ecosystem.
|
|
5
5
|
Author-email: tai42 <oss@tai42.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
License-File: NOTICE
|
|
22
22
|
Requires-Dist: tai42-contract<0.4,>=0.3
|
|
23
|
-
Requires-Dist: tai42-kit<0.
|
|
23
|
+
Requires-Dist: tai42-kit<0.5,>=0.4
|
|
24
24
|
Requires-Dist: httpx>=0.28
|
|
25
25
|
Requires-Dist: pydantic>=2.12
|
|
26
26
|
Requires-Dist: pydantic-settings>=2.11
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tai42-storage-github"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "GitHub-backed Storage provider (text + binary/media) for the TAI ecosystem."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -26,7 +26,7 @@ dependencies = [
|
|
|
26
26
|
# The pure interface contract (Storage, ObjectStat, assert_not_root, tai42_app).
|
|
27
27
|
"tai42-contract>=0.3,<0.4",
|
|
28
28
|
# Pooled clients + settings primitives (PooledClient, TaiBaseSettings, cache).
|
|
29
|
-
"tai42-kit>=0.
|
|
29
|
+
"tai42-kit>=0.4,<0.5",
|
|
30
30
|
"httpx>=0.28",
|
|
31
31
|
"pydantic>=2.12",
|
|
32
32
|
"pydantic-settings>=2.11",
|
{tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/settings.py
RENAMED
|
@@ -18,6 +18,13 @@ class GithubStorageSettings(TaiBaseSettings):
|
|
|
18
18
|
repo: str | None = None
|
|
19
19
|
branch: str = "main"
|
|
20
20
|
token: SecretStr | None = None
|
|
21
|
+
# Base-URL templates for the three REST surfaces the provider speaks, default to
|
|
22
|
+
# the real GitHub hosts. Overridable (like ``S3Settings.endpoint``) so a test can
|
|
23
|
+
# point the plugin at a hermetic stand-in; each carries ``{username}``/``{repo}``/
|
|
24
|
+
# ``{branch}`` placeholders the provider fills per request.
|
|
25
|
+
raw_base_url: str = "https://raw.githubusercontent.com/{username}/{repo}/refs/heads/{branch}"
|
|
26
|
+
contents_api_url: str = "https://api.github.com/repos/{username}/{repo}/contents"
|
|
27
|
+
trees_api_url: str = "https://api.github.com/repos/{username}/{repo}/git/trees/{branch}"
|
|
21
28
|
timeout_total: float = 15.0
|
|
22
29
|
max_connections: int = 200
|
|
23
30
|
max_keepalive_connections: int = 50
|
{tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/storage.py
RENAMED
|
@@ -21,22 +21,38 @@ from tai42_storage_github.settings import GithubStorageSettings, github_storage_
|
|
|
21
21
|
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
|
-
RAW_BASE_URL = "https://raw.githubusercontent.com/{username}/{repo}/refs/heads/{branch}"
|
|
25
|
-
CONTENTS_API_URL = "https://api.github.com/repos/{username}/{repo}/contents"
|
|
26
|
-
TREES_API_URL = "https://api.github.com/repos/{username}/{repo}/git/trees/{branch}"
|
|
27
|
-
|
|
28
24
|
# Conservative upload cap below GitHub's Contents API limit; an oversize file
|
|
29
25
|
# raises before the request. Reads are uncapped (raw endpoint).
|
|
30
26
|
MAX_UPLOAD_BYTES = 1024 * 1024
|
|
31
27
|
|
|
32
28
|
|
|
33
29
|
def _join(base: str, path: str) -> str:
|
|
34
|
-
"""Join ``path`` onto ``base``, dropping empty and stray-slash segments.
|
|
35
|
-
|
|
30
|
+
"""Join ``path`` onto ``base``, dropping empty and stray-slash segments.
|
|
31
|
+
|
|
32
|
+
A ``..`` segment is rejected loudly: preserved, it would climb out of the
|
|
33
|
+
configured repo/branch base and let a caller-supplied path reach an arbitrary
|
|
34
|
+
raw/contents URL (path traversal).
|
|
35
|
+
"""
|
|
36
|
+
segments = [segment for segment in path.split("/") if segment]
|
|
37
|
+
if ".." in segments:
|
|
38
|
+
raise ValueError(f"path traversal ('..') is not allowed in storage path: {path!r}")
|
|
39
|
+
joined = "/".join(segments)
|
|
36
40
|
base = base.rstrip("/")
|
|
37
41
|
return f"{base}/{joined}" if joined else base
|
|
38
42
|
|
|
39
43
|
|
|
44
|
+
def _raw_url(settings: GithubStorageSettings, path: str) -> str:
|
|
45
|
+
"""The RAW-endpoint URL for ``path``, from the configured raw base-URL template."""
|
|
46
|
+
base = settings.raw_base_url.format(username=settings.username, repo=settings.repo, branch=settings.branch)
|
|
47
|
+
return _join(base, path)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _contents_url(settings: GithubStorageSettings, path: str) -> str:
|
|
51
|
+
"""The Contents-API URL for ``path``, from the configured contents base-URL template."""
|
|
52
|
+
base = settings.contents_api_url.format(username=settings.username, repo=settings.repo)
|
|
53
|
+
return _join(base, path)
|
|
54
|
+
|
|
55
|
+
|
|
40
56
|
def _configured_settings() -> GithubStorageSettings:
|
|
41
57
|
"""The cached settings; raises a clear config error naming the env vars when
|
|
42
58
|
owner/repo is unset."""
|
|
@@ -83,7 +99,7 @@ def _raise_for_status(resp: httpx.Response, action: str, path: str, url: str) ->
|
|
|
83
99
|
class GithubStorage(Storage):
|
|
84
100
|
async def load(self, path: str) -> str:
|
|
85
101
|
settings = _configured_settings()
|
|
86
|
-
url =
|
|
102
|
+
url = _raw_url(settings, path)
|
|
87
103
|
async with tai42_app.clients.client_ctx(GithubHttpxClient) as client:
|
|
88
104
|
resp = await client.get(url, headers=_auth_headers(settings))
|
|
89
105
|
self._guard_read(resp, path, url)
|
|
@@ -91,7 +107,7 @@ class GithubStorage(Storage):
|
|
|
91
107
|
|
|
92
108
|
async def load_bytes(self, path: str) -> bytes:
|
|
93
109
|
settings = _configured_settings()
|
|
94
|
-
url =
|
|
110
|
+
url = _raw_url(settings, path)
|
|
95
111
|
async with tai42_app.clients.client_ctx(GithubHttpxClient) as client:
|
|
96
112
|
resp = await client.get(url, headers=_auth_headers(settings))
|
|
97
113
|
self._guard_read(resp, path, url)
|
|
@@ -114,7 +130,7 @@ class GithubStorage(Storage):
|
|
|
114
130
|
raised on loudly rather than acting on a partial listing.
|
|
115
131
|
"""
|
|
116
132
|
settings = _configured_settings()
|
|
117
|
-
url =
|
|
133
|
+
url = settings.trees_api_url.format(username=settings.username, repo=settings.repo, branch=settings.branch)
|
|
118
134
|
async with tai42_app.clients.client_ctx(GithubHttpxClient) as client:
|
|
119
135
|
resp = await client.get(url, headers=_api_headers(settings), params={"recursive": "1"})
|
|
120
136
|
_raise_for_status(resp, "listing", prefix or "/", url)
|
|
@@ -145,7 +161,7 @@ class GithubStorage(Storage):
|
|
|
145
161
|
f"{MAX_UPLOAD_BYTES}-byte GitHub Contents API upload cap. Store large objects elsewhere."
|
|
146
162
|
)
|
|
147
163
|
settings = _configured_settings()
|
|
148
|
-
url =
|
|
164
|
+
url = _contents_url(settings, path)
|
|
149
165
|
headers = _api_headers(settings)
|
|
150
166
|
encoded = base64.b64encode(data).decode("ascii")
|
|
151
167
|
async with tai42_app.clients.client_ctx(GithubHttpxClient) as client:
|
|
@@ -169,7 +185,7 @@ class GithubStorage(Storage):
|
|
|
169
185
|
|
|
170
186
|
async def delete(self, path: str) -> None:
|
|
171
187
|
settings = _configured_settings()
|
|
172
|
-
url =
|
|
188
|
+
url = _contents_url(settings, path)
|
|
173
189
|
headers = _api_headers(settings)
|
|
174
190
|
async with tai42_app.clients.client_ctx(GithubHttpxClient) as client:
|
|
175
191
|
get_resp = await client.get(url, headers=headers, params={"ref": settings.branch})
|
{tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0/src/tai42_storage_github.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tai42-storage-github
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: GitHub-backed Storage provider (text + binary/media) for the TAI ecosystem.
|
|
5
5
|
Author-email: tai42 <oss@tai42.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
License-File: NOTICE
|
|
22
22
|
Requires-Dist: tai42-contract<0.4,>=0.3
|
|
23
|
-
Requires-Dist: tai42-kit<0.
|
|
23
|
+
Requires-Dist: tai42-kit<0.5,>=0.4
|
|
24
24
|
Requires-Dist: httpx>=0.28
|
|
25
25
|
Requires-Dist: pydantic>=2.12
|
|
26
26
|
Requires-Dist: pydantic-settings>=2.11
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/__init__.py
RENAMED
|
File without changes
|
{tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|