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.
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/.gitignore +2 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/PKG-INFO +2 -2
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/import-linter.cfg +1 -11
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/pyproject.toml +2 -2
- fluidattacks_core_git-12.2.0/tests/test_remote.py +106 -0
- fluidattacks_core_git-12.2.0/tests/test_upload_repo.py +559 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/uv.lock +5 -5
- fluidattacks_core_git-12.1.0/build/default.nix +0 -24
- fluidattacks_core_git-12.1.0/build/filter.nix +0 -13
- fluidattacks_core_git-12.1.0/flake.lock +0 -303
- fluidattacks_core_git-12.1.0/flake.nix +0 -30
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/.envrc +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/__init__.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/classes.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/clone.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/codecommit_utils.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/constants.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/delete_files.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/download_file.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/download_repo.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/https_utils.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/py.typed +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/remote.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/show_file.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/ssh_utils.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/upload_repo.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/utils.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/warp.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/mypy.ini +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/ruff.toml +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/tests/__init__.py +0 -0
- {fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/tests/test_git.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fluidattacks_core_git
|
|
3
|
-
Version: 12.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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/
|
|
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,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
|
-
}
|
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/__init__.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/classes.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/clone.py
RENAMED
|
File without changes
|
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/constants.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/delete_files.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/download_file.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/download_repo.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/https_utils.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/py.typed
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/remote.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/show_file.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/ssh_utils.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/upload_repo.py
RENAMED
|
File without changes
|
{fluidattacks_core_git-12.1.0 → fluidattacks_core_git-12.2.0}/fluidattacks_core/git/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|