nucliadb-utils 4.0.3.post586__py3-none-any.whl → 4.0.3.post589__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.
- nucliadb_utils/asyncio_utils.py +1 -3
- nucliadb_utils/audit/audit.py +1 -0
- nucliadb_utils/audit/basic.py +2 -4
- nucliadb_utils/audit/stream.py +5 -11
- nucliadb_utils/authentication.py +7 -21
- nucliadb_utils/cache/nats.py +2 -6
- nucliadb_utils/debug.py +1 -4
- nucliadb_utils/fastapi/versioning.py +5 -4
- nucliadb_utils/featureflagging.py +1 -3
- nucliadb_utils/grpc.py +1 -3
- nucliadb_utils/helpers.py +1 -1
- nucliadb_utils/indexing.py +1 -1
- nucliadb_utils/nats.py +1 -3
- nucliadb_utils/run.py +1 -3
- nucliadb_utils/settings.py +3 -9
- nucliadb_utils/signals.py +1 -3
- nucliadb_utils/storages/exceptions.py +1 -3
- nucliadb_utils/storages/gcs.py +15 -40
- nucliadb_utils/storages/local.py +5 -14
- nucliadb_utils/storages/nuclia.py +1 -1
- nucliadb_utils/storages/s3.py +11 -29
- nucliadb_utils/storages/storage.py +16 -45
- nucliadb_utils/tests/asyncbenchmark.py +4 -11
- nucliadb_utils/tests/gcs.py +1 -3
- nucliadb_utils/tests/nats.py +4 -15
- nucliadb_utils/transaction.py +5 -13
- nucliadb_utils/utilities.py +4 -13
- {nucliadb_utils-4.0.3.post586.dist-info → nucliadb_utils-4.0.3.post589.dist-info}/METADATA +3 -3
- {nucliadb_utils-4.0.3.post586.dist-info → nucliadb_utils-4.0.3.post589.dist-info}/RECORD +32 -32
- {nucliadb_utils-4.0.3.post586.dist-info → nucliadb_utils-4.0.3.post589.dist-info}/WHEEL +0 -0
- {nucliadb_utils-4.0.3.post586.dist-info → nucliadb_utils-4.0.3.post589.dist-info}/top_level.txt +0 -0
- {nucliadb_utils-4.0.3.post586.dist-info → nucliadb_utils-4.0.3.post589.dist-info}/zip-safe +0 -0
nucliadb_utils/asyncio_utils.py
CHANGED
@@ -62,9 +62,7 @@ class ConcurrentRunner:
|
|
62
62
|
return results
|
63
63
|
|
64
64
|
|
65
|
-
async def run_concurrently(
|
66
|
-
tasks: list[Coroutine], max_concurrent: Optional[int] = None
|
67
|
-
) -> list[Any]:
|
65
|
+
async def run_concurrently(tasks: list[Coroutine], max_concurrent: Optional[int] = None) -> list[Any]:
|
68
66
|
"""
|
69
67
|
Runs a list of coroutines concurrently, with a maximum number of tasks running.
|
70
68
|
Returns the results of the coroutines in the order they were scheduled.
|
nucliadb_utils/audit/audit.py
CHANGED
nucliadb_utils/audit/basic.py
CHANGED
@@ -20,6 +20,7 @@
|
|
20
20
|
from typing import List, Optional
|
21
21
|
|
22
22
|
from google.protobuf.timestamp_pb2 import Timestamp
|
23
|
+
|
23
24
|
from nucliadb_protos.audit_pb2 import (
|
24
25
|
AuditField,
|
25
26
|
AuditKBCounter,
|
@@ -29,7 +30,6 @@ from nucliadb_protos.audit_pb2 import (
|
|
29
30
|
from nucliadb_protos.nodereader_pb2 import SearchRequest
|
30
31
|
from nucliadb_protos.resources_pb2 import FieldID
|
31
32
|
from nucliadb_protos.writer_pb2 import BrokerMessage
|
32
|
-
|
33
33
|
from nucliadb_utils import logger
|
34
34
|
from nucliadb_utils.audit.audit import AuditStorage
|
35
35
|
|
@@ -51,9 +51,7 @@ class BasicAuditStorage(AuditStorage):
|
|
51
51
|
audit_fields: Optional[List[AuditField]] = None,
|
52
52
|
kb_counter: Optional[AuditKBCounter] = None,
|
53
53
|
):
|
54
|
-
logger.debug(
|
55
|
-
f"AUDIT {audit_type} {kbid} {user} {origin} {rid} {audit_fields} {kb_counter}"
|
56
|
-
)
|
54
|
+
logger.debug(f"AUDIT {audit_type} {kbid} {user} {origin} {rid} {audit_fields} {kb_counter}")
|
57
55
|
|
58
56
|
def report_resources(
|
59
57
|
self,
|
nucliadb_utils/audit/stream.py
CHANGED
@@ -25,6 +25,8 @@ import backoff
|
|
25
25
|
import mmh3
|
26
26
|
import nats
|
27
27
|
from google.protobuf.timestamp_pb2 import Timestamp
|
28
|
+
from opentelemetry.trace import format_trace_id, get_current_span
|
29
|
+
|
28
30
|
from nucliadb_protos.audit_pb2 import (
|
29
31
|
AuditField,
|
30
32
|
AuditKBCounter,
|
@@ -34,8 +36,6 @@ from nucliadb_protos.audit_pb2 import (
|
|
34
36
|
)
|
35
37
|
from nucliadb_protos.nodereader_pb2 import SearchRequest
|
36
38
|
from nucliadb_protos.resources_pb2 import FieldID
|
37
|
-
from opentelemetry.trace import format_trace_id, get_current_span
|
38
|
-
|
39
39
|
from nucliadb_utils import logger
|
40
40
|
from nucliadb_utils.audit.audit import AuditStorage
|
41
41
|
from nucliadb_utils.nats import get_traced_jetstream
|
@@ -89,9 +89,7 @@ class StreamAuditStorage(AuditStorage):
|
|
89
89
|
logger.info("Got reconnected to NATS {url}".format(url=self.nc.connected_url))
|
90
90
|
|
91
91
|
async def error_cb(self, e):
|
92
|
-
logger.error(
|
93
|
-
"There was an error connecting to NATS audit: {}".format(e), exc_info=True
|
94
|
-
)
|
92
|
+
logger.error("There was an error connecting to NATS audit: {}".format(e), exc_info=True)
|
95
93
|
|
96
94
|
async def closed_cb(self):
|
97
95
|
logger.info("Connection is closed on NATS")
|
@@ -149,9 +147,7 @@ class StreamAuditStorage(AuditStorage):
|
|
149
147
|
async def send(self, message: AuditRequest):
|
150
148
|
self.queue.put_nowait(message)
|
151
149
|
|
152
|
-
@backoff.on_exception(
|
153
|
-
backoff.expo, (Exception,), jitter=backoff.random_jitter, max_tries=4
|
154
|
-
)
|
150
|
+
@backoff.on_exception(backoff.expo, (Exception,), jitter=backoff.random_jitter, max_tries=4)
|
155
151
|
async def _send(self, message: AuditRequest):
|
156
152
|
if self.js is None: # pragma: no cover
|
157
153
|
raise AttributeError()
|
@@ -205,9 +201,7 @@ class StreamAuditStorage(AuditStorage):
|
|
205
201
|
account_id=None,
|
206
202
|
kb_id=kbid,
|
207
203
|
kb_source=KBSource.HOSTED,
|
208
|
-
storage=Storage(
|
209
|
-
paragraphs=kb_counter.paragraphs, fields=kb_counter.fields
|
210
|
-
),
|
204
|
+
storage=Storage(paragraphs=kb_counter.paragraphs, fields=kb_counter.fields),
|
211
205
|
)
|
212
206
|
|
213
207
|
auditrequest.trace_id = get_trace_id()
|
nucliadb_utils/authentication.py
CHANGED
@@ -56,9 +56,7 @@ class NucliaCloudAuthenticationBackend(AuthenticationBackend):
|
|
56
56
|
self.roles_header = roles_header
|
57
57
|
self.user_header = user_header
|
58
58
|
|
59
|
-
async def authenticate(
|
60
|
-
self, request: HTTPConnection
|
61
|
-
) -> Optional[Tuple[AuthCredentials, BaseUser]]:
|
59
|
+
async def authenticate(self, request: HTTPConnection) -> Optional[Tuple[AuthCredentials, BaseUser]]:
|
62
60
|
if self.roles_header not in request.headers:
|
63
61
|
return None
|
64
62
|
else:
|
@@ -97,9 +95,7 @@ def requires(
|
|
97
95
|
elif isinstance(scopes, str):
|
98
96
|
scopes_list = [scopes]
|
99
97
|
elif isinstance(scopes, list):
|
100
|
-
scopes_list = [
|
101
|
-
scope.value if isinstance(scope, Enum) else scope for scope in scopes
|
102
|
-
]
|
98
|
+
scopes_list = [scope.value if isinstance(scope, Enum) else scope for scope in scopes]
|
103
99
|
|
104
100
|
def decorator(func: typing.Callable) -> typing.Callable:
|
105
101
|
func.__required_scopes__ = scopes_list # type: ignore
|
@@ -110,16 +106,12 @@ def requires(
|
|
110
106
|
type = parameter.name
|
111
107
|
break
|
112
108
|
else:
|
113
|
-
raise Exception(
|
114
|
-
f'No "request" or "websocket" argument on function "{func}"'
|
115
|
-
)
|
109
|
+
raise Exception(f'No "request" or "websocket" argument on function "{func}"')
|
116
110
|
|
117
111
|
if type == "websocket":
|
118
112
|
# Handle websocket functions. (Always async)
|
119
113
|
@functools.wraps(func)
|
120
|
-
async def websocket_wrapper(
|
121
|
-
*args: typing.Any, **kwargs: typing.Any
|
122
|
-
) -> None:
|
114
|
+
async def websocket_wrapper(*args: typing.Any, **kwargs: typing.Any) -> None:
|
123
115
|
websocket = kwargs.get("websocket", args[idx])
|
124
116
|
assert isinstance(websocket, WebSocket)
|
125
117
|
|
@@ -133,17 +125,13 @@ def requires(
|
|
133
125
|
elif asyncio.iscoroutinefunction(func):
|
134
126
|
# Handle async request/response functions.
|
135
127
|
@functools.wraps(func)
|
136
|
-
async def async_wrapper(
|
137
|
-
*args: typing.Any, **kwargs: typing.Any
|
138
|
-
) -> Response:
|
128
|
+
async def async_wrapper(*args: typing.Any, **kwargs: typing.Any) -> Response:
|
139
129
|
request = kwargs.get("request", None)
|
140
130
|
assert isinstance(request, Request)
|
141
131
|
|
142
132
|
if not has_required_scope(request, scopes_list):
|
143
133
|
if redirect is not None:
|
144
|
-
return RedirectResponse(
|
145
|
-
url=request.url_for(redirect), status_code=303
|
146
|
-
)
|
134
|
+
return RedirectResponse(url=request.url_for(redirect), status_code=303)
|
147
135
|
raise HTTPException(status_code=status_code)
|
148
136
|
return await func(*args, **kwargs)
|
149
137
|
|
@@ -161,9 +149,7 @@ def requires(
|
|
161
149
|
|
162
150
|
if not has_required_scope(request, scopes_list):
|
163
151
|
if redirect is not None:
|
164
|
-
return RedirectResponse(
|
165
|
-
url=request.url_for(redirect), status_code=303
|
166
|
-
)
|
152
|
+
return RedirectResponse(url=request.url_for(redirect), status_code=303)
|
167
153
|
raise HTTPException(status_code=status_code)
|
168
154
|
return func(*args, **kwargs)
|
169
155
|
|
nucliadb_utils/cache/nats.py
CHANGED
@@ -148,9 +148,7 @@ class NatsPubsub(PubSubDriver):
|
|
148
148
|
|
149
149
|
async def reconnected_cb(self):
|
150
150
|
# See who we are connected to on reconnect.
|
151
|
-
logger.info(
|
152
|
-
"Got reconnected NATS to {url}".format(url=self.nc.connected_url.netloc)
|
153
|
-
)
|
151
|
+
logger.info("Got reconnected NATS to {url}".format(url=self.nc.connected_url.netloc))
|
154
152
|
|
155
153
|
async def error_cb(self, e):
|
156
154
|
logger.info("There was an error connecting to NATS {}".format(e), exc_info=True)
|
@@ -169,9 +167,7 @@ class NatsPubsub(PubSubDriver):
|
|
169
167
|
else:
|
170
168
|
raise ErrConnectionClosed("Could not subscribe")
|
171
169
|
|
172
|
-
async def subscribe(
|
173
|
-
self, handler: Callback, key, group="", subscription_id: Optional[str] = None
|
174
|
-
):
|
170
|
+
async def subscribe(self, handler: Callback, key, group="", subscription_id: Optional[str] = None):
|
175
171
|
if subscription_id is None:
|
176
172
|
subscription_id = key
|
177
173
|
|
nucliadb_utils/debug.py
CHANGED
@@ -54,10 +54,7 @@ def display_top(snapshot, key_type="lineno", limit=10): # pragma: no cover
|
|
54
54
|
print(f"Top {limit} lines")
|
55
55
|
for index, stat in enumerate(top_stats[:limit], 1):
|
56
56
|
frame = stat.traceback[0]
|
57
|
-
print(
|
58
|
-
"#%s: %s:%s: %.1f KiB"
|
59
|
-
% (index, frame.filename, frame.lineno, stat.size / 1024)
|
60
|
-
)
|
57
|
+
print("#%s: %s:%s: %.1f KiB" % (index, frame.filename, frame.lineno, stat.size / 1024))
|
61
58
|
line = linecache.getline(frame.filename, frame.lineno).strip()
|
62
59
|
if line:
|
63
60
|
print(" %s" % line)
|
@@ -29,9 +29,7 @@ from starlette.routing import BaseRoute
|
|
29
29
|
CallableT = TypeVar("CallableT", bound=Callable[..., Any])
|
30
30
|
|
31
31
|
|
32
|
-
def version(
|
33
|
-
major: int, minor: int = 0
|
34
|
-
) -> Callable[[CallableT], CallableT]: # pragma: no cover
|
32
|
+
def version(major: int, minor: int = 0) -> Callable[[CallableT], CallableT]: # pragma: no cover
|
35
33
|
def decorator(func: CallableT) -> CallableT:
|
36
34
|
func._api_version = (major, minor) # type: ignore
|
37
35
|
return func
|
@@ -75,7 +73,10 @@ def VersionedFastAPI(
|
|
75
73
|
prefix = prefix_format.format(major=major, minor=minor)
|
76
74
|
semver = version_format.format(major=major, minor=minor)
|
77
75
|
versioned_app = FastAPI(
|
78
|
-
title=app.title,
|
76
|
+
title=app.title,
|
77
|
+
description=app.description,
|
78
|
+
version=semver,
|
79
|
+
**kwargs, # type: ignore
|
79
80
|
)
|
80
81
|
for route in version_route_mapping[version]:
|
81
82
|
for method in route.methods:
|
@@ -72,9 +72,7 @@ class FlagService:
|
|
72
72
|
else:
|
73
73
|
self.flag_service = mrflagly.FlagService(url=settings.flag_settings_url)
|
74
74
|
|
75
|
-
def enabled(
|
76
|
-
self, flag_key: str, default: bool = False, context: Optional[dict] = None
|
77
|
-
) -> bool:
|
75
|
+
def enabled(self, flag_key: str, default: bool = False, context: Optional[dict] = None) -> bool:
|
78
76
|
if context is None:
|
79
77
|
context = {}
|
80
78
|
context["environment"] = running_settings.running_environment
|
nucliadb_utils/grpc.py
CHANGED
@@ -90,8 +90,6 @@ def get_traced_grpc_server(service_name: str, max_receive_message: int = 100):
|
|
90
90
|
interceptors=[SentryInterceptor()],
|
91
91
|
)
|
92
92
|
else:
|
93
|
-
options = [
|
94
|
-
("grpc.max_receive_message_length", max_receive_message * 1024 * 1024)
|
95
|
-
]
|
93
|
+
options = [("grpc.max_receive_message_length", max_receive_message * 1024 * 1024)]
|
96
94
|
server = aio.server(options=options)
|
97
95
|
return server
|
nucliadb_utils/helpers.py
CHANGED
@@ -21,7 +21,7 @@ from typing import AsyncGenerator
|
|
21
21
|
|
22
22
|
|
23
23
|
async def async_gen_lookahead(
|
24
|
-
gen: AsyncGenerator[bytes, None]
|
24
|
+
gen: AsyncGenerator[bytes, None],
|
25
25
|
) -> AsyncGenerator[tuple[bytes, bool], None]:
|
26
26
|
"""Async generator that yields the next chunk and whether it's the last one.
|
27
27
|
Empty chunks are ignored.
|
nucliadb_utils/indexing.py
CHANGED
@@ -22,8 +22,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
22
22
|
import nats
|
23
23
|
from nats.aio.client import Client
|
24
24
|
from nats.js.client import JetStreamContext
|
25
|
-
from nucliadb_protos.nodewriter_pb2 import IndexMessage
|
26
25
|
|
26
|
+
from nucliadb_protos.nodewriter_pb2 import IndexMessage
|
27
27
|
from nucliadb_telemetry.jetstream import JetStreamContextTelemetry
|
28
28
|
from nucliadb_utils import const, logger
|
29
29
|
from nucliadb_utils.nats import get_traced_jetstream
|
nucliadb_utils/nats.py
CHANGED
@@ -100,9 +100,7 @@ class MessageProgressUpdater:
|
|
100
100
|
class NatsConnectionManager:
|
101
101
|
_nc: NATSClient
|
102
102
|
_subscriptions: list[tuple[Subscription, Callable[[], Awaitable[None]]]]
|
103
|
-
_unhealthy_timeout =
|
104
|
-
10 # needs to be unhealth for 10 seconds to be unhealthy and force exit
|
105
|
-
)
|
103
|
+
_unhealthy_timeout = 10 # needs to be unhealth for 10 seconds to be unhealthy and force exit
|
106
104
|
|
107
105
|
def __init__(
|
108
106
|
self,
|
nucliadb_utils/run.py
CHANGED
@@ -25,9 +25,7 @@ from typing import Awaitable, Callable
|
|
25
25
|
logger = logging.getLogger(__name__)
|
26
26
|
|
27
27
|
|
28
|
-
async def run_until_exit(
|
29
|
-
finalizers: list[Callable[[], Awaitable[None]]], sleep: float = 1.0
|
30
|
-
):
|
28
|
+
async def run_until_exit(finalizers: list[Callable[[], Awaitable[None]]], sleep: float = 1.0):
|
31
29
|
exit_event = asyncio.Event()
|
32
30
|
|
33
31
|
def handle_exit(*args):
|
nucliadb_utils/settings.py
CHANGED
@@ -98,9 +98,7 @@ class StorageSettings(BaseSettings):
|
|
98
98
|
s3_max_pool_connections: int = 30
|
99
99
|
s3_endpoint: Optional[str] = None
|
100
100
|
s3_region_name: Optional[str] = None
|
101
|
-
s3_bucket: Optional[str] = Field(
|
102
|
-
default=None, description="KnowledgeBox S3 bucket name template"
|
103
|
-
)
|
101
|
+
s3_bucket: Optional[str] = Field(default=None, description="KnowledgeBox S3 bucket name template")
|
104
102
|
s3_bucket_tags: Dict[str, str] = Field(
|
105
103
|
default={},
|
106
104
|
description="Map of tags with which S3 buckets will be tagged with: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketTagging.html", # noqa
|
@@ -125,13 +123,9 @@ storage_settings = StorageSettings()
|
|
125
123
|
class NucliaSettings(BaseSettings):
|
126
124
|
nuclia_service_account: Optional[str] = None
|
127
125
|
nuclia_public_url: str = "https://{zone}.nuclia.cloud"
|
128
|
-
nuclia_processing_cluster_url: str =
|
129
|
-
"http://processing-api.processing.svc.cluster.local:8080"
|
130
|
-
)
|
126
|
+
nuclia_processing_cluster_url: str = "http://processing-api.processing.svc.cluster.local:8080"
|
131
127
|
nuclia_inner_predict_url: str = "http://predict.learning.svc.cluster.local:8080"
|
132
|
-
learning_internal_svc_base_url: str =
|
133
|
-
"http://{service}.learning.svc.cluster.local:8080"
|
134
|
-
)
|
128
|
+
learning_internal_svc_base_url: str = "http://{service}.learning.svc.cluster.local:8080"
|
135
129
|
|
136
130
|
nuclia_zone: str = "europe-1"
|
137
131
|
onprem: bool = True
|
nucliadb_utils/signals.py
CHANGED
@@ -62,9 +62,7 @@ class Signal:
|
|
62
62
|
|
63
63
|
awaitables = [
|
64
64
|
cb(payload=payload)
|
65
|
-
for cb, _ in sorted(
|
66
|
-
self.callbacks.values(), key=lambda t: t[1], reverse=True
|
67
|
-
)
|
65
|
+
for cb, _ in sorted(self.callbacks.values(), key=lambda t: t[1], reverse=True)
|
68
66
|
]
|
69
67
|
|
70
68
|
results = await asyncio.gather(*awaitables, return_exceptions=True)
|
@@ -39,9 +39,7 @@ class InvalidOffset(Exception):
|
|
39
39
|
class ResumableUploadGone(Exception):
|
40
40
|
def __init__(self, text: str):
|
41
41
|
self.text = text
|
42
|
-
super().__init__(
|
43
|
-
"Resumable upload is no longer available " "Google: \n " f"{text}"
|
44
|
-
)
|
42
|
+
super().__init__("Resumable upload is no longer available " "Google: \n " f"{text}")
|
45
43
|
|
46
44
|
|
47
45
|
class CouldNotCopyNotFound(Exception):
|
nucliadb_utils/storages/gcs.py
CHANGED
@@ -35,8 +35,8 @@ import backoff
|
|
35
35
|
import google.auth.transport.requests # type: ignore
|
36
36
|
import yarl
|
37
37
|
from google.oauth2 import service_account # type: ignore
|
38
|
-
from nucliadb_protos.resources_pb2 import CloudFile
|
39
38
|
|
39
|
+
from nucliadb_protos.resources_pb2 import CloudFile
|
40
40
|
from nucliadb_telemetry import errors, metrics
|
41
41
|
from nucliadb_telemetry.utils import setup_telemetry
|
42
42
|
from nucliadb_utils import logger
|
@@ -117,9 +117,7 @@ class GCSStorageField(StorageField):
|
|
117
117
|
origin_bucket_name: str,
|
118
118
|
destination_bucket_name: str,
|
119
119
|
):
|
120
|
-
await self.copy(
|
121
|
-
origin_uri, destination_uri, origin_bucket_name, destination_bucket_name
|
122
|
-
)
|
120
|
+
await self.copy(origin_uri, destination_uri, origin_bucket_name, destination_bucket_name)
|
123
121
|
await self.storage.delete_upload(origin_uri, origin_bucket_name)
|
124
122
|
|
125
123
|
@backoff.on_exception(
|
@@ -163,9 +161,7 @@ class GCSStorageField(StorageField):
|
|
163
161
|
assert data["resource"]["name"] == destination_uri
|
164
162
|
|
165
163
|
@storage_ops_observer.wrap({"type": "iter_data"})
|
166
|
-
async def iter_data(
|
167
|
-
self, range: Optional[Range] = None
|
168
|
-
) -> AsyncGenerator[bytes, None]:
|
164
|
+
async def iter_data(self, range: Optional[Range] = None) -> AsyncGenerator[bytes, None]:
|
169
165
|
attempt = 1
|
170
166
|
while True:
|
171
167
|
try:
|
@@ -243,9 +239,7 @@ class GCSStorageField(StorageField):
|
|
243
239
|
|
244
240
|
if self.field is not None and self.field.upload_uri != "":
|
245
241
|
# If there is a temporal url
|
246
|
-
await self.storage.delete_upload(
|
247
|
-
self.field.upload_uri, self.field.bucket_name
|
248
|
-
)
|
242
|
+
await self.storage.delete_upload(self.field.upload_uri, self.field.bucket_name)
|
249
243
|
|
250
244
|
if self.field is not None and self.field.uri != "":
|
251
245
|
field: CloudFile = CloudFile(
|
@@ -381,18 +375,13 @@ class GCSStorageField(StorageField):
|
|
381
375
|
if self.field.old_uri not in ("", None):
|
382
376
|
# Already has a file
|
383
377
|
try:
|
384
|
-
await self.storage.delete_upload(
|
385
|
-
self.field.old_uri, self.field.bucket_name
|
386
|
-
)
|
378
|
+
await self.storage.delete_upload(self.field.old_uri, self.field.bucket_name)
|
387
379
|
except GoogleCloudException as e:
|
388
380
|
logger.warning(
|
389
|
-
f"Could not delete existing google cloud file "
|
390
|
-
f"with uri: {self.field.uri}: {e}"
|
381
|
+
f"Could not delete existing google cloud file " f"with uri: {self.field.uri}: {e}"
|
391
382
|
)
|
392
383
|
if self.field.upload_uri != self.key:
|
393
|
-
await self.move(
|
394
|
-
self.field.upload_uri, self.key, self.field.bucket_name, self.bucket
|
395
|
-
)
|
384
|
+
await self.move(self.field.upload_uri, self.key, self.field.bucket_name, self.bucket)
|
396
385
|
|
397
386
|
self.field.uri = self.key
|
398
387
|
self.field.ClearField("resumable_uri")
|
@@ -490,9 +479,7 @@ class GCSStorage(Storage):
|
|
490
479
|
self._bucket_labels = labels or {}
|
491
480
|
self._executor = executor
|
492
481
|
self._creation_access_token = datetime.now()
|
493
|
-
self._upload_url =
|
494
|
-
url + "/upload/storage/v1/b/{bucket}/o?uploadType=resumable"
|
495
|
-
) # noqa
|
482
|
+
self._upload_url = url + "/upload/storage/v1/b/{bucket}/o?uploadType=resumable" # noqa
|
496
483
|
self.object_base_url = url + "/storage/v1/b"
|
497
484
|
self._client = None
|
498
485
|
|
@@ -516,17 +503,13 @@ class GCSStorage(Storage):
|
|
516
503
|
if self.deadletter_bucket is not None and self.deadletter_bucket != "":
|
517
504
|
await self.create_bucket(self.deadletter_bucket)
|
518
505
|
except Exception: # pragma: no cover
|
519
|
-
logger.exception(
|
520
|
-
f"Could not create bucket {self.deadletter_bucket}", exc_info=True
|
521
|
-
)
|
506
|
+
logger.exception(f"Could not create bucket {self.deadletter_bucket}", exc_info=True)
|
522
507
|
|
523
508
|
try:
|
524
509
|
if self.indexing_bucket is not None and self.indexing_bucket != "":
|
525
510
|
await self.create_bucket(self.indexing_bucket)
|
526
511
|
except Exception: # pragma: no cover
|
527
|
-
logger.exception(
|
528
|
-
f"Could not create bucket {self.indexing_bucket}", exc_info=True
|
529
|
-
)
|
512
|
+
logger.exception(f"Could not create bucket {self.indexing_bucket}", exc_info=True)
|
530
513
|
|
531
514
|
async def finalize(self):
|
532
515
|
await self.session.close()
|
@@ -549,9 +532,7 @@ class GCSStorage(Storage):
|
|
549
532
|
if self.session is None:
|
550
533
|
raise AttributeError()
|
551
534
|
if uri:
|
552
|
-
url = "{}/{}/o/{}".format(
|
553
|
-
self.object_base_url, bucket_name, quote_plus(uri)
|
554
|
-
)
|
535
|
+
url = "{}/{}/o/{}".format(self.object_base_url, bucket_name, quote_plus(uri))
|
555
536
|
headers = await self.get_access_headers()
|
556
537
|
async with self.session.delete(url, headers=headers) as resp:
|
557
538
|
try:
|
@@ -562,8 +543,7 @@ class GCSStorage(Storage):
|
|
562
543
|
if resp.status not in (200, 204, 404):
|
563
544
|
if resp.status == 404:
|
564
545
|
logger.error(
|
565
|
-
f"Attempt to delete not found gcloud: {data}, "
|
566
|
-
f"status: {resp.status}",
|
546
|
+
f"Attempt to delete not found gcloud: {data}, " f"status: {resp.status}",
|
567
547
|
exc_info=True,
|
568
548
|
)
|
569
549
|
else:
|
@@ -668,8 +648,7 @@ class GCSStorage(Storage):
|
|
668
648
|
logger.error("Not implemented")
|
669
649
|
elif resp.status == 404:
|
670
650
|
logger.error(
|
671
|
-
f"Attempt to delete not found gcloud: {data}, "
|
672
|
-
f"status: {resp.status}",
|
651
|
+
f"Attempt to delete not found gcloud: {data}, " f"status: {resp.status}",
|
673
652
|
exc_info=True,
|
674
653
|
)
|
675
654
|
else:
|
@@ -706,9 +685,7 @@ class GCSStorage(Storage):
|
|
706
685
|
errors.capture_message(msg, "error", scope)
|
707
686
|
return deleted, conflict
|
708
687
|
|
709
|
-
async def iterate_objects(
|
710
|
-
self, bucket: str, prefix: str
|
711
|
-
) -> AsyncGenerator[ObjectInfo, None]:
|
688
|
+
async def iterate_objects(self, bucket: str, prefix: str) -> AsyncGenerator[ObjectInfo, None]:
|
712
689
|
if self.session is None:
|
713
690
|
raise AttributeError()
|
714
691
|
url = "{}/{}/o".format(self.object_base_url, bucket)
|
@@ -755,9 +732,7 @@ def parse_object_metadata(object_data: dict[str, Any], key: str) -> ObjectMetada
|
|
755
732
|
size = int(custom_size)
|
756
733
|
|
757
734
|
# Parse content-type
|
758
|
-
content_type = (
|
759
|
-
custom_metadata.get("content_type") or object_data.get("contentType") or ""
|
760
|
-
)
|
735
|
+
content_type = custom_metadata.get("content_type") or object_data.get("contentType") or ""
|
761
736
|
|
762
737
|
# Parse filename
|
763
738
|
filename = custom_metadata.get("filename") or key.split("/")[-1]
|
nucliadb_utils/storages/local.py
CHANGED
@@ -27,8 +27,8 @@ from datetime import datetime
|
|
27
27
|
from typing import AsyncGenerator, AsyncIterator, Optional
|
28
28
|
|
29
29
|
import aiofiles
|
30
|
-
from nucliadb_protos.resources_pb2 import CloudFile
|
31
30
|
|
31
|
+
from nucliadb_protos.resources_pb2 import CloudFile
|
32
32
|
from nucliadb_utils.storages import CHUNK_SIZE
|
33
33
|
from nucliadb_utils.storages.storage import (
|
34
34
|
ObjectInfo,
|
@@ -78,9 +78,7 @@ class LocalStorageField(StorageField):
|
|
78
78
|
destination_path = f"{destination_bucket_path}/{destination_uri}"
|
79
79
|
shutil.copy(origin_path, destination_path)
|
80
80
|
|
81
|
-
async def iter_data(
|
82
|
-
self, range: Optional[Range] = None
|
83
|
-
) -> AsyncGenerator[bytes, None]:
|
81
|
+
async def iter_data(self, range: Optional[Range] = None) -> AsyncGenerator[bytes, None]:
|
84
82
|
key = self.field.uri if self.field else self.key
|
85
83
|
if self.field is None:
|
86
84
|
bucket = self.bucket
|
@@ -89,7 +87,6 @@ class LocalStorageField(StorageField):
|
|
89
87
|
|
90
88
|
path = self.storage.get_file_path(bucket, key)
|
91
89
|
async with aiofiles.open(path, mode="rb") as resp:
|
92
|
-
|
93
90
|
if range and range.start is not None:
|
94
91
|
# Seek to the start of the range
|
95
92
|
await resp.seek(range.start)
|
@@ -123,9 +120,7 @@ class LocalStorageField(StorageField):
|
|
123
120
|
async def start(self, cf: CloudFile) -> CloudFile:
|
124
121
|
if self.field is not None and self.field.upload_uri != "":
|
125
122
|
# If there is a temporal url
|
126
|
-
await self.storage.delete_upload(
|
127
|
-
self.field.upload_uri, self.field.bucket_name
|
128
|
-
)
|
123
|
+
await self.storage.delete_upload(self.field.upload_uri, self.field.bucket_name)
|
129
124
|
|
130
125
|
if self.field is not None and self.field.uri != "":
|
131
126
|
field: CloudFile = CloudFile(
|
@@ -193,9 +188,7 @@ class LocalStorageField(StorageField):
|
|
193
188
|
# Already has a file
|
194
189
|
await self.storage.delete_upload(self.field.uri, self.field.bucket_name)
|
195
190
|
if self.field.upload_uri != self.key:
|
196
|
-
await self.move(
|
197
|
-
self.field.upload_uri, self.key, self.field.bucket_name, self.bucket
|
198
|
-
)
|
191
|
+
await self.move(self.field.upload_uri, self.key, self.field.bucket_name, self.bucket)
|
199
192
|
|
200
193
|
await self._handler.close()
|
201
194
|
self.field.uri = self.key
|
@@ -284,9 +277,7 @@ class LocalStorage(Storage):
|
|
284
277
|
deleted = False
|
285
278
|
return deleted
|
286
279
|
|
287
|
-
async def iterate_objects(
|
288
|
-
self, bucket: str, prefix: str
|
289
|
-
) -> AsyncGenerator[ObjectInfo, None]:
|
280
|
+
async def iterate_objects(self, bucket: str, prefix: str) -> AsyncGenerator[ObjectInfo, None]:
|
290
281
|
for key in glob.glob(f"{bucket}/{prefix}*"):
|
291
282
|
yield ObjectInfo(name=key)
|
292
283
|
|
nucliadb_utils/storages/s3.py
CHANGED
@@ -29,8 +29,8 @@ import backoff
|
|
29
29
|
import botocore # type: ignore
|
30
30
|
from aiobotocore.client import AioBaseClient # type: ignore
|
31
31
|
from aiobotocore.session import AioSession, get_session # type: ignore
|
32
|
-
from nucliadb_protos.resources_pb2 import CloudFile
|
33
32
|
|
33
|
+
from nucliadb_protos.resources_pb2 import CloudFile
|
34
34
|
from nucliadb_telemetry import errors
|
35
35
|
from nucliadb_utils import logger
|
36
36
|
from nucliadb_utils.storages.exceptions import UnparsableResponse
|
@@ -90,9 +90,7 @@ class S3StorageField(StorageField):
|
|
90
90
|
):
|
91
91
|
range = range or Range()
|
92
92
|
if range.any():
|
93
|
-
coro = self.storage._s3aioclient.get_object(
|
94
|
-
Bucket=bucket, Key=uri, Range=range.to_header()
|
95
|
-
)
|
93
|
+
coro = self.storage._s3aioclient.get_object(Bucket=bucket, Key=uri, Range=range.to_header())
|
96
94
|
else:
|
97
95
|
coro = self.storage._s3aioclient.get_object(Bucket=bucket, Key=uri)
|
98
96
|
try:
|
@@ -104,9 +102,7 @@ class S3StorageField(StorageField):
|
|
104
102
|
else:
|
105
103
|
raise
|
106
104
|
|
107
|
-
async def iter_data(
|
108
|
-
self, range: Optional[Range] = None
|
109
|
-
) -> AsyncGenerator[bytes, None]:
|
105
|
+
async def iter_data(self, range: Optional[Range] = None) -> AsyncGenerator[bytes, None]:
|
110
106
|
# Suports field and key based iter
|
111
107
|
uri = self.field.uri if self.field else self.key
|
112
108
|
if self.field is None:
|
@@ -235,9 +231,7 @@ class S3StorageField(StorageField):
|
|
235
231
|
self.field.ClearField("old_uri")
|
236
232
|
self.field.ClearField("old_bucket")
|
237
233
|
except botocore.exceptions.ClientError:
|
238
|
-
logger.error(
|
239
|
-
f"Referenced key {self.field.uri} could not be found", exc_info=True
|
240
|
-
)
|
234
|
+
logger.error(f"Referenced key {self.field.uri} could not be found", exc_info=True)
|
241
235
|
logger.warning("Error deleting object", exc_info=True)
|
242
236
|
|
243
237
|
if self.field.resumable_uri != "":
|
@@ -264,8 +258,7 @@ class S3StorageField(StorageField):
|
|
264
258
|
self.field.offset += 1
|
265
259
|
part_info = {
|
266
260
|
"Parts": [
|
267
|
-
{"PartNumber": part + 1, "ETag": etag}
|
268
|
-
for part, etag in enumerate(self.field.parts)
|
261
|
+
{"PartNumber": part + 1, "ETag": etag} for part, etag in enumerate(self.field.parts)
|
269
262
|
]
|
270
263
|
}
|
271
264
|
await self.storage._s3aioclient.complete_multipart_upload(
|
@@ -323,9 +316,7 @@ class S3StorageField(StorageField):
|
|
323
316
|
origin_bucket_name: str,
|
324
317
|
destination_bucket_name: str,
|
325
318
|
):
|
326
|
-
await self.copy(
|
327
|
-
origin_uri, destination_uri, origin_bucket_name, destination_bucket_name
|
328
|
-
)
|
319
|
+
await self.copy(origin_uri, destination_uri, origin_bucket_name, destination_bucket_name)
|
329
320
|
await self.storage.delete_upload(origin_uri, origin_bucket_name)
|
330
321
|
|
331
322
|
async def upload(self, iterator: AsyncIterator, origin: CloudFile) -> CloudFile:
|
@@ -373,9 +364,7 @@ class S3Storage(Storage):
|
|
373
364
|
verify=verify_ssl,
|
374
365
|
use_ssl=use_ssl,
|
375
366
|
region_name=region_name,
|
376
|
-
config=aiobotocore.config.AioConfig(
|
377
|
-
None, max_pool_connections=max_pool_connections
|
378
|
-
),
|
367
|
+
config=aiobotocore.config.AioConfig(None, max_pool_connections=max_pool_connections),
|
379
368
|
)
|
380
369
|
self._exit_stack = AsyncExitStack()
|
381
370
|
self.bucket = bucket
|
@@ -420,9 +409,7 @@ class S3Storage(Storage):
|
|
420
409
|
else:
|
421
410
|
raise AttributeError("No valid uri")
|
422
411
|
|
423
|
-
async def iterate_objects(
|
424
|
-
self, bucket: str, prefix: str = "/"
|
425
|
-
) -> AsyncGenerator[ObjectInfo, None]:
|
412
|
+
async def iterate_objects(self, bucket: str, prefix: str = "/") -> AsyncGenerator[ObjectInfo, None]:
|
426
413
|
paginator = self._s3aioclient.get_paginator("list_objects")
|
427
414
|
async for result in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
428
415
|
for item in result.get("Contents", []):
|
@@ -436,9 +423,7 @@ class S3Storage(Storage):
|
|
436
423
|
return await bucket_exists(self._s3aioclient, bucket_name)
|
437
424
|
|
438
425
|
async def create_bucket(self, bucket_name: str):
|
439
|
-
await create_bucket(
|
440
|
-
self._s3aioclient, bucket_name, self._bucket_tags, self._region_name
|
441
|
-
)
|
426
|
+
await create_bucket(self._s3aioclient, bucket_name, self._bucket_tags, self._region_name)
|
442
427
|
|
443
428
|
async def schedule_delete_kb(self, kbid: str):
|
444
429
|
bucket_name = self.get_bucket_name(kbid)
|
@@ -509,9 +494,7 @@ async def create_bucket(
|
|
509
494
|
):
|
510
495
|
bucket_creation_options = {}
|
511
496
|
if region_name is not None:
|
512
|
-
bucket_creation_options = {
|
513
|
-
"CreateBucketConfiguration": {"LocationConstraint": region_name}
|
514
|
-
}
|
497
|
+
bucket_creation_options = {"CreateBucketConfiguration": {"LocationConstraint": region_name}}
|
515
498
|
# Create the bucket
|
516
499
|
await client.create_bucket(Bucket=bucket_name, **bucket_creation_options)
|
517
500
|
|
@@ -521,8 +504,7 @@ async def create_bucket(
|
|
521
504
|
Bucket=bucket_name,
|
522
505
|
Tagging={
|
523
506
|
"TagSet": [
|
524
|
-
{"Key": tag_key, "Value": tag_value}
|
525
|
-
for tag_key, tag_value in bucket_tags.items()
|
507
|
+
{"Key": tag_key, "Value": tag_value} for tag_key, tag_value in bucket_tags.items()
|
526
508
|
]
|
527
509
|
},
|
528
510
|
)
|
@@ -36,12 +36,12 @@ from typing import (
|
|
36
36
|
cast,
|
37
37
|
)
|
38
38
|
|
39
|
+
from pydantic import BaseModel
|
40
|
+
|
39
41
|
from nucliadb_protos.noderesources_pb2 import Resource as BrainResource
|
40
42
|
from nucliadb_protos.nodewriter_pb2 import IndexMessage
|
41
43
|
from nucliadb_protos.resources_pb2 import CloudFile
|
42
44
|
from nucliadb_protos.writer_pb2 import BrokerMessage
|
43
|
-
from pydantic import BaseModel
|
44
|
-
|
45
45
|
from nucliadb_utils import logger
|
46
46
|
from nucliadb_utils.helpers import async_gen_lookahead
|
47
47
|
from nucliadb_utils.storages import CHUNK_SIZE
|
@@ -110,9 +110,7 @@ class StorageField(abc.ABC, metaclass=abc.ABCMeta):
|
|
110
110
|
async def upload(self, iterator: AsyncIterator, origin: CloudFile) -> CloudFile: ...
|
111
111
|
|
112
112
|
@abc.abstractmethod
|
113
|
-
async def iter_data(
|
114
|
-
self, range: Optional[Range] = None
|
115
|
-
) -> AsyncGenerator[bytes, None]:
|
113
|
+
async def iter_data(self, range: Optional[Range] = None) -> AsyncGenerator[bytes, None]:
|
116
114
|
raise NotImplementedError()
|
117
115
|
yield b""
|
118
116
|
|
@@ -166,14 +164,10 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
166
164
|
# Delete all keys inside a resource
|
167
165
|
bucket = self.get_bucket_name(kbid)
|
168
166
|
resource_storage_base_path = STORAGE_RESOURCE.format(kbid=kbid, uuid=uuid)
|
169
|
-
async for object_info in self.iterate_objects(
|
170
|
-
bucket, resource_storage_base_path
|
171
|
-
):
|
167
|
+
async for object_info in self.iterate_objects(bucket, resource_storage_base_path):
|
172
168
|
await self.delete_upload(object_info.name, bucket)
|
173
169
|
|
174
|
-
async def deadletter(
|
175
|
-
self, message: BrokerMessage, seq: int, seqid: int, partition: str
|
176
|
-
):
|
170
|
+
async def deadletter(self, message: BrokerMessage, seq: int, seqid: int, partition: str):
|
177
171
|
if self.deadletter_bucket is None:
|
178
172
|
logger.error("No Deadletter Bucket defined will not store the error")
|
179
173
|
return
|
@@ -183,9 +177,7 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
183
177
|
def get_indexing_storage_key(
|
184
178
|
self, *, kb: str, logical_shard: str, resource_uid: str, txid: Union[int, str]
|
185
179
|
):
|
186
|
-
return INDEXING_KEY.format(
|
187
|
-
kb=kb, shard=logical_shard, resource=resource_uid, txid=txid
|
188
|
-
)
|
180
|
+
return INDEXING_KEY.format(kb=kb, shard=logical_shard, resource=resource_uid, txid=txid)
|
189
181
|
|
190
182
|
async def indexing(
|
191
183
|
self,
|
@@ -246,9 +238,7 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
246
238
|
txid=payload.reindex_id,
|
247
239
|
)
|
248
240
|
else:
|
249
|
-
key = OLD_INDEXING_KEY.format(
|
250
|
-
node=payload.node, shard=payload.shard, txid=payload.txid
|
251
|
-
)
|
241
|
+
key = OLD_INDEXING_KEY.format(node=payload.node, shard=payload.shard, txid=payload.txid)
|
252
242
|
|
253
243
|
bytes_buffer = await self.downloadbytes(self.indexing_bucket, key)
|
254
244
|
if bytes_buffer.getbuffer().nbytes == 0:
|
@@ -286,17 +276,12 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
286
276
|
# The cloudfile is valid for our environment
|
287
277
|
if file.uri == "":
|
288
278
|
return False
|
289
|
-
elif (
|
290
|
-
file.source == self.source
|
291
|
-
and self.get_bucket_name(kbid) == file.bucket_name
|
292
|
-
):
|
279
|
+
elif file.source == self.source and self.get_bucket_name(kbid) == file.bucket_name:
|
293
280
|
return False
|
294
281
|
else:
|
295
282
|
return True
|
296
283
|
|
297
|
-
async def normalize_binary(
|
298
|
-
self, file: CloudFile, destination: StorageField
|
299
|
-
): # pragma: no cover
|
284
|
+
async def normalize_binary(self, file: CloudFile, destination: StorageField): # pragma: no cover
|
300
285
|
if file.source == self.source and file.uri != destination.key:
|
301
286
|
# This MAY BE the case for NucliaDB hosted deployment (Nuclia's cloud deployment):
|
302
287
|
# The data has been pushed to the bucket but with a different key.
|
@@ -346,14 +331,10 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
346
331
|
self, kbid: str, uuid: str, field: str, ident: str, count: int
|
347
332
|
) -> StorageField:
|
348
333
|
bucket = self.get_bucket_name(kbid)
|
349
|
-
key = KB_CONVERSATION_FIELD.format(
|
350
|
-
kbid=kbid, uuid=uuid, field=field, ident=ident, count=count
|
351
|
-
)
|
334
|
+
key = KB_CONVERSATION_FIELD.format(kbid=kbid, uuid=uuid, field=field, ident=ident, count=count)
|
352
335
|
return self.field_klass(storage=self, bucket=bucket, fullkey=key)
|
353
336
|
|
354
|
-
def layout_field(
|
355
|
-
self, kbid: str, uuid: str, field: str, ident: str
|
356
|
-
) -> StorageField:
|
337
|
+
def layout_field(self, kbid: str, uuid: str, field: str, ident: str) -> StorageField:
|
357
338
|
bucket = self.get_bucket_name(kbid)
|
358
339
|
key = KB_LAYOUT_FIELD.format(kbid=kbid, uuid=uuid, field=field, ident=ident)
|
359
340
|
return self.field_klass(storage=self, bucket=bucket, fullkey=key)
|
@@ -368,9 +349,7 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
368
349
|
# Its a file field value
|
369
350
|
bucket = self.get_bucket_name(kbid)
|
370
351
|
key = KB_RESOURCE_FIELD.format(kbid=kbid, uuid=uuid, field=field)
|
371
|
-
return self.field_klass(
|
372
|
-
storage=self, bucket=bucket, fullkey=key, field=old_field
|
373
|
-
)
|
352
|
+
return self.field_klass(storage=self, bucket=bucket, fullkey=key, field=old_field)
|
374
353
|
|
375
354
|
def file_extracted(
|
376
355
|
self, kbid: str, uuid: str, field_type: str, field: str, key: str
|
@@ -452,9 +431,7 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
452
431
|
key: str,
|
453
432
|
range: Optional[Range] = None,
|
454
433
|
):
|
455
|
-
destination: StorageField = self.field_klass(
|
456
|
-
storage=self, bucket=bucket, fullkey=key
|
457
|
-
)
|
434
|
+
destination: StorageField = self.field_klass(storage=self, bucket=bucket, fullkey=key)
|
458
435
|
try:
|
459
436
|
async for data in destination.iter_data(range=range):
|
460
437
|
yield data
|
@@ -536,21 +513,15 @@ class Storage(abc.ABC, metaclass=abc.ABCMeta):
|
|
536
513
|
async def finalize(self) -> None: ...
|
537
514
|
|
538
515
|
@abc.abstractmethod
|
539
|
-
async def iterate_objects(
|
540
|
-
self, bucket: str, prefix: str
|
541
|
-
) -> AsyncGenerator[ObjectInfo, None]:
|
516
|
+
async def iterate_objects(self, bucket: str, prefix: str) -> AsyncGenerator[ObjectInfo, None]:
|
542
517
|
raise NotImplementedError()
|
543
518
|
yield ObjectInfo(name="")
|
544
519
|
|
545
520
|
async def copy(self, file: CloudFile, destination: StorageField) -> None:
|
546
|
-
await destination.copy(
|
547
|
-
file.uri, destination.key, file.bucket_name, destination.bucket
|
548
|
-
)
|
521
|
+
await destination.copy(file.uri, destination.key, file.bucket_name, destination.bucket)
|
549
522
|
|
550
523
|
async def move(self, file: CloudFile, destination: StorageField) -> None:
|
551
|
-
await destination.move(
|
552
|
-
file.uri, destination.key, file.bucket_name, destination.bucket
|
553
|
-
)
|
524
|
+
await destination.move(file.uri, destination.key, file.bucket_name, destination.bucket)
|
554
525
|
|
555
526
|
@abc.abstractmethod
|
556
527
|
async def create_kb(self, kbid: str) -> bool: ...
|
@@ -108,9 +108,7 @@ class AsyncBenchmarkFixture(object): # pragma: no cover
|
|
108
108
|
)
|
109
109
|
return timer_precision
|
110
110
|
|
111
|
-
async def _make_runner(
|
112
|
-
self, function_to_benchmark: Callable[..., Awaitable[T]], args, kwargs
|
113
|
-
):
|
111
|
+
async def _make_runner(self, function_to_benchmark: Callable[..., Awaitable[T]], args, kwargs):
|
114
112
|
async def runner(loops_range, timer=self._timer):
|
115
113
|
gc_enabled = gc.isenabled()
|
116
114
|
if self._disable_gc:
|
@@ -162,8 +160,7 @@ class AsyncBenchmarkFixture(object): # pragma: no cover
|
|
162
160
|
if self._mode:
|
163
161
|
self.has_error = True
|
164
162
|
raise FixtureAlreadyUsed(
|
165
|
-
"Fixture can only be used once. Previously it was used in %s mode."
|
166
|
-
% self._mode
|
163
|
+
"Fixture can only be used once. Previously it was used in %s mode." % self._mode
|
167
164
|
)
|
168
165
|
try:
|
169
166
|
self._mode = "benchmark(...)"
|
@@ -199,8 +196,7 @@ class AsyncBenchmarkFixture(object): # pragma: no cover
|
|
199
196
|
if self._warmup:
|
200
197
|
warmup_rounds = min(rounds, max(1, int(self._warmup / iterations)))
|
201
198
|
self._logger.debug(
|
202
|
-
" Warmup %s rounds x %s iterations ..."
|
203
|
-
% (warmup_rounds, iterations)
|
199
|
+
" Warmup %s rounds x %s iterations ..." % (warmup_rounds, iterations)
|
204
200
|
)
|
205
201
|
for _ in range(warmup_rounds):
|
206
202
|
await runner(loops_range)
|
@@ -251,10 +247,7 @@ class AsyncBenchmarkFixture(object): # pragma: no cover
|
|
251
247
|
warmup_start = time.time()
|
252
248
|
warmup_iterations = 0
|
253
249
|
warmup_rounds = 0
|
254
|
-
while (
|
255
|
-
time.time() - warmup_start < self._max_time
|
256
|
-
and warmup_iterations < self._warmup
|
257
|
-
):
|
250
|
+
while time.time() - warmup_start < self._max_time and warmup_iterations < self._warmup:
|
258
251
|
duration = min(duration, await runner(loops_range))
|
259
252
|
warmup_rounds += 1
|
260
253
|
warmup_iterations += loops
|
nucliadb_utils/tests/gcs.py
CHANGED
@@ -61,9 +61,7 @@ class GCS(BaseImage):
|
|
61
61
|
|
62
62
|
def check(self):
|
63
63
|
try:
|
64
|
-
response = requests.get(
|
65
|
-
f"http://{self.host}:{self.get_port()}/storage/v1/b"
|
66
|
-
)
|
64
|
+
response = requests.get(f"http://{self.host}:{self.get_port()}/storage/v1/b")
|
67
65
|
return response.status_code == 200
|
68
66
|
except Exception: # pragma: no cover
|
69
67
|
return False
|
nucliadb_utils/tests/nats.py
CHANGED
@@ -124,9 +124,7 @@ class Gnatsd(object):
|
|
124
124
|
if self.debug:
|
125
125
|
self.proc = subprocess.Popen(cmd)
|
126
126
|
else:
|
127
|
-
self.proc = subprocess.Popen(
|
128
|
-
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
129
|
-
)
|
127
|
+
self.proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
130
128
|
|
131
129
|
if self.debug:
|
132
130
|
if self.proc is None:
|
@@ -135,20 +133,14 @@ class Gnatsd(object):
|
|
135
133
|
% self.port
|
136
134
|
)
|
137
135
|
else:
|
138
|
-
print(
|
139
|
-
"[\033[0;33mDEBUG\033[0;0m] Server listening on port %d started."
|
140
|
-
% self.port
|
141
|
-
)
|
136
|
+
print("[\033[0;33mDEBUG\033[0;0m] Server listening on port %d started." % self.port)
|
142
137
|
return self.proc
|
143
138
|
|
144
139
|
def stop(self):
|
145
140
|
if self.folder is not None:
|
146
141
|
self.folder.cleanup()
|
147
142
|
if self.debug:
|
148
|
-
print(
|
149
|
-
"[\033[0;33mDEBUG\033[0;0m] Server listening on %d will stop."
|
150
|
-
% self.port
|
151
|
-
)
|
143
|
+
print("[\033[0;33mDEBUG\033[0;0m] Server listening on %d will stop." % self.port)
|
152
144
|
|
153
145
|
if self.debug:
|
154
146
|
if self.proc is None:
|
@@ -168,10 +160,7 @@ class Gnatsd(object):
|
|
168
160
|
os.kill(self.proc.pid, signal.SIGKILL)
|
169
161
|
self.proc.wait()
|
170
162
|
if self.debug:
|
171
|
-
print(
|
172
|
-
"[\033[0;33mDEBUG\033[0;0m] Server listening on %d was stopped."
|
173
|
-
% self.port
|
174
|
-
)
|
163
|
+
print("[\033[0;33mDEBUG\033[0;0m] Server listening on %d was stopped." % self.port)
|
175
164
|
|
176
165
|
|
177
166
|
class NatsServer(Gnatsd): # pragma: no cover
|
nucliadb_utils/transaction.py
CHANGED
@@ -26,13 +26,13 @@ from typing import Any, Optional, Union
|
|
26
26
|
import nats
|
27
27
|
from nats.aio.client import Client
|
28
28
|
from nats.js.client import JetStreamContext
|
29
|
+
|
29
30
|
from nucliadb_protos.writer_pb2 import (
|
30
31
|
BrokerMessage,
|
31
32
|
BrokerMessageBlobReference,
|
32
33
|
Notification,
|
33
34
|
OpStatusWriter,
|
34
35
|
)
|
35
|
-
|
36
36
|
from nucliadb_telemetry.jetstream import JetStreamContextTelemetry
|
37
37
|
from nucliadb_utils import const, logger
|
38
38
|
from nucliadb_utils.cache.pubsub import PubSubDriver
|
@@ -180,9 +180,7 @@ class TransactionUtility:
|
|
180
180
|
writer: Union[BrokerMessage, BrokerMessageBlobReference],
|
181
181
|
partition: int,
|
182
182
|
wait: bool = False,
|
183
|
-
target_subject: Optional[
|
184
|
-
str
|
185
|
-
] = None, # allow customizing where to send the message
|
183
|
+
target_subject: Optional[str] = None, # allow customizing where to send the message
|
186
184
|
headers: Optional[dict[str, str]] = None,
|
187
185
|
) -> int:
|
188
186
|
headers = headers or {}
|
@@ -192,24 +190,18 @@ class TransactionUtility:
|
|
192
190
|
request_id = uuid.uuid4().hex
|
193
191
|
|
194
192
|
if wait:
|
195
|
-
waiting_event = await self.wait_for_commited(
|
196
|
-
writer.kbid, waiting_for, request_id=request_id
|
197
|
-
)
|
193
|
+
waiting_event = await self.wait_for_commited(writer.kbid, waiting_for, request_id=request_id)
|
198
194
|
|
199
195
|
if target_subject is None:
|
200
196
|
target_subject = const.Streams.INGEST.subject.format(partition=partition)
|
201
197
|
|
202
|
-
res = await self.js.publish(
|
203
|
-
target_subject, writer.SerializeToString(), headers=headers
|
204
|
-
)
|
198
|
+
res = await self.js.publish(target_subject, writer.SerializeToString(), headers=headers)
|
205
199
|
|
206
200
|
waiting_for.seq = res.seq
|
207
201
|
|
208
202
|
if wait and waiting_event is not None:
|
209
203
|
try:
|
210
|
-
await asyncio.wait_for(
|
211
|
-
waiting_event.wait(), timeout=self.commit_timeout
|
212
|
-
)
|
204
|
+
await asyncio.wait_for(waiting_event.wait(), timeout=self.commit_timeout)
|
213
205
|
except asyncio.TimeoutError:
|
214
206
|
logger.warning("Took too much to commit")
|
215
207
|
raise TransactionCommitTimeoutError()
|
nucliadb_utils/utilities.py
CHANGED
@@ -27,7 +27,6 @@ from enum import Enum
|
|
27
27
|
from typing import TYPE_CHECKING, Any, List, Optional, Union, cast
|
28
28
|
|
29
29
|
from nucliadb_protos.writer_pb2_grpc import WriterStub
|
30
|
-
|
31
30
|
from nucliadb_utils import featureflagging
|
32
31
|
from nucliadb_utils.audit.audit import AuditStorage
|
33
32
|
from nucliadb_utils.audit.basic import BasicAuditStorage
|
@@ -150,9 +149,7 @@ async def get_storage(
|
|
150
149
|
await localutil.initialize()
|
151
150
|
logger.info("Configuring Local Storage")
|
152
151
|
else:
|
153
|
-
raise ConfigurationError(
|
154
|
-
"Invalid storage settings, please configure FILE_BACKEND"
|
155
|
-
)
|
152
|
+
raise ConfigurationError("Invalid storage settings, please configure FILE_BACKEND")
|
156
153
|
|
157
154
|
return MAIN[Utility.STORAGE]
|
158
155
|
|
@@ -322,9 +319,7 @@ async def start_audit_utility(service: str):
|
|
322
319
|
seed=audit_settings.audit_hash_seed,
|
323
320
|
service=service,
|
324
321
|
)
|
325
|
-
logger.info(
|
326
|
-
f"Configuring stream audit log {audit_settings.audit_jetstream_target}"
|
327
|
-
)
|
322
|
+
logger.info(f"Configuring stream audit log {audit_settings.audit_jetstream_target}")
|
328
323
|
else:
|
329
324
|
raise ConfigurationError("Invalid audit driver")
|
330
325
|
await audit_utility.initialize()
|
@@ -390,13 +385,9 @@ def has_feature(
|
|
390
385
|
context = {}
|
391
386
|
if headers is not None:
|
392
387
|
if X_USER_HEADER in headers:
|
393
|
-
context["user_id_md5"] = hashlib.md5(
|
394
|
-
headers[X_USER_HEADER].encode("utf-8")
|
395
|
-
).hexdigest()
|
388
|
+
context["user_id_md5"] = hashlib.md5(headers[X_USER_HEADER].encode("utf-8")).hexdigest()
|
396
389
|
if X_ACCOUNT_HEADER in headers:
|
397
|
-
context["account_id_md5"] = hashlib.md5(
|
398
|
-
headers[X_ACCOUNT_HEADER].encode()
|
399
|
-
).hexdigest()
|
390
|
+
context["account_id_md5"] = hashlib.md5(headers[X_ACCOUNT_HEADER].encode()).hexdigest()
|
400
391
|
if X_ACCOUNT_TYPE_HEADER in headers:
|
401
392
|
context["account_type"] = headers[X_ACCOUNT_TYPE_HEADER]
|
402
393
|
return get_feature_flags().enabled(name, default=default, context=context)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: nucliadb_utils
|
3
|
-
Version: 4.0.3.
|
3
|
+
Version: 4.0.3.post589
|
4
4
|
Home-page: https://nuclia.com
|
5
5
|
License: BSD
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
@@ -23,8 +23,8 @@ Requires-Dist: PyNaCl
|
|
23
23
|
Requires-Dist: pyjwt >=2.4.0
|
24
24
|
Requires-Dist: memorylru >=1.1.2
|
25
25
|
Requires-Dist: mrflagly
|
26
|
-
Requires-Dist: nucliadb-protos >=4.0.3.
|
27
|
-
Requires-Dist: nucliadb-telemetry >=4.0.3.
|
26
|
+
Requires-Dist: nucliadb-protos >=4.0.3.post589
|
27
|
+
Requires-Dist: nucliadb-telemetry >=4.0.3.post589
|
28
28
|
Provides-Extra: cache
|
29
29
|
Requires-Dist: redis >=4.3.4 ; extra == 'cache'
|
30
30
|
Requires-Dist: orjson >=3.6.7 ; extra == 'cache'
|
@@ -1,35 +1,35 @@
|
|
1
1
|
nucliadb_utils/__init__.py,sha256=EvBCH1iTODe-AgXm48aj4kVUt_Std3PeL8QnwimR5wI,895
|
2
|
-
nucliadb_utils/asyncio_utils.py,sha256=
|
3
|
-
nucliadb_utils/authentication.py,sha256=
|
2
|
+
nucliadb_utils/asyncio_utils.py,sha256=h8Y-xpcFFRgNzaiIW0eidz7griAQa7ggbNk34-tAt2c,2888
|
3
|
+
nucliadb_utils/authentication.py,sha256=N__d2Ez3JHJv5asYK5TgUcIkKqcAC8ZTLlnfLhfSneM,5837
|
4
4
|
nucliadb_utils/const.py,sha256=V9SWXB_Nfy0qqgPkQcEYN_zj90WkOpLubeixRKUprfM,2391
|
5
|
-
nucliadb_utils/debug.py,sha256=
|
5
|
+
nucliadb_utils/debug.py,sha256=Q56Nx9Dp7V2ae3CU2H0ztaZcHTJXdlflPLKLeOPZ170,2436
|
6
6
|
nucliadb_utils/exceptions.py,sha256=y_3wk77WLVUtdo-5FtbBsdSkCtK_DsJkdWb5BoPn3qo,1094
|
7
|
-
nucliadb_utils/featureflagging.py,sha256=
|
8
|
-
nucliadb_utils/grpc.py,sha256=
|
9
|
-
nucliadb_utils/helpers.py,sha256=
|
10
|
-
nucliadb_utils/indexing.py,sha256=
|
11
|
-
nucliadb_utils/nats.py,sha256=
|
7
|
+
nucliadb_utils/featureflagging.py,sha256=pgqqO4Kbqj4aCuTRbfDK4nXWvWgRUM4GPZu0da-8DG8,2726
|
8
|
+
nucliadb_utils/grpc.py,sha256=apu0uePnkGHCAT7GRQ9YZfRYyFj26kJ440i8jitbM3U,3314
|
9
|
+
nucliadb_utils/helpers.py,sha256=nPw8yod3hP-pxq80VF8QC36s7ygSg0dBUdfI-LatvCs,1600
|
10
|
+
nucliadb_utils/indexing.py,sha256=QGR0d-Oy_rIgMKOqL01ZPZ_ZFTlL8Ed1UXE2U9nqvpo,3446
|
11
|
+
nucliadb_utils/nats.py,sha256=zTAXECDXeCPtydk3F_6EMFDZ059kK0UYUU_tnWoxgXs,8208
|
12
12
|
nucliadb_utils/partition.py,sha256=jBgy4Hu5Iwn4gjbPPcthSykwf-qNx-GcLAIwbzPd1d0,1157
|
13
13
|
nucliadb_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
-
nucliadb_utils/run.py,sha256=
|
15
|
-
nucliadb_utils/settings.py,sha256=
|
16
|
-
nucliadb_utils/signals.py,sha256=
|
14
|
+
nucliadb_utils/run.py,sha256=HpAIM8xbR7UpVC2_7xOjB4fYbUVykyPP6yHrv2RD3DI,1707
|
15
|
+
nucliadb_utils/settings.py,sha256=fI3AOn30tNvYS_PqoKilVhJN4OppPAGCM6-OUUitO2s,7192
|
16
|
+
nucliadb_utils/signals.py,sha256=JRNv2y9zLtBjOANBf7krGfDGfOc9qcoXZ6N1nKWS2FE,2674
|
17
17
|
nucliadb_utils/store.py,sha256=kQ35HemE0v4_Qg6xVqNIJi8vSFAYQtwI3rDtMsNy62Y,890
|
18
|
-
nucliadb_utils/transaction.py,sha256=
|
19
|
-
nucliadb_utils/utilities.py,sha256=
|
18
|
+
nucliadb_utils/transaction.py,sha256=ym9hmPAoIt8xgxjd8JHG14_PelYTqhUOVfUAq_ghJDM,7100
|
19
|
+
nucliadb_utils/utilities.py,sha256=E7W9TzvbyJ7_Yenho9CT059E_g4JQOCS02HrGurwNqs,13603
|
20
20
|
nucliadb_utils/audit/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
|
21
|
-
nucliadb_utils/audit/audit.py,sha256=
|
22
|
-
nucliadb_utils/audit/basic.py,sha256=
|
23
|
-
nucliadb_utils/audit/stream.py,sha256=
|
21
|
+
nucliadb_utils/audit/audit.py,sha256=dn5ZnCVQUlCcvdjzaORghbrjk9QgVGrtkfIftq30Bp8,2819
|
22
|
+
nucliadb_utils/audit/basic.py,sha256=NViey6mKbCXqRTLDBX2xNTcCg9I-2e4oB2xkekuhDvM,3392
|
23
|
+
nucliadb_utils/audit/stream.py,sha256=CSNFrwCT_IeHFStx4q4ImgOQ7v0WzJr4YG6QRzE7YlQ,11952
|
24
24
|
nucliadb_utils/cache/__init__.py,sha256=itSI7dtTwFP55YMX4iK7JzdMHS5CQVUiB1XzQu4UBh8,833
|
25
25
|
nucliadb_utils/cache/exceptions.py,sha256=Zu-O_-0-yctOEgoDGI92gPzWfBMRrpiAyESA62ld6MA,975
|
26
|
-
nucliadb_utils/cache/nats.py,sha256
|
26
|
+
nucliadb_utils/cache/nats.py,sha256=-AjCfkFgKVdJUlGR0hT9JDSNkPVFg4S6w9eW-ZIcXPM,7037
|
27
27
|
nucliadb_utils/cache/pubsub.py,sha256=l8i_RwRf7OPzfmPy-gyn66xgYFs5aHidCIjEaU9VOHE,1654
|
28
28
|
nucliadb_utils/cache/settings.py,sha256=WVeHOE6Re5i4k2hUHdFKfkoL4n83v_Z6UPBK6GHYb8g,1059
|
29
29
|
nucliadb_utils/fastapi/__init__.py,sha256=itSI7dtTwFP55YMX4iK7JzdMHS5CQVUiB1XzQu4UBh8,833
|
30
30
|
nucliadb_utils/fastapi/openapi.py,sha256=b0pLuri0QuzQd0elDyOVXM42YYmES_cmT-jEfsQ1G6Y,1737
|
31
31
|
nucliadb_utils/fastapi/run.py,sha256=n6vOX64QqF1I5n4UlKnpm_ZJ24rmwfRGi-J9YMGpZzA,3631
|
32
|
-
nucliadb_utils/fastapi/versioning.py,sha256=
|
32
|
+
nucliadb_utils/fastapi/versioning.py,sha256=pwiwuesJW1jElUUI3pI5kcxkigfGBvI64IL6QCBEWS8,3805
|
33
33
|
nucliadb_utils/nuclia_usage/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
|
34
34
|
nucliadb_utils/nuclia_usage/protos/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
|
35
35
|
nucliadb_utils/nuclia_usage/protos/kb_usage_pb2.py,sha256=7jBWh5aFkNOU6rF_rdp-twZONgrJe3ztJdqKvRWcMM4,5934
|
@@ -39,23 +39,23 @@ nucliadb_utils/nuclia_usage/protos/kb_usage_pb2_grpc.pyi,sha256=6RIsZ2934iodEckf
|
|
39
39
|
nucliadb_utils/nuclia_usage/utils/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
|
40
40
|
nucliadb_utils/nuclia_usage/utils/kb_usage_report.py,sha256=E1eUSFXBVNzQP9Q2rWj9y3koCO5S7iKwckny_AoLKuk,3870
|
41
41
|
nucliadb_utils/storages/__init__.py,sha256=5Qc8AUWiJv9_JbGCBpAn88AIJhwDlm0OPQpg2ZdRL4U,872
|
42
|
-
nucliadb_utils/storages/exceptions.py,sha256=
|
43
|
-
nucliadb_utils/storages/gcs.py,sha256=
|
44
|
-
nucliadb_utils/storages/local.py,sha256=
|
45
|
-
nucliadb_utils/storages/nuclia.py,sha256=
|
46
|
-
nucliadb_utils/storages/s3.py,sha256=
|
42
|
+
nucliadb_utils/storages/exceptions.py,sha256=BfJcn0-60Ts2gLHRTxQKD0QuR7L4WDJtIdsUp7zhQ0k,2395
|
43
|
+
nucliadb_utils/storages/gcs.py,sha256=KQ9puMOE89CPIA8q8DeCs7qOp0YoB5ZctXPws1h7lbA,27006
|
44
|
+
nucliadb_utils/storages/local.py,sha256=GAEzvbmLzEeEJhhIWKa-vX2i9B0qdq6mbHMolpa2Q20,10259
|
45
|
+
nucliadb_utils/storages/nuclia.py,sha256=vEv94xAT7QM2g80S25QyrOw2pzvP2BAX-ADgZLtuCVc,2097
|
46
|
+
nucliadb_utils/storages/s3.py,sha256=ABzS9X3fj7SUq-3cLvnKEClngb8hcPyKNSfxubMpyCo,19256
|
47
47
|
nucliadb_utils/storages/settings.py,sha256=ugCPy1zxBOmA2KosT-4tsjpvP002kg5iQyi42yCGCJA,1285
|
48
|
-
nucliadb_utils/storages/storage.py,sha256=
|
48
|
+
nucliadb_utils/storages/storage.py,sha256=SWeQv6136ruj7TvCPQR6WkG458IDEz2fzQQjkDRRReQ,20533
|
49
49
|
nucliadb_utils/tests/__init__.py,sha256=Oo9CAE7B0eW5VHn8sHd6o30SQzOWUhktLPRXdlDOleA,1456
|
50
|
-
nucliadb_utils/tests/asyncbenchmark.py,sha256=
|
50
|
+
nucliadb_utils/tests/asyncbenchmark.py,sha256=x4be2IwCawle9zWgMOJkmwoUwk5p1tv7cLQGmybkEOg,10587
|
51
51
|
nucliadb_utils/tests/fixtures.py,sha256=j58fTvoWZClC52LX7QOvLXX9DS5QbytSnRp0F4nGzN8,1671
|
52
|
-
nucliadb_utils/tests/gcs.py,sha256=
|
52
|
+
nucliadb_utils/tests/gcs.py,sha256=Ii8BCHUAAxFIzX67pKTRFRgbqv3FJ6DrPAdAx2Xod1Y,3036
|
53
53
|
nucliadb_utils/tests/indexing.py,sha256=YW2QhkhO9Q_8A4kKWJaWSvXvyQ_AiAwY1VylcfVQFxk,1513
|
54
54
|
nucliadb_utils/tests/local.py,sha256=c3gZJJWmvOftruJkIQIwB3q_hh3uxEhqGIAVWim1Bbk,1343
|
55
|
-
nucliadb_utils/tests/nats.py,sha256=
|
55
|
+
nucliadb_utils/tests/nats.py,sha256=Tosonm9A9cusImyji80G4pgdXEHNVPaCLT5TbFK_ra0,7543
|
56
56
|
nucliadb_utils/tests/s3.py,sha256=YB8QqDaBXxyhHonEHmeBbRRDmvB7sTOaKBSi8KBGokg,2330
|
57
|
-
nucliadb_utils-4.0.3.
|
58
|
-
nucliadb_utils-4.0.3.
|
59
|
-
nucliadb_utils-4.0.3.
|
60
|
-
nucliadb_utils-4.0.3.
|
61
|
-
nucliadb_utils-4.0.3.
|
57
|
+
nucliadb_utils-4.0.3.post589.dist-info/METADATA,sha256=YHbtxvU4f1pk0u4KI5CoM1J2bJxAFD9nPOZ9jI5wc8c,2030
|
58
|
+
nucliadb_utils-4.0.3.post589.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
59
|
+
nucliadb_utils-4.0.3.post589.dist-info/top_level.txt,sha256=fE3vJtALTfgh7bcAWcNhcfXkNPp_eVVpbKK-2IYua3E,15
|
60
|
+
nucliadb_utils-4.0.3.post589.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
61
|
+
nucliadb_utils-4.0.3.post589.dist-info/RECORD,,
|
File without changes
|
{nucliadb_utils-4.0.3.post586.dist-info → nucliadb_utils-4.0.3.post589.dist-info}/top_level.txt
RENAMED
File without changes
|
File without changes
|