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.
Files changed (24) hide show
  1. {tai42_storage_github-0.2.2/src/tai42_storage_github.egg-info → tai42_storage_github-0.3.0}/PKG-INFO +2 -2
  2. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/pyproject.toml +2 -2
  3. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/settings.py +7 -0
  4. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/storage.py +27 -11
  5. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/tai-plugin.yml +1 -1
  6. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0/src/tai42_storage_github.egg-info}/PKG-INFO +2 -2
  7. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/requires.txt +1 -1
  8. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/LICENSE +0 -0
  9. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/NOTICE +0 -0
  10. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/README.md +0 -0
  11. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/setup.cfg +0 -0
  12. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/__init__.py +0 -0
  13. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/client.py +0 -0
  14. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github/py.typed +0 -0
  15. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/SOURCES.txt +0 -0
  16. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/dependency_links.txt +0 -0
  17. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/src/tai42_storage_github.egg-info/top_level.txt +0 -0
  18. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_binary.py +0 -0
  19. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_delete_dir.py +0 -0
  20. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_import_graph.py +0 -0
  21. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_plugin_spec.py +0 -0
  22. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_registration.py +0 -0
  23. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_security.py +0 -0
  24. {tai42_storage_github-0.2.2 → tai42_storage_github-0.3.0}/tests/test_text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai42-storage-github
3
- Version: 0.2.2
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.4,>=0.2
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.2.2"
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.2,<0.4",
29
+ "tai42-kit>=0.4,<0.5",
30
30
  "httpx>=0.28",
31
31
  "pydantic>=2.12",
32
32
  "pydantic-settings>=2.11",
@@ -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
@@ -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
- joined = "/".join(segment for segment in path.split("/") if segment)
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 = _join(RAW_BASE_URL.format(username=settings.username, repo=settings.repo, branch=settings.branch), path)
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 = _join(RAW_BASE_URL.format(username=settings.username, repo=settings.repo, branch=settings.branch), path)
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 = TREES_API_URL.format(username=settings.username, repo=settings.repo, branch=settings.branch)
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 = _join(CONTENTS_API_URL.format(username=settings.username, repo=settings.repo), path)
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 = _join(CONTENTS_API_URL.format(username=settings.username, repo=settings.repo), path)
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})
@@ -2,7 +2,7 @@ spec_version: 1
2
2
  namespace: tai42
3
3
  name: storage-github
4
4
  package: tai42-storage-github
5
- version: 0.2.2
5
+ version: 0.3.0
6
6
  description: GitHub-backed Storage provider (text + binary/media) for the TAI ecosystem.
7
7
  license: Apache-2.0
8
8
  homepage: https://tai42.ai
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai42-storage-github
3
- Version: 0.2.2
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.4,>=0.2
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
@@ -1,5 +1,5 @@
1
1
  tai42-contract<0.4,>=0.3
2
- tai42-kit<0.4,>=0.2
2
+ tai42-kit<0.5,>=0.4
3
3
  httpx>=0.28
4
4
  pydantic>=2.12
5
5
  pydantic-settings>=2.11