fluidattacks_core_git 12.1.0__tar.gz → 12.2.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 (32) hide show
  1. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/.gitignore +2 -0
  2. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/PKG-INFO +2 -2
  3. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/import-linter.cfg +1 -11
  4. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/pyproject.toml +2 -2
  5. fluidattacks_core_git-12.2.0/tests/test_remote.py +106 -0
  6. fluidattacks_core_git-12.2.0/tests/test_upload_repo.py +559 -0
  7. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/uv.lock +5 -5
  8. fluidattacks_core_git-12.1.0/build/default.nix +0 -24
  9. fluidattacks_core_git-12.1.0/build/filter.nix +0 -13
  10. fluidattacks_core_git-12.1.0/flake.lock +0 -303
  11. fluidattacks_core_git-12.1.0/flake.nix +0 -30
  12. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/.envrc +0 -0
  13. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/__init__.py +0 -0
  14. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/classes.py +0 -0
  15. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/clone.py +0 -0
  16. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/codecommit_utils.py +0 -0
  17. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/constants.py +0 -0
  18. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/delete_files.py +0 -0
  19. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/download_file.py +0 -0
  20. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/download_repo.py +0 -0
  21. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/https_utils.py +0 -0
  22. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/py.typed +0 -0
  23. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/remote.py +0 -0
  24. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/show_file.py +0 -0
  25. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/ssh_utils.py +0 -0
  26. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/upload_repo.py +0 -0
  27. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/utils.py +0 -0
  28. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/warp.py +0 -0
  29. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/mypy.ini +0 -0
  30. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/ruff.toml +0 -0
  31. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/tests/__init__.py +0 -0
  32. {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/tests/test_git.py +0 -0
@@ -1,3 +1,4 @@
1
+ dist/
1
2
  dist-*/
2
3
  /.cargo-cache
3
4
  target/
@@ -5,3 +6,4 @@ target/
5
6
  .venv/
6
7
  .vscode/settings.json
7
8
  .zed/settings.json
9
+ *.so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fluidattacks_core_git
3
- Version: 12.1.0
3
+ Version: 12.2.0
4
4
  Summary: Fluid Attacks Core Git Library
5
5
  Author-email: Development <development@fluidattacks.com>
6
6
  License: MPL-2.0
@@ -16,7 +16,7 @@ Requires-Dist: aiohttp>=3.10.0
16
16
  Requires-Dist: anyio>=4.7.0
17
17
  Requires-Dist: boto3>=1.34
18
18
  Requires-Dist: botocore>=1.40.18
19
- Requires-Dist: fluidattacks-core-http>=12.0.0
19
+ Requires-Dist: fluidattacks-core-http>=12.2.0
20
20
  Requires-Dist: gitpython>=3.1.41
21
21
  Requires-Dist: pathspec>=0.12.1
22
22
  Requires-Dist: urllib3>=2.6.1
@@ -1,17 +1,7 @@
1
1
  [importlinter]
2
- root_package = fluidattacks_core
2
+ root_package = fluidattacks_core.git
3
3
  include_external_packages = True
4
4
 
5
- [importlinter:contract:portion]
6
- name = Single Portion
7
- type = layers
8
- containers =
9
- fluidattacks_core
10
- exhaustive = true
11
- layers =
12
- git
13
- http
14
-
15
5
  [importlinter:contract:dag]
16
6
  name = Direct Acyclic Graph
17
7
  type = layers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fluidattacks_core_git"
3
- version = "12.1.0"
3
+ version = "12.2.0"
4
4
  description = "Fluid Attacks Core Git Library"
5
5
  authors = [{ name = "Development", email = "development@fluidattacks.com" }]
6
6
  license = { text = "MPL-2.0" }
@@ -19,7 +19,7 @@ dependencies = [
19
19
  "anyio>=4.7.0",
20
20
  "boto3>=1.34",
21
21
  "botocore>=1.40.18",
22
- "fluidattacks-core-http>=12.0.0",
22
+ "fluidattacks-core-http>=12.2.0",
23
23
  "GitPython>=3.1.41",
24
24
  "pathspec>=0.12.1",
25
25
  "urllib3>=2.6.1",
@@ -0,0 +1,106 @@
1
+ from unittest.mock import AsyncMock, patch
2
+
3
+ import pytest
4
+
5
+ from fluidattacks_core.git.remote import ls_remote
6
+
7
+ _SSH_PATH = "fluidattacks_core.git.remote.call_ssh_ls_remote"
8
+ _CODECOMMIT_PATH = "fluidattacks_core.git.remote.call_codecommit_ls_remote"
9
+ _HTTPS_PATH = "fluidattacks_core.git.remote.call_https_ls_remote"
10
+
11
+ _SHA = "0" * 40
12
+ _CREDENTIAL = "a-token"
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_ls_remote_with_a_credential_key_goes_over_ssh() -> None:
17
+ with (
18
+ patch(_SSH_PATH, AsyncMock(return_value=(_SHA, None))) as ssh,
19
+ patch(_CODECOMMIT_PATH, AsyncMock()) as codecommit,
20
+ patch(_HTTPS_PATH, AsyncMock()) as https,
21
+ ):
22
+ sha, error = await ls_remote(
23
+ repo_url="git@github.com:fluidattacks/universe",
24
+ repo_branch="main",
25
+ credential_key="a-key",
26
+ arn="an-arn",
27
+ org_external_id="an-id",
28
+ )
29
+
30
+ assert (sha, error) == (_SHA, None)
31
+ ssh.assert_awaited_once_with("git@github.com:fluidattacks/universe", "a-key", "main")
32
+ codecommit.assert_not_awaited()
33
+ https.assert_not_awaited()
34
+
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_ls_remote_with_an_arn_and_an_external_id_goes_over_codecommit() -> None:
38
+ with (
39
+ patch(_SSH_PATH, AsyncMock()) as ssh,
40
+ patch(_CODECOMMIT_PATH, AsyncMock(return_value=(_SHA, None))) as codecommit,
41
+ patch(_HTTPS_PATH, AsyncMock()) as https,
42
+ ):
43
+ sha, error = await ls_remote(
44
+ repo_url="codecommit://a-repo",
45
+ repo_branch="main",
46
+ arn="an-arn",
47
+ org_external_id="an-id",
48
+ follow_redirects=True,
49
+ )
50
+
51
+ assert (sha, error) == (_SHA, None)
52
+ codecommit.assert_awaited_once_with(
53
+ "codecommit://a-repo",
54
+ "an-arn",
55
+ "main",
56
+ org_external_id="an-id",
57
+ follow_redirects=True,
58
+ )
59
+ ssh.assert_not_awaited()
60
+ https.assert_not_awaited()
61
+
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_ls_remote_without_ssh_or_codecommit_inputs_goes_over_https() -> None:
65
+ with (
66
+ patch(_SSH_PATH, AsyncMock()) as ssh,
67
+ patch(_CODECOMMIT_PATH, AsyncMock()) as codecommit,
68
+ patch(_HTTPS_PATH, AsyncMock(return_value=(_SHA, None))) as https,
69
+ ):
70
+ sha, error = await ls_remote(
71
+ repo_url="https://github.com/fluidattacks/universe",
72
+ repo_branch="main",
73
+ token=_CREDENTIAL,
74
+ provider="GITHUB",
75
+ is_pat=True,
76
+ )
77
+
78
+ assert (sha, error) == (_SHA, None)
79
+ https.assert_awaited_once_with(
80
+ repo_url="https://github.com/fluidattacks/universe",
81
+ user=None,
82
+ password=None,
83
+ token=_CREDENTIAL,
84
+ branch="main",
85
+ provider="GITHUB",
86
+ is_pat=True,
87
+ follow_redirects=False,
88
+ )
89
+ ssh.assert_not_awaited()
90
+ codecommit.assert_not_awaited()
91
+
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_ls_remote_forwards_the_error_from_the_transport() -> None:
95
+ with (
96
+ patch(_SSH_PATH, AsyncMock()),
97
+ patch(_CODECOMMIT_PATH, AsyncMock()),
98
+ patch(_HTTPS_PATH, AsyncMock(return_value=(None, "Repository not found"))),
99
+ ):
100
+ sha, error = await ls_remote(
101
+ repo_url="https://github.com/fluidattacks/nope",
102
+ repo_branch="main",
103
+ )
104
+
105
+ assert sha is None
106
+ assert error == "Repository not found"
@@ -0,0 +1,559 @@
1
+ import math
2
+ import tarfile
3
+ import tempfile
4
+ from pathlib import Path
5
+ from unittest.mock import AsyncMock, MagicMock, patch
6
+
7
+ import pytest
8
+
9
+ from fluidattacks_core.git.constants import MAX_S3_PARTS, MULTIPART_PART_SIZE
10
+ from fluidattacks_core.git.upload_repo import (
11
+ MultipartCallbacks,
12
+ PartUploadConfig,
13
+ UploadPartResult,
14
+ _complete_with_retry,
15
+ _compute_part_size,
16
+ _create_tar_archive,
17
+ _do_part_upload,
18
+ _file_chunks,
19
+ _initiate_with_retry,
20
+ _upload_all_parts,
21
+ _upload_part_with_retry,
22
+ upload_repo_to_s3,
23
+ )
24
+
25
+ _SLEEP_PATH = "fluidattacks_core.git.upload_repo.anyio.sleep"
26
+ _PART_RETRY_PATH = "fluidattacks_core.git.upload_repo._upload_part_with_retry"
27
+ _SESSION_PATH = "fluidattacks_core.git.upload_repo.aiohttp.ClientSession"
28
+ _THRESHOLD_PATH = "fluidattacks_core.git.upload_repo.MULTIPART_THRESHOLD"
29
+
30
+
31
+ def _fake_git_repo(base: Path) -> Path:
32
+ repo = base / "repo"
33
+ git_dir = repo / ".git"
34
+ git_dir.mkdir(parents=True)
35
+ (git_dir / "HEAD").write_text("ref: refs/heads/main\n")
36
+ return repo
37
+
38
+
39
+ def _fake_file(base: Path, content: bytes = b"x" * 512) -> Path:
40
+ path = base / "data.bin"
41
+ path.write_bytes(content)
42
+ return path
43
+
44
+
45
+ def _make_mock_session(status: int = 200, etag: str = '"etag-value"') -> MagicMock:
46
+ mock_response = MagicMock()
47
+ mock_response.status = status
48
+ mock_response.headers = {"ETag": etag}
49
+ mock_response.text = AsyncMock(return_value="error body")
50
+
51
+ mock_put_cm = MagicMock()
52
+ mock_put_cm.__aenter__ = AsyncMock(return_value=mock_response)
53
+ mock_put_cm.__aexit__ = AsyncMock(return_value=None)
54
+
55
+ mock_session = MagicMock()
56
+ mock_session.__aenter__ = AsyncMock(return_value=mock_session)
57
+ mock_session.__aexit__ = AsyncMock(return_value=None)
58
+ mock_session.put = MagicMock(return_value=mock_put_cm)
59
+
60
+ return mock_session
61
+
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_create_tar_archive_produces_valid_gzip_with_git_entries() -> None:
65
+ with tempfile.TemporaryDirectory() as tmp:
66
+ repo_path = _fake_git_repo(Path(tmp))
67
+ output = Path(tmp) / "archive.tar.gz"
68
+
69
+ result = await _create_tar_archive(repo_path, output, "my-repo")
70
+
71
+ assert result is True
72
+ assert output.exists()
73
+ with tarfile.open(output, "r:gz") as tf:
74
+ names = tf.getnames()
75
+ assert all(name.startswith("my-repo/.git") for name in names)
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_create_tar_archive_returns_false_when_git_dir_missing() -> None:
80
+ with tempfile.TemporaryDirectory() as tmp:
81
+ repo_path = Path(tmp) / "no-git-here"
82
+ repo_path.mkdir()
83
+ output = Path(tmp) / "archive.tar.gz"
84
+
85
+ result = await _create_tar_archive(repo_path, output, "my-repo")
86
+
87
+ assert result is False
88
+ assert not output.exists()
89
+
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_create_tar_archive_returns_false_on_io_error() -> None:
93
+ with tempfile.TemporaryDirectory() as tmp:
94
+ repo_path = _fake_git_repo(Path(tmp))
95
+ invalid_output = Path(tmp) / "nonexistent_dir" / "archive.tar.gz"
96
+
97
+ result = await _create_tar_archive(repo_path, invalid_output, "my-repo")
98
+
99
+ assert result is False
100
+
101
+
102
+ @pytest.mark.asyncio
103
+ async def test_file_chunks_yields_complete_file_content() -> None:
104
+ with tempfile.TemporaryDirectory() as tmp:
105
+ content = b"abcdefgh" * 10_000
106
+ file_path = Path(tmp) / "test.bin"
107
+ file_path.write_bytes(content)
108
+
109
+ received = [chunk async for chunk in _file_chunks(file_path)]
110
+
111
+ assert b"".join(received) == content
112
+
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_file_chunks_empty_file_yields_nothing() -> None:
116
+ with tempfile.TemporaryDirectory() as tmp:
117
+ file_path = Path(tmp) / "empty.bin"
118
+ file_path.write_bytes(b"")
119
+
120
+ received = [chunk async for chunk in _file_chunks(file_path)]
121
+
122
+ assert received == []
123
+
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_do_part_upload_returns_etag_on_200() -> None:
127
+ mock_session = _make_mock_session(status=200, etag='"abc-etag"')
128
+
129
+ result = await _do_part_upload(mock_session, "https://example.com/part", b"data")
130
+
131
+ assert result == '"abc-etag"'
132
+
133
+
134
+ @pytest.mark.asyncio
135
+ async def test_do_part_upload_preserves_etag_quotes_for_s3_completion() -> None:
136
+ mock_session = _make_mock_session(status=200, etag='"quoted-etag"')
137
+
138
+ result = await _do_part_upload(mock_session, "https://example.com/part", b"data")
139
+
140
+ assert result == '"quoted-etag"'
141
+ assert result is not None
142
+ assert result.startswith('"')
143
+ assert result.endswith('"')
144
+
145
+
146
+ @pytest.mark.asyncio
147
+ async def test_do_part_upload_returns_none_on_non_200_status() -> None:
148
+ mock_session = _make_mock_session(status=403, etag="")
149
+
150
+ result = await _do_part_upload(mock_session, "https://example.com/part", b"data")
151
+
152
+ assert result is None
153
+
154
+
155
+ @pytest.mark.asyncio
156
+ async def test_initiate_with_retry_returns_upload_id_on_first_attempt() -> None:
157
+ initiate = AsyncMock(return_value="upload-id-123")
158
+
159
+ result = await _initiate_with_retry(initiate)
160
+
161
+ assert result == "upload-id-123"
162
+ assert initiate.call_count == 1
163
+
164
+
165
+ @pytest.mark.asyncio
166
+ async def test_initiate_with_retry_retries_after_none_and_succeeds() -> None:
167
+ initiate = AsyncMock(side_effect=[None, None, "upload-id-ok"])
168
+
169
+ with patch(_SLEEP_PATH, new_callable=AsyncMock):
170
+ result = await _initiate_with_retry(initiate)
171
+
172
+ assert result == "upload-id-ok"
173
+ assert initiate.call_count == 3
174
+
175
+
176
+ @pytest.mark.asyncio
177
+ async def test_initiate_with_retry_returns_none_after_all_attempts_fail() -> None:
178
+ initiate = AsyncMock(return_value=None)
179
+
180
+ with patch(_SLEEP_PATH, new_callable=AsyncMock):
181
+ result = await _initiate_with_retry(initiate)
182
+
183
+ assert result is None
184
+ assert initiate.call_count == 3
185
+
186
+
187
+ @pytest.mark.asyncio
188
+ async def test_complete_with_retry_returns_true_on_first_success() -> None:
189
+ complete = AsyncMock(return_value=True)
190
+
191
+ result = await _complete_with_retry("uid", [], complete)
192
+
193
+ assert result is True
194
+ assert complete.call_count == 1
195
+
196
+
197
+ @pytest.mark.asyncio
198
+ async def test_complete_with_retry_retries_on_false_then_succeeds() -> None:
199
+ complete = AsyncMock(side_effect=[False, False, True])
200
+
201
+ with patch(_SLEEP_PATH, new_callable=AsyncMock):
202
+ result = await _complete_with_retry("uid", [], complete)
203
+
204
+ assert result is True
205
+ assert complete.call_count == 3
206
+
207
+
208
+ @pytest.mark.asyncio
209
+ async def test_complete_with_retry_returns_false_after_all_attempts_fail() -> None:
210
+ complete = AsyncMock(return_value=False)
211
+
212
+ with patch(_SLEEP_PATH, new_callable=AsyncMock):
213
+ result = await _complete_with_retry("uid", [], complete)
214
+
215
+ assert result is False
216
+ assert complete.call_count == 3
217
+
218
+
219
+ @pytest.mark.asyncio
220
+ async def test_upload_part_with_retry_returns_result_on_first_success() -> None:
221
+ with tempfile.TemporaryDirectory() as tmp:
222
+ file_path = _fake_file(Path(tmp))
223
+ config = PartUploadConfig(
224
+ file_path=file_path,
225
+ part_number=2,
226
+ start_byte=0,
227
+ end_byte=512,
228
+ upload_id="upload-id",
229
+ )
230
+ get_part_url = AsyncMock(return_value="https://s3.example.com/part")
231
+
232
+ with (
233
+ patch(_SESSION_PATH, return_value=_make_mock_session()),
234
+ patch(
235
+ "fluidattacks_core.git.upload_repo._do_part_upload",
236
+ new_callable=AsyncMock,
237
+ return_value="part-etag",
238
+ ),
239
+ ):
240
+ result = await _upload_part_with_retry(config, get_part_url)
241
+
242
+ assert result is not None
243
+ assert result.part_number == 2
244
+ assert result.etag == "part-etag"
245
+
246
+
247
+ @pytest.mark.asyncio
248
+ async def test_upload_part_with_retry_succeeds_on_second_attempt() -> None:
249
+ with tempfile.TemporaryDirectory() as tmp:
250
+ file_path = _fake_file(Path(tmp))
251
+ config = PartUploadConfig(
252
+ file_path=file_path,
253
+ part_number=1,
254
+ start_byte=0,
255
+ end_byte=512,
256
+ upload_id="upload-id",
257
+ )
258
+ get_part_url = AsyncMock(return_value="https://s3.example.com/part")
259
+
260
+ with (
261
+ patch(_SESSION_PATH, return_value=_make_mock_session()),
262
+ patch(
263
+ "fluidattacks_core.git.upload_repo._do_part_upload",
264
+ new_callable=AsyncMock,
265
+ side_effect=[None, "recovered-etag"],
266
+ ),
267
+ patch(_SLEEP_PATH, new_callable=AsyncMock),
268
+ ):
269
+ result = await _upload_part_with_retry(config, get_part_url)
270
+
271
+ assert result is not None
272
+ assert result.etag == "recovered-etag"
273
+
274
+
275
+ @pytest.mark.asyncio
276
+ async def test_upload_part_with_retry_returns_none_after_all_attempts_exhausted() -> None:
277
+ with tempfile.TemporaryDirectory() as tmp:
278
+ file_path = _fake_file(Path(tmp))
279
+ config = PartUploadConfig(
280
+ file_path=file_path,
281
+ part_number=1,
282
+ start_byte=0,
283
+ end_byte=512,
284
+ upload_id="upload-id",
285
+ )
286
+ get_part_url = AsyncMock(return_value="https://s3.example.com/part")
287
+
288
+ with (
289
+ patch(_SESSION_PATH, return_value=_make_mock_session()),
290
+ patch(
291
+ "fluidattacks_core.git.upload_repo._do_part_upload",
292
+ new_callable=AsyncMock,
293
+ return_value=None,
294
+ ),
295
+ patch(_SLEEP_PATH, new_callable=AsyncMock),
296
+ ):
297
+ result = await _upload_part_with_retry(config, get_part_url)
298
+
299
+ assert result is None
300
+
301
+
302
+ @pytest.mark.asyncio
303
+ async def test_upload_part_with_retry_skips_attempt_when_presigned_url_empty() -> None:
304
+ with tempfile.TemporaryDirectory() as tmp:
305
+ file_path = _fake_file(Path(tmp))
306
+ config = PartUploadConfig(
307
+ file_path=file_path,
308
+ part_number=1,
309
+ start_byte=0,
310
+ end_byte=512,
311
+ upload_id="upload-id",
312
+ )
313
+ get_part_url = AsyncMock(return_value="")
314
+
315
+ with (
316
+ patch(_SESSION_PATH, return_value=_make_mock_session()),
317
+ patch(_SLEEP_PATH, new_callable=AsyncMock),
318
+ ):
319
+ result = await _upload_part_with_retry(config, get_part_url)
320
+
321
+ assert result is None
322
+
323
+
324
+ @pytest.mark.asyncio
325
+ async def test_upload_all_parts_collects_results_indexed_by_part_number() -> None:
326
+ with tempfile.TemporaryDirectory() as tmp:
327
+ file_path = _fake_file(Path(tmp), content=b"x" * 100)
328
+
329
+ def _make_result(config: PartUploadConfig, _: object) -> UploadPartResult:
330
+ return UploadPartResult(
331
+ part_number=config.part_number, etag=f"etag-{config.part_number}"
332
+ )
333
+
334
+ with patch(_PART_RETRY_PATH, new_callable=AsyncMock, side_effect=_make_result):
335
+ result = await _upload_all_parts(
336
+ file_path=file_path,
337
+ file_size=100,
338
+ upload_id="uid",
339
+ get_part_url=AsyncMock(return_value="url"),
340
+ part_size=100,
341
+ )
342
+
343
+ assert result is not None
344
+ assert result[0].part_number == 1
345
+ assert result[0].etag == "etag-1"
346
+
347
+
348
+ @pytest.mark.asyncio
349
+ async def test_upload_all_parts_returns_none_when_any_part_fails() -> None:
350
+ with tempfile.TemporaryDirectory() as tmp:
351
+ file_path = _fake_file(Path(tmp), content=b"x" * 100)
352
+
353
+ with patch(_PART_RETRY_PATH, new_callable=AsyncMock, return_value=None):
354
+ result = await _upload_all_parts(
355
+ file_path=file_path,
356
+ file_size=100,
357
+ upload_id="uid",
358
+ get_part_url=AsyncMock(return_value="url"),
359
+ part_size=100,
360
+ )
361
+
362
+ assert result is None
363
+
364
+
365
+ @pytest.mark.asyncio
366
+ async def test_upload_repo_to_s3_returns_false_for_nonexistent_path() -> None:
367
+ result = await upload_repo_to_s3(
368
+ repo_path=Path("/does/not/exist"),
369
+ archive_name="test",
370
+ upload_url="https://example.com",
371
+ )
372
+
373
+ assert result is False
374
+
375
+
376
+ @pytest.mark.asyncio
377
+ async def test_upload_repo_to_s3_returns_false_when_upload_url_missing_for_small_file() -> None:
378
+ with tempfile.TemporaryDirectory() as tmp:
379
+ repo_path = _fake_git_repo(Path(tmp))
380
+
381
+ result = await upload_repo_to_s3(repo_path=repo_path, archive_name="test")
382
+
383
+ assert result is False
384
+
385
+
386
+ @pytest.mark.asyncio
387
+ async def test_upload_repo_to_s3_returns_false_when_multipart_missing_for_large_file() -> None:
388
+ with tempfile.TemporaryDirectory() as tmp:
389
+ repo_path = _fake_git_repo(Path(tmp))
390
+
391
+ with patch(_THRESHOLD_PATH, 0):
392
+ result = await upload_repo_to_s3(
393
+ repo_path=repo_path,
394
+ archive_name="test",
395
+ upload_url="https://example.com",
396
+ )
397
+
398
+ assert result is False
399
+
400
+
401
+ @pytest.mark.asyncio
402
+ async def test_upload_repo_to_s3_single_part_returns_true_on_200() -> None:
403
+ with tempfile.TemporaryDirectory() as tmp:
404
+ repo_path = _fake_git_repo(Path(tmp))
405
+
406
+ with patch(_SESSION_PATH, return_value=_make_mock_session(status=200)):
407
+ result = await upload_repo_to_s3(
408
+ repo_path=repo_path,
409
+ archive_name="test-repo",
410
+ upload_url="https://s3.example.com/upload",
411
+ )
412
+
413
+ assert result is True
414
+
415
+
416
+ @pytest.mark.asyncio
417
+ async def test_upload_repo_to_s3_single_part_returns_false_on_non_200() -> None:
418
+ with tempfile.TemporaryDirectory() as tmp:
419
+ repo_path = _fake_git_repo(Path(tmp))
420
+
421
+ with patch(_SESSION_PATH, return_value=_make_mock_session(status=500)):
422
+ result = await upload_repo_to_s3(
423
+ repo_path=repo_path,
424
+ archive_name="test-repo",
425
+ upload_url="https://s3.example.com/upload",
426
+ )
427
+
428
+ assert result is False
429
+
430
+
431
+ @pytest.mark.asyncio
432
+ async def test_upload_repo_to_s3_multipart_path_returns_true_on_success() -> None:
433
+ with tempfile.TemporaryDirectory() as tmp:
434
+ repo_path = _fake_git_repo(Path(tmp))
435
+
436
+ multipart = MultipartCallbacks(
437
+ initiate_upload=AsyncMock(return_value="upload-id"),
438
+ get_part_url=AsyncMock(return_value="https://s3.example.com/part"),
439
+ complete_upload=AsyncMock(return_value=True),
440
+ abort_upload=AsyncMock(),
441
+ )
442
+
443
+ with (
444
+ patch(_THRESHOLD_PATH, 0),
445
+ patch(
446
+ _PART_RETRY_PATH,
447
+ new_callable=AsyncMock,
448
+ return_value=UploadPartResult(part_number=1, etag="etag-1"),
449
+ ),
450
+ ):
451
+ result = await upload_repo_to_s3(
452
+ repo_path=repo_path,
453
+ archive_name="test-repo",
454
+ multipart=multipart,
455
+ )
456
+
457
+ assert result is True
458
+
459
+
460
+ @pytest.mark.asyncio
461
+ async def test_upload_repo_to_s3_multipart_calls_abort_when_parts_fail() -> None:
462
+ with tempfile.TemporaryDirectory() as tmp:
463
+ repo_path = _fake_git_repo(Path(tmp))
464
+
465
+ abort_mock = AsyncMock()
466
+ multipart = MultipartCallbacks(
467
+ initiate_upload=AsyncMock(return_value="upload-id"),
468
+ get_part_url=AsyncMock(return_value="https://s3.example.com/part"),
469
+ complete_upload=AsyncMock(return_value=True),
470
+ abort_upload=abort_mock,
471
+ )
472
+
473
+ with (
474
+ patch(_THRESHOLD_PATH, 0),
475
+ patch(_PART_RETRY_PATH, new_callable=AsyncMock, return_value=None),
476
+ ):
477
+ result = await upload_repo_to_s3(
478
+ repo_path=repo_path,
479
+ archive_name="test-repo",
480
+ multipart=multipart,
481
+ )
482
+
483
+ assert result is False
484
+ abort_mock.assert_called_once_with("upload-id")
485
+
486
+
487
+ @pytest.mark.asyncio
488
+ async def test_upload_repo_to_s3_multipart_calls_abort_when_complete_fails() -> None:
489
+ with tempfile.TemporaryDirectory() as tmp:
490
+ repo_path = _fake_git_repo(Path(tmp))
491
+
492
+ abort_mock = AsyncMock()
493
+ multipart = MultipartCallbacks(
494
+ initiate_upload=AsyncMock(return_value="upload-id"),
495
+ get_part_url=AsyncMock(return_value="https://s3.example.com/part"),
496
+ complete_upload=AsyncMock(return_value=False),
497
+ abort_upload=abort_mock,
498
+ )
499
+
500
+ with (
501
+ patch(_THRESHOLD_PATH, 0),
502
+ patch(
503
+ _PART_RETRY_PATH,
504
+ new_callable=AsyncMock,
505
+ return_value=UploadPartResult(part_number=1, etag="etag-1"),
506
+ ),
507
+ patch(_SLEEP_PATH, new_callable=AsyncMock),
508
+ ):
509
+ result = await upload_repo_to_s3(
510
+ repo_path=repo_path,
511
+ archive_name="test-repo",
512
+ multipart=multipart,
513
+ )
514
+
515
+ assert result is False
516
+ abort_mock.assert_called_once_with("upload-id")
517
+
518
+
519
+ @pytest.mark.asyncio
520
+ async def test_upload_repo_to_s3_multipart_aborts_on_unexpected_exception() -> None:
521
+ with tempfile.TemporaryDirectory() as tmp:
522
+ repo_path = _fake_git_repo(Path(tmp))
523
+
524
+ abort_mock = AsyncMock()
525
+ multipart = MultipartCallbacks(
526
+ initiate_upload=AsyncMock(return_value="upload-id"),
527
+ get_part_url=AsyncMock(return_value="https://s3.example.com/part"),
528
+ complete_upload=AsyncMock(return_value=True),
529
+ abort_upload=abort_mock,
530
+ )
531
+
532
+ with (
533
+ pytest.raises(ValueError, match="unexpected callback error"),
534
+ patch(_THRESHOLD_PATH, 0),
535
+ patch(
536
+ "fluidattacks_core.git.upload_repo._upload_all_parts",
537
+ new_callable=AsyncMock,
538
+ side_effect=ValueError("unexpected callback error"),
539
+ ),
540
+ ):
541
+ await upload_repo_to_s3(
542
+ repo_path=repo_path,
543
+ archive_name="test-repo",
544
+ multipart=multipart,
545
+ )
546
+
547
+ abort_mock.assert_called_once_with("upload-id")
548
+
549
+
550
+ def test_compute_part_size_returns_default_for_small_files() -> None:
551
+ small_file = MULTIPART_PART_SIZE * 100
552
+ assert _compute_part_size(small_file) == MULTIPART_PART_SIZE
553
+
554
+
555
+ def test_compute_part_size_scales_up_for_files_exceeding_10000_parts() -> None:
556
+ huge_file = MULTIPART_PART_SIZE * (MAX_S3_PARTS + 1)
557
+ result = _compute_part_size(huge_file)
558
+ assert result > MULTIPART_PART_SIZE
559
+ assert math.ceil(huge_file / result) <= MAX_S3_PARTS
@@ -432,7 +432,7 @@ wheels = [
432
432
 
433
433
  [[package]]
434
434
  name = "fluidattacks-core-git"
435
- version = "12.1.0"
435
+ version = "12.2.0"
436
436
  source = { editable = "." }
437
437
  dependencies = [
438
438
  { name = "aiofiles" },
@@ -467,7 +467,7 @@ requires-dist = [
467
467
  { name = "anyio", specifier = ">=4.7.0" },
468
468
  { name = "boto3", specifier = ">=1.34" },
469
469
  { name = "botocore", specifier = ">=1.40.18" },
470
- { name = "fluidattacks-core-http", specifier = ">=12.0.0" },
470
+ { name = "fluidattacks-core-http", specifier = ">=12.2.0" },
471
471
  { name = "gitpython", specifier = ">=3.1.41" },
472
472
  { name = "pathspec", specifier = ">=0.12.1" },
473
473
  { name = "urllib3", specifier = ">=2.6.1" },
@@ -489,16 +489,16 @@ dev = [
489
489
 
490
490
  [[package]]
491
491
  name = "fluidattacks-core-http"
492
- version = "12.0.0"
492
+ version = "12.2.0"
493
493
  source = { registry = "https://pypi.org/simple" }
494
494
  dependencies = [
495
495
  { name = "aiohttp" },
496
496
  { name = "certifi" },
497
497
  { name = "urllib3" },
498
498
  ]
499
- sdist = { url = "https://files.pythonhosted.org/packages/cf/89/51755605b83f8ce87624badb0b54bdc5787b06c8fd47047c2c0c6cb50772/fluidattacks_core_http-12.0.0.tar.gz", hash = "sha256:5f4111394c194fb10a869f6ee0ea7d76f313c85a0dedbbc3d26f9c9d9d33aa13", size = 86512, upload-time = "2026-08-18T20:17:39.722Z" }
499
+ sdist = { url = "https://files.pythonhosted.org/packages/6b/99/40c8c8033bbb0b23375420c5b040821a18291b104e8e555c2ea6cd0233f5/fluidattacks_core_http-12.2.0.tar.gz", hash = "sha256:015c4de5181c5a0b56007a2fe9a5791a4bf6507401f11ceb22f264b4131f63d4", size = 89098, upload-time = "2026-09-01T22:06:31.705Z" }
500
500
  wheels = [
501
- { url = "https://files.pythonhosted.org/packages/9e/9f/42ff5b135fe9fcf30d14b08a8b68a404f9b0e7338166952ab75539911a52/fluidattacks_core_http-12.0.0-py3-none-any.whl", hash = "sha256:e6f41f6a1683fa04f0867e0867d6922be2332f776ef651bec12d56c5d18caff2", size = 3707, upload-time = "2026-08-18T20:17:41.207Z" },
501
+ { url = "https://files.pythonhosted.org/packages/04/bd/82c05bd3ba0a6d7eb86ec8d0ce8294c515bade2b53782de51454dddce678/fluidattacks_core_http-12.2.0-py3-none-any.whl", hash = "sha256:31e5600e0dc7d060f9b6a0df92f19f2516ecded0e341809613533c605939594a", size = 5143, upload-time = "2026-09-01T22:06:32.53Z" },
502
502
  ]
503
503
 
504
504
  [[package]]
@@ -1,24 +0,0 @@
1
- {
2
- nixpkgs,
3
- src,
4
- workspace_root,
5
- }:
6
- {
7
- inherit src workspace_root;
8
- root_path = "develops/fluidattacks-core/python/git";
9
- module_name = "fluidattacks_core";
10
- native.nativeBuildInputs = [ nixpkgs.git ];
11
- pypi_token = {
12
- method = "oidc";
13
- };
14
- override = {
15
- checks = {
16
- basedpyright = false;
17
- };
18
- configs = {
19
- import_linter = ../import-linter.cfg;
20
- mypy = ../mypy.ini;
21
- ruff = ../ruff.toml;
22
- };
23
- };
24
- }
@@ -1,13 +0,0 @@
1
- path_filter: src:
2
- path_filter {
3
- root = src;
4
- include = [
5
- "fluidattacks_core"
6
- "tests"
7
- "import-linter.cfg"
8
- "mypy.ini"
9
- "pyproject.toml"
10
- "ruff.toml"
11
- "uv.lock"
12
- ];
13
- }
@@ -1,303 +0,0 @@
1
- {
2
- "nodes": {
3
- "flake-parts": {
4
- "inputs": {
5
- "nixpkgs-lib": "nixpkgs-lib"
6
- },
7
- "locked": {
8
- "lastModified": 1762810396,
9
- "narHash": "sha256-dxFVgQPG+R72dkhXTtqUm7KpxElw3u6E+YlQ2WaDgt8=",
10
- "owner": "hercules-ci",
11
- "repo": "flake-parts",
12
- "rev": "0bdadb1b265fb4143a75bd1ec7d8c915898a9923",
13
- "type": "github"
14
- },
15
- "original": {
16
- "owner": "hercules-ci",
17
- "repo": "flake-parts",
18
- "type": "github"
19
- }
20
- },
21
- "nix_filter": {
22
- "locked": {
23
- "lastModified": 1757882181,
24
- "narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
25
- "owner": "numtide",
26
- "repo": "nix-filter",
27
- "rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
28
- "type": "github"
29
- },
30
- "original": {
31
- "owner": "numtide",
32
- "repo": "nix-filter",
33
- "type": "github"
34
- }
35
- },
36
- "nixpkgs": {
37
- "locked": {
38
- "lastModified": 1758690382,
39
- "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
40
- "owner": "nixos",
41
- "repo": "nixpkgs",
42
- "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
43
- "type": "github"
44
- },
45
- "original": {
46
- "owner": "nixos",
47
- "ref": "nixos-unstable",
48
- "repo": "nixpkgs",
49
- "type": "github"
50
- }
51
- },
52
- "nixpkgs-lib": {
53
- "locked": {
54
- "lastModified": 1761765539,
55
- "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
56
- "owner": "nix-community",
57
- "repo": "nixpkgs.lib",
58
- "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
59
- "type": "github"
60
- },
61
- "original": {
62
- "owner": "nix-community",
63
- "repo": "nixpkgs.lib",
64
- "type": "github"
65
- }
66
- },
67
- "nixpkgs_2": {
68
- "locked": {
69
- "lastModified": 1758690382,
70
- "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
71
- "owner": "nixos",
72
- "repo": "nixpkgs",
73
- "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
74
- "type": "github"
75
- },
76
- "original": {
77
- "owner": "nixos",
78
- "ref": "nixos-unstable",
79
- "repo": "nixpkgs",
80
- "type": "github"
81
- }
82
- },
83
- "nixpkgs_3": {
84
- "locked": {
85
- "lastModified": 1761373498,
86
- "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=",
87
- "owner": "nixos",
88
- "repo": "nixpkgs",
89
- "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce",
90
- "type": "github"
91
- },
92
- "original": {
93
- "owner": "nixos",
94
- "ref": "nixos-unstable",
95
- "repo": "nixpkgs",
96
- "type": "github"
97
- }
98
- },
99
- "nixpkgs_flake": {
100
- "locked": {
101
- "lastModified": 1778253243,
102
- "narHash": "sha256-KkH2QvU6iRLDTGGEwH5jnMqHWj4zMTuHxi1S1zdr01w=",
103
- "owner": "nixos",
104
- "repo": "nixpkgs",
105
- "rev": "4fcda7103530b23c5ca44286344912082aad4220",
106
- "type": "github"
107
- },
108
- "original": {
109
- "owner": "nixos",
110
- "repo": "nixpkgs",
111
- "type": "github"
112
- }
113
- },
114
- "pyproject-build-systems": {
115
- "inputs": {
116
- "nixpkgs": "nixpkgs",
117
- "pyproject-nix": "pyproject-nix",
118
- "uv2nix": "uv2nix"
119
- },
120
- "locked": {
121
- "lastModified": 1761781027,
122
- "narHash": "sha256-YDvxPAm2WnxrznRqWwHLjryBGG5Ey1ATEJXrON+TWt8=",
123
- "owner": "pyproject-nix",
124
- "repo": "build-system-pkgs",
125
- "rev": "795a980d25301e5133eca37adae37283ec3c8e66",
126
- "type": "github"
127
- },
128
- "original": {
129
- "owner": "pyproject-nix",
130
- "repo": "build-system-pkgs",
131
- "type": "github"
132
- }
133
- },
134
- "pyproject-nix": {
135
- "inputs": {
136
- "nixpkgs": [
137
- "python_builder",
138
- "pyproject-build-systems",
139
- "nixpkgs"
140
- ]
141
- },
142
- "locked": {
143
- "lastModified": 1758265079,
144
- "narHash": "sha256-amLaLNwKSZPShQHzfgmc/9o76dU8xzN0743dWgvYlr8=",
145
- "owner": "nix-community",
146
- "repo": "pyproject.nix",
147
- "rev": "02e9418fd4af638447dca4b17b1280da95527fc9",
148
- "type": "github"
149
- },
150
- "original": {
151
- "owner": "nix-community",
152
- "repo": "pyproject.nix",
153
- "type": "github"
154
- }
155
- },
156
- "pyproject-nix_2": {
157
- "inputs": {
158
- "nixpkgs": "nixpkgs_2"
159
- },
160
- "locked": {
161
- "lastModified": 1762427963,
162
- "narHash": "sha256-CkPlAbIQ87wmjy5qHibfzk4DmMGBNqFer+lLfXjpP5M=",
163
- "owner": "pyproject-nix",
164
- "repo": "pyproject.nix",
165
- "rev": "4540ea004e04fcd12dd2738d51383d10f956f7b9",
166
- "type": "github"
167
- },
168
- "original": {
169
- "owner": "pyproject-nix",
170
- "repo": "pyproject.nix",
171
- "type": "github"
172
- }
173
- },
174
- "pyproject-nix_3": {
175
- "inputs": {
176
- "nixpkgs": [
177
- "python_builder",
178
- "uv2nix",
179
- "nixpkgs"
180
- ]
181
- },
182
- "locked": {
183
- "lastModified": 1760402624,
184
- "narHash": "sha256-jF6UKLs2uGc2rtved8Vrt58oTWjTQoAssuYs/0578Z4=",
185
- "owner": "pyproject-nix",
186
- "repo": "pyproject.nix",
187
- "rev": "84c4ea102127c77058ea1ed7be7300261fafc7d2",
188
- "type": "github"
189
- },
190
- "original": {
191
- "owner": "pyproject-nix",
192
- "repo": "pyproject.nix",
193
- "type": "github"
194
- }
195
- },
196
- "python_builder": {
197
- "inputs": {
198
- "nix_filter": "nix_filter",
199
- "nixpkgs_flake": "nixpkgs_flake",
200
- "pyproject-build-systems": "pyproject-build-systems",
201
- "pyproject-nix": "pyproject-nix_2",
202
- "shell-helpers": "shell-helpers",
203
- "uv2nix": "uv2nix_2"
204
- },
205
- "locked": {
206
- "dir": "common/utils/python-builder",
207
- "lastModified": 1787078362,
208
- "narHash": "sha256-HYL7Rn6SuNjYxkt/PwFLksZjsauCiY+ENbOWryLsIlw=",
209
- "ref": "refs/heads/trunk",
210
- "rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
211
- "shallow": true,
212
- "type": "git",
213
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
214
- },
215
- "original": {
216
- "dir": "common/utils/python-builder",
217
- "rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
218
- "shallow": true,
219
- "type": "git",
220
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
221
- }
222
- },
223
- "root": {
224
- "inputs": {
225
- "python_builder": "python_builder"
226
- }
227
- },
228
- "shell-helpers": {
229
- "inputs": {
230
- "flake-parts": "flake-parts",
231
- "nixpkgs": [
232
- "python_builder",
233
- "nixpkgs_flake"
234
- ]
235
- },
236
- "locked": {
237
- "dir": "common/utils/shell-helpers",
238
- "lastModified": 1762806066,
239
- "narHash": "sha256-Wz99Fl7SpPU1NkTHJ36XOo6DjOEAMXCw3ThRABOS2Hs=",
240
- "ref": "refs/heads/trunk",
241
- "rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
242
- "shallow": true,
243
- "type": "git",
244
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
245
- },
246
- "original": {
247
- "dir": "common/utils/shell-helpers",
248
- "rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
249
- "shallow": true,
250
- "type": "git",
251
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
252
- }
253
- },
254
- "uv2nix": {
255
- "inputs": {
256
- "nixpkgs": [
257
- "python_builder",
258
- "pyproject-build-systems",
259
- "nixpkgs"
260
- ],
261
- "pyproject-nix": [
262
- "python_builder",
263
- "pyproject-build-systems",
264
- "pyproject-nix"
265
- ]
266
- },
267
- "locked": {
268
- "lastModified": 1758933732,
269
- "narHash": "sha256-HAmm1GBS1myZCFuog0DC2ZLaynvZtiUI2Crmo+cdQI0=",
270
- "owner": "pyproject-nix",
271
- "repo": "uv2nix",
272
- "rev": "273ce18f913d8559e0d04f820d724308966d7c4d",
273
- "type": "github"
274
- },
275
- "original": {
276
- "owner": "pyproject-nix",
277
- "repo": "uv2nix",
278
- "type": "github"
279
- }
280
- },
281
- "uv2nix_2": {
282
- "inputs": {
283
- "nixpkgs": "nixpkgs_3",
284
- "pyproject-nix": "pyproject-nix_3"
285
- },
286
- "locked": {
287
- "lastModified": 1761872265,
288
- "narHash": "sha256-i25GRgp2vUOebY70L3NTAgkd+Pr1hnn5xM3qHxH0ONU=",
289
- "owner": "pyproject-nix",
290
- "repo": "uv2nix",
291
- "rev": "74dfb62871be152ad3673b143b0cc56105a4f3c5",
292
- "type": "github"
293
- },
294
- "original": {
295
- "owner": "pyproject-nix",
296
- "repo": "uv2nix",
297
- "type": "github"
298
- }
299
- }
300
- },
301
- "root": "root",
302
- "version": 7
303
- }
@@ -1,30 +0,0 @@
1
- {
2
- description = "Fluid Attacks Core git package.";
3
-
4
- inputs = {
5
- python_builder = {
6
- url = "git+ssh://git@gitlab.com/fluidattacks/universe?shallow=1&rev=09fd25a13309eb5e3b5b2a061bb340df3ae5d281&dir=common/utils/python-builder";
7
- };
8
- };
9
-
10
- outputs =
11
- { self, ... }@inputs:
12
- let
13
- build_args =
14
- {
15
- system,
16
- python_version,
17
- nixpkgs,
18
- builders,
19
- scripts,
20
- }:
21
- import ./build {
22
- inherit nixpkgs;
23
- src = import ./build/filter.nix nixpkgs.nix-filter self;
24
- workspace_root = ./.;
25
- };
26
- in
27
- {
28
- packages = inputs.python_builder.outputs.build build_args;
29
- };
30
- }