nucliadb-utils 4.0.3.post573__py3-none-any.whl → 4.0.3.post575__py3-none-any.whl

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.
@@ -1,228 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
-
20
- from math import ceil
21
- from unittest.mock import AsyncMock, MagicMock
22
-
23
- import pytest
24
- from nucliadb_protos.noderesources_pb2 import Resource as BrainResource
25
- from nucliadb_protos.noderesources_pb2 import ResourceID
26
- from nucliadb_protos.nodewriter_pb2 import IndexMessage
27
- from nucliadb_protos.resources_pb2 import CloudFile
28
-
29
- from nucliadb_utils.storages.local import LocalStorageField
30
- from nucliadb_utils.storages.storage import (
31
- ObjectInfo,
32
- Storage,
33
- StorageField,
34
- iter_and_add_size,
35
- iter_in_chunk_size,
36
- )
37
-
38
-
39
- class TestStorageField:
40
- @pytest.fixture
41
- def storage(self):
42
- yield AsyncMock(source=0)
43
-
44
- @pytest.fixture
45
- def field(self):
46
- yield MagicMock(uri="uri")
47
-
48
- @pytest.fixture
49
- def storage_field(self, storage, field):
50
- yield LocalStorageField(storage, "bucket", "fullkey", field)
51
-
52
- @pytest.mark.asyncio
53
- async def test_delete(self, storage_field: StorageField, storage):
54
- await storage_field.delete()
55
- storage.delete_upload.assert_called_once_with("uri", "bucket")
56
-
57
-
58
- class StorageTest(Storage):
59
- def __init__(self):
60
- self.source = 0
61
- self.field_klass = lambda: MagicMock()
62
- self.deadletter_bucket = "deadletter_bucket"
63
- self.indexing_bucket = "indexing_bucket"
64
- self.delete_upload = AsyncMock()
65
- self.uploadbytes = AsyncMock()
66
- self.move = AsyncMock()
67
-
68
- def get_bucket_name(self, kbid):
69
- return "bucket"
70
-
71
- async def iterate_objects(self, bucket_name, prefix):
72
- yield ObjectInfo(name="uri")
73
-
74
- async def download(self, bucket_name, uri):
75
- br = BrainResource(labels=["label"])
76
- yield br.SerializeToString()
77
-
78
- async def create_kb(self, kbid):
79
- return True
80
-
81
- async def delete_kb(self, kbid):
82
- return True
83
-
84
- async def delete_upload(self, uri, bucket):
85
- return True
86
-
87
- async def initialize(self) -> None:
88
- pass
89
-
90
- async def finalize(self) -> None:
91
- pass
92
-
93
- async def schedule_delete_kb(self, kbid: str) -> bool:
94
- return True
95
-
96
-
97
- class TestStorage:
98
- @pytest.fixture
99
- def storage(self):
100
- yield StorageTest()
101
-
102
- @pytest.mark.asyncio
103
- async def test_delete_resource(self, storage: StorageTest):
104
- await storage.delete_resource("bucket", "uri")
105
-
106
- storage.delete_upload.assert_called_once_with("uri", "bucket")
107
-
108
- @pytest.mark.asyncio
109
- async def test_indexing(self, storage: StorageTest):
110
- msg = BrainResource(resource=ResourceID(uuid="uuid"))
111
- await storage.indexing(msg, 1, "1", "kb", "shard")
112
-
113
- storage.uploadbytes.assert_called_once_with(
114
- "indexing_bucket", "index/kb/shard/uuid/1", msg.SerializeToString()
115
- )
116
-
117
- @pytest.mark.asyncio
118
- async def test_reindexing(self, storage: StorageTest):
119
- msg = BrainResource(resource=ResourceID(uuid="uuid"))
120
- await storage.reindexing(msg, "reindex_id", "1", "kb", "shard")
121
-
122
- storage.uploadbytes.assert_called_once_with(
123
- "indexing_bucket", "index/kb/shard/uuid/reindex_id", msg.SerializeToString()
124
- )
125
-
126
- @pytest.mark.asyncio
127
- async def test_get_indexing(self, storage: StorageTest):
128
- im = IndexMessage()
129
- im.node = "node"
130
- im.shard = "shard"
131
- im.txid = 0
132
- assert isinstance(await storage.get_indexing(im), BrainResource)
133
-
134
- @pytest.mark.asyncio
135
- async def test_get_indexing_storage_key(self, storage: StorageTest):
136
- im = IndexMessage()
137
- im.node = "node"
138
- im.shard = "shard"
139
- im.txid = 0
140
- im.storage_key = "index/kb/uuid/1"
141
- assert isinstance(await storage.get_indexing(im), BrainResource)
142
-
143
- @pytest.mark.asyncio
144
- async def test_delete_indexing(self, storage: StorageTest):
145
- im = IndexMessage()
146
- im.node = "node"
147
- im.txid = 0
148
- im.storage_key = "index/kb/uuid/1"
149
- await storage.delete_indexing(
150
- resource_uid="resource_uid", txid=1, kb="kb", logical_shard="logical_shard"
151
- )
152
-
153
- storage.uploadbytes.assert_called_once()
154
-
155
- @pytest.mark.asyncio
156
- async def test_download_pb(self, storage: StorageTest):
157
- assert isinstance(
158
- await storage.download_pb(
159
- LocalStorageField(storage, "bucket", "fullkey"), BrainResource
160
- ),
161
- BrainResource,
162
- )
163
-
164
- @pytest.mark.asyncio
165
- async def test_indexing_bucket_none_attributeerrror(self, storage: StorageTest):
166
- storage.indexing_bucket = None
167
- msg = BrainResource()
168
- im = IndexMessage(node="node", shard="shard", txid=0)
169
-
170
- with pytest.raises(AttributeError):
171
- await storage.indexing(msg, 1, "1", "kb", "shard")
172
-
173
- with pytest.raises(AttributeError):
174
- await storage.reindexing(msg, "reindex_id", "1", "kb", "shard")
175
-
176
- with pytest.raises(AttributeError):
177
- await storage.get_indexing(im)
178
-
179
- with pytest.raises(AttributeError):
180
- await storage.delete_indexing(
181
- resource_uid="resource_uid",
182
- txid=1,
183
- kb="kb",
184
- logical_shard="logical_shard",
185
- )
186
-
187
-
188
- async def testiter_and_add_size():
189
- cf = CloudFile()
190
-
191
- async def iter():
192
- yield b"foo"
193
- yield b"bar"
194
-
195
- cf.size = 0
196
- async for _ in iter_and_add_size(iter(), cf):
197
- pass
198
-
199
- assert cf.size == 6
200
-
201
-
202
- async def test_iter_in_chunk_size():
203
- async def iterable(total_size, *, chunk_size=1):
204
- data = b"0" * total_size
205
- for i in range(ceil(total_size / chunk_size)):
206
- chunk = data[i * chunk_size : (i + 1) * chunk_size]
207
- yield chunk
208
-
209
- chunks = [chunk async for chunk in iter_in_chunk_size(iterable(10), chunk_size=4)]
210
- assert len(chunks) == 3
211
- assert len(chunks[0]) == 4
212
- assert len(chunks[1]) == 4
213
- assert len(chunks[2]) == 2
214
-
215
- chunks = [chunk async for chunk in iter_in_chunk_size(iterable(0), chunk_size=4)]
216
- assert len(chunks) == 0
217
-
218
- # Try with an iterable that yields chunks bigger than the chunk size
219
- chunks = [
220
- chunk
221
- async for chunk in iter_in_chunk_size(
222
- iterable(total_size=12, chunk_size=10), chunk_size=4
223
- )
224
- ]
225
- assert len(chunks) == 3
226
- assert len(chunks[0]) == 4
227
- assert len(chunks[1]) == 4
228
- assert len(chunks[2]) == 4
@@ -1,72 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- import asyncio
20
- import random
21
- import time
22
-
23
- import pytest
24
-
25
- from nucliadb_utils.asyncio_utils import ConcurrentRunner, run_concurrently
26
-
27
-
28
- async def test_run_concurrently():
29
- async def mycoro(value):
30
- await asyncio.sleep(random.uniform(0.0, 0.5))
31
- return value
32
-
33
- tasks = [mycoro(i) for i in range(10)]
34
- results = await run_concurrently(tasks)
35
- assert results == list(range(10))
36
-
37
-
38
- async def test_concurrent_runner():
39
- async def mycoro(value):
40
- await asyncio.sleep(0.5)
41
- return value
42
-
43
- # Test without max_tasks: execution time should be less than 1 second
44
- # as both should be executed in concurrently
45
- runner = ConcurrentRunner()
46
- runner.schedule(mycoro(1))
47
- runner.schedule(mycoro(2))
48
- start = time.perf_counter()
49
- results = await runner.wait()
50
- end = time.perf_counter()
51
- assert results == [1, 2]
52
- assert end - start < 1
53
-
54
- # Test with max_tasks: execution time should be at least 1 second
55
- runner = ConcurrentRunner(max_tasks=1)
56
- runner.schedule(mycoro(1))
57
- runner.schedule(mycoro(2))
58
- start = time.perf_counter()
59
- results = await runner.wait()
60
- end = time.perf_counter()
61
- assert results == [1, 2]
62
- assert end - start >= 1
63
-
64
- # Exception should be raised if any of the coroutines raises an exception
65
- with pytest.raises(ValueError):
66
-
67
- async def coro_with_exception():
68
- raise ValueError("Test")
69
-
70
- runner = ConcurrentRunner()
71
- runner.schedule(coro_with_exception())
72
- await runner.wait()
@@ -1,159 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
-
20
- from unittest.mock import AsyncMock, Mock, patch
21
-
22
- import pytest
23
- from fastapi import WebSocket
24
- from starlette.exceptions import HTTPException
25
- from starlette.requests import Request
26
-
27
- from nucliadb_utils import authentication
28
-
29
-
30
- class TestNucliaCloudAuthenticationBackend:
31
- @pytest.fixture()
32
- def backend(self):
33
- return authentication.NucliaCloudAuthenticationBackend()
34
-
35
- @pytest.fixture()
36
- def req(self):
37
- return Mock(headers={})
38
-
39
- @pytest.mark.asyncio
40
- async def test_authenticate(
41
- self, backend: authentication.NucliaCloudAuthenticationBackend, req
42
- ):
43
- assert await backend.authenticate(req) is None
44
-
45
- @pytest.mark.asyncio
46
- async def test_authenticate_with_user(
47
- self, backend: authentication.NucliaCloudAuthenticationBackend, req
48
- ):
49
- req.headers = {
50
- backend.roles_header: "admin",
51
- backend.user_header: "user",
52
- }
53
-
54
- creds, user = await backend.authenticate(req)
55
-
56
- assert creds.scopes == ["admin"]
57
- assert user.username == "user"
58
-
59
- @pytest.mark.asyncio
60
- async def test_authenticate_with_anon(
61
- self, backend: authentication.NucliaCloudAuthenticationBackend, req
62
- ):
63
- req.headers = {backend.roles_header: "admin"}
64
-
65
- creds, user = await backend.authenticate(req)
66
-
67
- assert creds.scopes == ["admin"]
68
- assert user.username == "anonymous"
69
-
70
-
71
- def test_has_required_scope():
72
- conn = Mock(auth=Mock(scopes=["admin"]))
73
- assert authentication.has_required_scope(conn, ["admin"])
74
- assert not authentication.has_required_scope(conn, ["foobar"])
75
-
76
-
77
- class TestRequires:
78
- def test_requires_sync(self):
79
- req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
80
-
81
- assert authentication.requires(["admin"])(lambda request: None)(req) is None
82
-
83
- # test passed as kwargs
84
- assert (
85
- authentication.requires(["admin"])(lambda request: None)(request=req)
86
- is None
87
- )
88
-
89
- def test_requires_sync_returns_status(self):
90
- req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
91
-
92
- with pytest.raises(HTTPException):
93
- assert authentication.requires(["foobar"])(lambda request: None)(req)
94
-
95
- def test_requires_sync_returns_redirect(self):
96
- req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
97
-
98
- with patch.object(req, "url_for", return_value="http://foobar"):
99
- resp = authentication.requires(["foobar"], redirect="/foobar")(
100
- lambda request: None
101
- )(req)
102
- assert resp.status_code == 303
103
-
104
- @pytest.mark.asyncio
105
- async def test_requires_async(self):
106
- req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
107
-
108
- async def noop(request): ...
109
-
110
- assert await authentication.requires(["admin"])(noop)(request=req) is None
111
-
112
- @pytest.mark.asyncio
113
- async def test_requires_async_returns_status(self):
114
- req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
115
-
116
- async def noop(request): ...
117
-
118
- with pytest.raises(HTTPException):
119
- assert await authentication.requires(["foobar"])(noop)(request=req)
120
-
121
- @pytest.mark.asyncio
122
- async def test_requires_async_returns_redirect(self):
123
- req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
124
-
125
- async def noop(request): ...
126
-
127
- with patch.object(req, "url_for", return_value="http://foobar"):
128
- resp = await authentication.requires(["foobar"], redirect="/foobar")(noop)(
129
- request=req
130
- )
131
- assert resp.status_code == 303
132
-
133
- @pytest.mark.asyncio
134
- async def test_requires_ws(self):
135
- ws = AsyncMock()
136
- req = WebSocket(
137
- {"type": "websocket", "auth": Mock(scopes=["admin"]), "websocket": ws},
138
- receive=AsyncMock(),
139
- send=AsyncMock(),
140
- )
141
-
142
- async def noop(websocket): ...
143
-
144
- assert await authentication.requires(["admin"])(noop)(req) is None
145
-
146
- @pytest.mark.asyncio
147
- async def test_requires_ws_fail(self):
148
- req = WebSocket(
149
- {"type": "websocket", "auth": Mock(scopes=["admin"])},
150
- receive=AsyncMock(),
151
- send=AsyncMock(),
152
- )
153
-
154
- async def noop(websocket): ...
155
-
156
- with patch.object(req, "close", return_value=None):
157
- assert await authentication.requires(["notallowed"])(noop)(req) is None
158
-
159
- req.close.assert_called_once()
@@ -1,45 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- #
20
-
21
- from nucliadb_utils.helpers import async_gen_lookahead
22
-
23
-
24
- async def test_async_gen_lookahead():
25
- async def gen(n):
26
- for i in range(n):
27
- yield f"{i}".encode()
28
-
29
- assert [item async for item in async_gen_lookahead(gen(0))] == []
30
- assert [item async for item in async_gen_lookahead(gen(1))] == [(b"0", True)]
31
- assert [item async for item in async_gen_lookahead(gen(2))] == [
32
- (b"0", False),
33
- (b"1", True),
34
- ]
35
-
36
-
37
- async def test_async_gen_lookahead_last_chunk_is_empty():
38
- async def gen():
39
- for chunk in [b"empty", None, b"chunk", b""]:
40
- yield chunk
41
-
42
- assert [item async for item in async_gen_lookahead(gen())] == [
43
- (b"empty", False),
44
- (b"chunk", True),
45
- ]
@@ -1,154 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
-
20
- import asyncio
21
- import time
22
- from unittest.mock import AsyncMock, MagicMock, patch
23
-
24
- import pytest
25
-
26
- from nucliadb_utils import nats
27
-
28
- pytestmark = pytest.mark.unit
29
-
30
-
31
- class TestNatsConnectionManager:
32
- @pytest.fixture()
33
- def nats_conn(self):
34
- conn = MagicMock()
35
- conn.drain = AsyncMock()
36
- conn.close = AsyncMock()
37
- with patch("nucliadb_utils.nats.nats.connect", return_value=conn):
38
- yield conn
39
-
40
- @pytest.fixture()
41
- def js(self):
42
- conn = AsyncMock()
43
- with patch(
44
- "nucliadb_utils.nats.get_traced_jetstream",
45
- return_value=conn,
46
- ):
47
- yield conn
48
-
49
- @pytest.fixture()
50
- def manager(self, nats_conn, js):
51
- yield nats.NatsConnectionManager(service_name="test", nats_servers=["test"])
52
-
53
- async def test_initialize(self, manager: nats.NatsConnectionManager, nats_conn):
54
- await manager.initialize()
55
-
56
- assert manager.nc == nats_conn
57
-
58
- async def test_lifecycle_finalize(
59
- self, manager: nats.NatsConnectionManager, nats_conn, js
60
- ):
61
- await manager.initialize()
62
-
63
- cb = AsyncMock()
64
- lost_cb = AsyncMock()
65
- await manager.subscribe(
66
- subject="subject",
67
- queue="queue",
68
- stream="stream",
69
- cb=cb,
70
- subscription_lost_cb=lost_cb,
71
- flow_control=True,
72
- )
73
-
74
- js.subscribe.assert_called_once_with(
75
- subject="subject",
76
- queue="queue",
77
- stream="stream",
78
- cb=cb,
79
- manual_ack=True,
80
- flow_control=True,
81
- config=None,
82
- )
83
-
84
- await manager.reconnected_cb()
85
- lost_cb.assert_called_once()
86
-
87
- await manager.finalize()
88
-
89
- nats_conn.drain.assert_called_once()
90
- nats_conn.close.assert_called_once()
91
-
92
- async def test_healthy(self, manager: nats.NatsConnectionManager):
93
- await manager.initialize()
94
-
95
- assert manager.healthy()
96
-
97
- manager._healthy = False
98
- assert not manager.healthy()
99
-
100
- manager._healthy = True
101
- manager._last_unhealthy = time.monotonic() - 100
102
- assert not manager.healthy()
103
-
104
- manager._last_unhealthy = None
105
- manager._nc.is_connected = False
106
- assert manager.healthy()
107
- assert manager._last_unhealthy is not None
108
-
109
- async def test_unsubscribe(
110
- self, manager: nats.NatsConnectionManager, nats_conn, js
111
- ):
112
- await manager.initialize()
113
-
114
- cb = AsyncMock()
115
- lost_cb = AsyncMock()
116
- sub = await manager.subscribe(
117
- subject="subject",
118
- queue="queue",
119
- stream="stream",
120
- cb=cb,
121
- subscription_lost_cb=lost_cb,
122
- flow_control=True,
123
- )
124
- assert len(manager._subscriptions) == 1
125
-
126
- await manager.unsubscribe(sub)
127
-
128
- sub.unsubscribe.assert_awaited_once()
129
- assert len(manager._subscriptions) == 0
130
-
131
- await manager.finalize()
132
-
133
- nats_conn.drain.assert_called_once()
134
- nats_conn.close.assert_called_once()
135
-
136
-
137
- async def test_message_progress_updater():
138
- in_progress = AsyncMock()
139
- msg = MagicMock(in_progress=in_progress, _ackd=False)
140
-
141
- async with nats.MessageProgressUpdater(msg, 0.05):
142
- await asyncio.sleep(0.07)
143
-
144
- in_progress.assert_awaited_once()
145
-
146
-
147
- async def test_message_progress_updater_does_not_update_ack():
148
- in_progress = AsyncMock()
149
- msg = MagicMock(in_progress=in_progress, _ackd=True)
150
-
151
- async with nats.MessageProgressUpdater(msg, 0.05):
152
- await asyncio.sleep(0.07)
153
-
154
- in_progress.assert_not_awaited()