konduktor-nightly 0.1.0.dev20250722105323__py3-none-any.whl → 0.1.0.dev20250724105301__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.
Potentially problematic release.
This version of konduktor-nightly might be problematic. Click here for more details.
- konduktor/__init__.py +2 -2
- konduktor/backends/jobset.py +3 -3
- konduktor/backends/jobset_utils.py +1 -1
- konduktor/cli.py +5 -5
- konduktor/utils/log_utils.py +167 -1
- konduktor/utils/schemas.py +13 -0
- {konduktor_nightly-0.1.0.dev20250722105323.dist-info → konduktor_nightly-0.1.0.dev20250724105301.dist-info}/METADATA +1 -1
- {konduktor_nightly-0.1.0.dev20250722105323.dist-info → konduktor_nightly-0.1.0.dev20250724105301.dist-info}/RECORD +11 -11
- {konduktor_nightly-0.1.0.dev20250722105323.dist-info → konduktor_nightly-0.1.0.dev20250724105301.dist-info}/LICENSE +0 -0
- {konduktor_nightly-0.1.0.dev20250722105323.dist-info → konduktor_nightly-0.1.0.dev20250724105301.dist-info}/WHEEL +0 -0
- {konduktor_nightly-0.1.0.dev20250722105323.dist-info → konduktor_nightly-0.1.0.dev20250724105301.dist-info}/entry_points.txt +0 -0
konduktor/__init__.py
CHANGED
|
@@ -14,7 +14,7 @@ __all__ = [
|
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
# Replaced with the current commit when building the wheels.
|
|
17
|
-
_KONDUKTOR_COMMIT_SHA = '
|
|
17
|
+
_KONDUKTOR_COMMIT_SHA = '8cc0f7da813cd75a82206a1c8e534c8be07d4156'
|
|
18
18
|
os.makedirs(os.path.expanduser('~/.konduktor'), exist_ok=True)
|
|
19
19
|
|
|
20
20
|
|
|
@@ -48,5 +48,5 @@ def _get_git_commit():
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
__commit__ = _get_git_commit()
|
|
51
|
-
__version__ = '1.0.0.dev0.1.0.
|
|
51
|
+
__version__ = '1.0.0.dev0.1.0.dev20250724105301'
|
|
52
52
|
__root_dir__ = os.path.dirname(os.path.abspath(__file__))
|
konduktor/backends/jobset.py
CHANGED
|
@@ -16,13 +16,13 @@ if typing.TYPE_CHECKING:
|
|
|
16
16
|
|
|
17
17
|
from konduktor import config, logging
|
|
18
18
|
from konduktor.backends import backend, jobset_utils
|
|
19
|
-
from konduktor.utils import kubernetes_utils,
|
|
19
|
+
from konduktor.utils import kubernetes_utils, log_utils, rich_utils, ux_utils
|
|
20
20
|
|
|
21
21
|
Path = str
|
|
22
22
|
logger = logging.get_logger(__file__)
|
|
23
23
|
|
|
24
24
|
POLL_INTERVAL = 5
|
|
25
|
-
DEFAULT_ATTACH_TIMEOUT =
|
|
25
|
+
DEFAULT_ATTACH_TIMEOUT = 86400 # 1 day
|
|
26
26
|
FLUSH_LOGS_TIMEOUT = 5
|
|
27
27
|
|
|
28
28
|
|
|
@@ -193,7 +193,7 @@ class JobsetBackend(backend.Backend):
|
|
|
193
193
|
_wait_for_jobset_start(namespace, task.name)
|
|
194
194
|
try:
|
|
195
195
|
log_thread = threading.Thread(
|
|
196
|
-
target=
|
|
196
|
+
target=log_utils.tail_logs,
|
|
197
197
|
args=(task.name,),
|
|
198
198
|
daemon=True,
|
|
199
199
|
)
|
|
@@ -595,7 +595,7 @@ def show_status_table(namespace: str, all_users: bool):
|
|
|
595
595
|
rows.append(
|
|
596
596
|
[
|
|
597
597
|
job['metadata']['name'],
|
|
598
|
-
_get_status_string_colorized(job
|
|
598
|
+
_get_status_string_colorized(job.get('status', {})),
|
|
599
599
|
_get_resources(job),
|
|
600
600
|
*_get_time_delta(job['metadata']['creationTimestamp']),
|
|
601
601
|
]
|
konduktor/cli.py
CHANGED
|
@@ -56,7 +56,6 @@ from konduktor.utils import (
|
|
|
56
56
|
common_utils,
|
|
57
57
|
kubernetes_utils,
|
|
58
58
|
log_utils,
|
|
59
|
-
loki_utils,
|
|
60
59
|
ux_utils,
|
|
61
60
|
)
|
|
62
61
|
|
|
@@ -554,7 +553,8 @@ def status(all_users: bool):
|
|
|
554
553
|
),
|
|
555
554
|
)
|
|
556
555
|
@click.option(
|
|
557
|
-
'--
|
|
556
|
+
'--num-lines',
|
|
557
|
+
'--num_lines' '-n',
|
|
558
558
|
default=1000,
|
|
559
559
|
type=int,
|
|
560
560
|
help=(
|
|
@@ -568,10 +568,10 @@ def logs(
|
|
|
568
568
|
status: bool,
|
|
569
569
|
job_id: str,
|
|
570
570
|
follow: bool,
|
|
571
|
-
|
|
571
|
+
num_lines: int,
|
|
572
572
|
):
|
|
573
573
|
# NOTE(dev): Keep the docstring consistent between the Python API and CLI.
|
|
574
|
-
"""
|
|
574
|
+
"""Retrieve/tail the log of a job."""
|
|
575
575
|
if status:
|
|
576
576
|
raise click.UsageError('`--status` is being deprecated)')
|
|
577
577
|
|
|
@@ -598,7 +598,7 @@ def logs(
|
|
|
598
598
|
'Logs are tailed from 1 hour ago, ' 'to see more logs, check Grafana.',
|
|
599
599
|
fg='yellow',
|
|
600
600
|
)
|
|
601
|
-
|
|
601
|
+
log_utils.tail_logs(job_id, follow=follow, num_logs=num_lines)
|
|
602
602
|
|
|
603
603
|
|
|
604
604
|
@cli.command(cls=_DocumentedCodeCommand)
|
konduktor/utils/log_utils.py
CHANGED
|
@@ -10,19 +10,41 @@
|
|
|
10
10
|
# See the License for the specific language governing permissions and
|
|
11
11
|
# limitations under the License.
|
|
12
12
|
|
|
13
|
+
import asyncio
|
|
13
14
|
import copy
|
|
15
|
+
import enum
|
|
14
16
|
import io
|
|
17
|
+
import json
|
|
15
18
|
import multiprocessing
|
|
16
19
|
import os
|
|
17
20
|
import subprocess
|
|
18
21
|
import sys
|
|
19
22
|
import types
|
|
20
|
-
|
|
23
|
+
import urllib.parse
|
|
24
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
21
25
|
|
|
26
|
+
import colorama
|
|
27
|
+
import kr8s
|
|
22
28
|
import prettytable
|
|
29
|
+
import requests
|
|
30
|
+
import websockets
|
|
23
31
|
|
|
32
|
+
from konduktor import config, logging
|
|
24
33
|
from konduktor.utils import subprocess_utils
|
|
25
34
|
|
|
35
|
+
logger = logging.get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
LOKI_REMOTE_PORT = 3100
|
|
39
|
+
WEBSOCKET_TIMEOUT = 10
|
|
40
|
+
INFINITY = 999999
|
|
41
|
+
VICKY_REMOTE_PORT = 9428
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LogBackend(enum.Enum):
|
|
45
|
+
VICTORIA = 'victoria'
|
|
46
|
+
LOKI = 'loki'
|
|
47
|
+
|
|
26
48
|
|
|
27
49
|
class LineProcessor(object):
|
|
28
50
|
"""A processor for log lines."""
|
|
@@ -247,3 +269,147 @@ def run_with_log(
|
|
|
247
269
|
# causing the stream handling stuck at `readline`.
|
|
248
270
|
subprocess_utils.kill_children_processes()
|
|
249
271
|
raise
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def _read_logs(url: str, timeout: int, job_name: str, worker_id: int, port: int):
|
|
275
|
+
ws = await asyncio.wait_for(websockets.connect(url), timeout=WEBSOCKET_TIMEOUT)
|
|
276
|
+
logger.info(
|
|
277
|
+
f'{colorama.Fore.YELLOW}Tailing logs. '
|
|
278
|
+
f'Forwarding from remote port {port}. Press Ctrl+C to stop. '
|
|
279
|
+
f'{colorama.Style.RESET_ALL}'
|
|
280
|
+
)
|
|
281
|
+
try:
|
|
282
|
+
while True:
|
|
283
|
+
message = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
|
284
|
+
try:
|
|
285
|
+
payload = json.loads(message)
|
|
286
|
+
for stream in payload['streams']:
|
|
287
|
+
if stream['values'][0][1] is not None:
|
|
288
|
+
print(
|
|
289
|
+
f"{colorama.Fore.CYAN}{colorama.Style.BRIGHT} "
|
|
290
|
+
f"(job_name={job_name} worker_id={worker_id})"
|
|
291
|
+
f"{colorama.Style.RESET_ALL} {stream['values'][0][1]}",
|
|
292
|
+
flush=True,
|
|
293
|
+
)
|
|
294
|
+
except json.JSONDecodeError:
|
|
295
|
+
logger.warning(f'Failed to decode log skipping: {message}')
|
|
296
|
+
logger.debug(f'Dropped log: {message}')
|
|
297
|
+
continue
|
|
298
|
+
except asyncio.exceptions.TimeoutError:
|
|
299
|
+
logger.debug('Websocket timed-out, closing log stream!')
|
|
300
|
+
except KeyboardInterrupt:
|
|
301
|
+
logger.debug('Keyboard interrupt, closing log stream!')
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def tail_loki_logs_ws(
|
|
305
|
+
job_name: str, worker_id: int = 0, num_logs: int = 1000, follow: bool = True
|
|
306
|
+
):
|
|
307
|
+
if num_logs > 5000:
|
|
308
|
+
# TODO(asaiacai): we should not have a limit on the number of logs, but rather
|
|
309
|
+
# let the user specify any number of lines, and we can print the last N lines.
|
|
310
|
+
# this can be done in chunks. Potentially, we can query range
|
|
311
|
+
# until we reach the end of the log and then invoke tail again.
|
|
312
|
+
# Also include checks that the job is running/ever ran.
|
|
313
|
+
raise ValueError('num_logs must be less than or equal to 5000')
|
|
314
|
+
loki_svc = kr8s.objects.Service.get('loki', namespace='loki')
|
|
315
|
+
with kr8s.portforward.PortForward(
|
|
316
|
+
loki_svc, LOKI_REMOTE_PORT, local_port='auto'
|
|
317
|
+
) as port:
|
|
318
|
+
loki_url = f'ws://localhost:{port}/loki/api/v1/tail'
|
|
319
|
+
logger.debug(f'Loki URL: {loki_url}')
|
|
320
|
+
params = {
|
|
321
|
+
'query': urllib.parse.quote(
|
|
322
|
+
r'{' + f'k8s_job_name="{job_name}-workers-0",'
|
|
323
|
+
r' k8s_container_name="konduktor-container"} '
|
|
324
|
+
f' | batch_kubernetes_io_job_completion_index = `{worker_id}`'
|
|
325
|
+
),
|
|
326
|
+
'limit': num_logs,
|
|
327
|
+
'delay': 5,
|
|
328
|
+
# TODO(asaiacai): need to auto-generate the start and end times.
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
query_string = '&'.join(f'{key}={value}' for key, value in params.items())
|
|
332
|
+
loki_url += f'?{query_string}'
|
|
333
|
+
timeout = INFINITY if follow else WEBSOCKET_TIMEOUT
|
|
334
|
+
asyncio.run(
|
|
335
|
+
_read_logs(loki_url, timeout, job_name, worker_id, LOKI_REMOTE_PORT)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def tail_vicky_logs(
|
|
340
|
+
job_name: str, worker_id: int = 0, num_logs: int = 1000, follow: bool = True
|
|
341
|
+
):
|
|
342
|
+
query: Dict[str, Any] = {}
|
|
343
|
+
if num_logs > 5000:
|
|
344
|
+
# TODO(asaiacai): we should not have a limit on the number of logs, but rather
|
|
345
|
+
# let the user specify any number of lines, and we can print the last N lines.
|
|
346
|
+
# this can be done in chunks. Potentially, we can query range
|
|
347
|
+
# until we reach the end of the log and then invoke tail again.
|
|
348
|
+
# Also include checks that the job is running/ever ran.
|
|
349
|
+
raise ValueError('num_logs must be less than or equal to 5000')
|
|
350
|
+
logger.info('ignoring num_logs argument for VictoriaLogs')
|
|
351
|
+
vicky_svc = kr8s.objects.Service.get(
|
|
352
|
+
'vls-victoria-logs-single-server', namespace='victoria-logs'
|
|
353
|
+
)
|
|
354
|
+
with kr8s.portforward.PortForward(
|
|
355
|
+
vicky_svc, VICKY_REMOTE_PORT, local_port='auto'
|
|
356
|
+
) as port:
|
|
357
|
+
if follow:
|
|
358
|
+
timeout = INFINITY
|
|
359
|
+
vicky_url = f'http://localhost:{port}/select/logsql/tail'
|
|
360
|
+
query = {}
|
|
361
|
+
else:
|
|
362
|
+
vicky_url = f'http://localhost:{port}/select/logsql/query'
|
|
363
|
+
query = {'limit': num_logs}
|
|
364
|
+
timeout = 1
|
|
365
|
+
logger.debug(f'Vicky URL: {vicky_url}')
|
|
366
|
+
|
|
367
|
+
query['query'] = 'k8s.namespace.name: "default"'
|
|
368
|
+
query['start_offset'] = '1h'
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
logger.debug(f'Making request to {vicky_url} with query: {query}')
|
|
372
|
+
with requests.post(
|
|
373
|
+
vicky_url, data=query, stream=True, timeout=timeout
|
|
374
|
+
) as response:
|
|
375
|
+
logger.debug(f'Response status: {response.status_code}')
|
|
376
|
+
if response.status_code != 200:
|
|
377
|
+
logger.error(
|
|
378
|
+
f'VictoriaLogs API returned status {response.status_code}: '
|
|
379
|
+
f'{response.text}'
|
|
380
|
+
)
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
for line in response.iter_lines(decode_unicode=True):
|
|
384
|
+
if line:
|
|
385
|
+
payload = json.loads(line)
|
|
386
|
+
print(
|
|
387
|
+
f"{colorama.Fore.CYAN}{colorama.Style.BRIGHT} "
|
|
388
|
+
f"(job_name={job_name} worker_id={worker_id})"
|
|
389
|
+
f"{colorama.Style.RESET_ALL} {payload['_msg']}",
|
|
390
|
+
flush=True,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
except KeyboardInterrupt:
|
|
394
|
+
logger.info('\nStopping log stream...')
|
|
395
|
+
except requests.exceptions.Timeout:
|
|
396
|
+
logger.error(f'Request to VictoriaLogs timed out after {timeout} seconds')
|
|
397
|
+
except requests.exceptions.ConnectionError as e:
|
|
398
|
+
logger.error(f'Failed to connect to VictoriaLogs at {vicky_url}: {e}')
|
|
399
|
+
except requests.exceptions.RequestException as e:
|
|
400
|
+
logger.error(f'Request to VictoriaLogs failed: {e}')
|
|
401
|
+
except Exception as e:
|
|
402
|
+
logger.error(f'Unexpected error while tailing VictoriaLogs: {e}')
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def tail_logs(
|
|
406
|
+
job_name: str, worker_id: int = 0, num_logs: int = 1000, follow: bool = True
|
|
407
|
+
):
|
|
408
|
+
logs_backend = config.get_nested(('logs', 'backend'), None)
|
|
409
|
+
if logs_backend == LogBackend.VICTORIA:
|
|
410
|
+
tail_vicky_logs(job_name, worker_id, num_logs, follow)
|
|
411
|
+
elif logs_backend == LogBackend.LOKI:
|
|
412
|
+
tail_loki_logs_ws(job_name, worker_id, num_logs, follow)
|
|
413
|
+
else:
|
|
414
|
+
logger.info('Defaulting to VictoriaLogs')
|
|
415
|
+
tail_vicky_logs(job_name, worker_id, num_logs, follow)
|
konduktor/utils/schemas.py
CHANGED
|
@@ -490,6 +490,18 @@ def get_config_schema():
|
|
|
490
490
|
},
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
logs_configs = {
|
|
494
|
+
'type': 'object',
|
|
495
|
+
'required': [],
|
|
496
|
+
'additionalProperties': False,
|
|
497
|
+
'properties': {
|
|
498
|
+
'backend': {
|
|
499
|
+
'type': 'string',
|
|
500
|
+
'case_insensitive_enum': ['loki', 'victoria'],
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
}
|
|
504
|
+
|
|
493
505
|
gpu_configs = {
|
|
494
506
|
'type': 'object',
|
|
495
507
|
'required': [],
|
|
@@ -537,6 +549,7 @@ def get_config_schema():
|
|
|
537
549
|
'admin_policy': admin_policy_schema,
|
|
538
550
|
'nvidia_gpus': gpu_configs,
|
|
539
551
|
'allowed_clouds': allowed_clouds,
|
|
552
|
+
'logs': logs_configs,
|
|
540
553
|
'tailscale': tailscale_configs,
|
|
541
554
|
'ssh': ssh_configs,
|
|
542
555
|
**cloud_configs,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
konduktor/__init__.py,sha256=
|
|
1
|
+
konduktor/__init__.py,sha256=uQyPDiocyL172fLW4bNhd5Gtvka3Z_sgWDMTXAHvbfo,1540
|
|
2
2
|
konduktor/adaptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
konduktor/adaptors/aws.py,sha256=s47Ra-GaqCQibzVfmD0pmwEWHif1EGO5opMbwkLxTCU,8244
|
|
4
4
|
konduktor/adaptors/common.py,sha256=ZIqzjx77PIHUwpjfAQ1uX8B2aX78YMuGj4Bppd-MdyM,4183
|
|
@@ -7,10 +7,10 @@ konduktor/authentication.py,sha256=_mVy3eqoKohicHostFiGwG1-2ybxP-l7ouofQ0LRlCY,4
|
|
|
7
7
|
konduktor/backends/__init__.py,sha256=1Q6sqqdeMYarpTX_U-QVywJYf7idiUTRsyP-E4BQSOw,129
|
|
8
8
|
konduktor/backends/backend.py,sha256=qh0bp94lzoTYZkzyQv2-CVrB5l91FkG2vclXg24UFC0,2910
|
|
9
9
|
konduktor/backends/constants.py,sha256=nU_cd4x8V2GwP9-oGlcIwjt5snnyhmOlxXbXRZ8d6Fc,26
|
|
10
|
-
konduktor/backends/jobset.py,sha256=
|
|
11
|
-
konduktor/backends/jobset_utils.py,sha256=
|
|
10
|
+
konduktor/backends/jobset.py,sha256=jJhNueZThMmEbMhpH162Ij0W_qG2Q7TmixVPQzbvHDY,8422
|
|
11
|
+
konduktor/backends/jobset_utils.py,sha256=kezVxzire4r_LR4FyhSFolPY6O2hR4hxkSeqkWj4Luc,23050
|
|
12
12
|
konduktor/check.py,sha256=JennyWoaqSKhdyfUldd266KwVXTPJpcYQa4EED4a_BA,7569
|
|
13
|
-
konduktor/cli.py,sha256=
|
|
13
|
+
konduktor/cli.py,sha256=_qfhtukWG6povdB8mfysAqzBEpICWUcBx9PYQWjHk90,36027
|
|
14
14
|
konduktor/config.py,sha256=J50JxC6MsXMnlrJPXdDUMr38C89xvOO7mR8KJ6fyils,15520
|
|
15
15
|
konduktor/constants.py,sha256=T3AeXXxuQHINW_bAWyztvDeS8r4g8kXBGIwIq13cys0,1814
|
|
16
16
|
konduktor/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -85,15 +85,15 @@ konduktor/utils/env_options.py,sha256=T41Slzf4Mzl-n45CGXXqdy2fCrYhPNZQ7RP5vmnN4x
|
|
|
85
85
|
konduktor/utils/exceptions.py,sha256=5IFnN5bIUSBJv4KRRrCepk5jyY9EG5vWWQqbjCmP3NU,6682
|
|
86
86
|
konduktor/utils/kubernetes_enums.py,sha256=SabUueF6Bpzbpa57gyH5VB65xla2N9l8CZmAeYTfGmM,176
|
|
87
87
|
konduktor/utils/kubernetes_utils.py,sha256=VG7qatUFyWHY-PCQ8fYWh2kn2TMwfg84cn-VkXdCwI8,26077
|
|
88
|
-
konduktor/utils/log_utils.py,sha256=
|
|
88
|
+
konduktor/utils/log_utils.py,sha256=OOXXFuFRI_DAWebrJuak_Q2ZXFmeldpEUbsIBsIldw0,16543
|
|
89
89
|
konduktor/utils/loki_utils.py,sha256=h2ZvZQr1nE_wXXsKsGMjhG2s2MXknNd4icydTR_ruKU,3539
|
|
90
90
|
konduktor/utils/rich_utils.py,sha256=ycADW6Ij3wX3uT8ou7T8qxX519RxlkJivsLvUahQaJo,3583
|
|
91
|
-
konduktor/utils/schemas.py,sha256=
|
|
91
|
+
konduktor/utils/schemas.py,sha256=7z_-sggfwIMRDaCSpTnBUfGHQH3M2oiwImS5PvfAefw,16858
|
|
92
92
|
konduktor/utils/subprocess_utils.py,sha256=WoFkoFhGecPR8-rF8WJxbIe-YtV94LXz9UG64SDhCY4,9448
|
|
93
93
|
konduktor/utils/ux_utils.py,sha256=czCwiS1bDqgeKtzAJctczpLwFZzAse7WuozdvzEFYJ4,7437
|
|
94
94
|
konduktor/utils/validator.py,sha256=uCRlScO1NYxsbTNKY9dkoqvlO8S0ISIIB8XmX2ItcO8,2793
|
|
95
|
-
konduktor_nightly-0.1.0.
|
|
96
|
-
konduktor_nightly-0.1.0.
|
|
97
|
-
konduktor_nightly-0.1.0.
|
|
98
|
-
konduktor_nightly-0.1.0.
|
|
99
|
-
konduktor_nightly-0.1.0.
|
|
95
|
+
konduktor_nightly-0.1.0.dev20250724105301.dist-info/LICENSE,sha256=MuuqTZbHvmqXR_aNKAXzggdV45ANd3wQ5YI7tnpZhm0,6586
|
|
96
|
+
konduktor_nightly-0.1.0.dev20250724105301.dist-info/METADATA,sha256=oLbOAvHgxyAgFgb-U_DDJ2U96EMEqW2907dmHM8eWdM,4247
|
|
97
|
+
konduktor_nightly-0.1.0.dev20250724105301.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
98
|
+
konduktor_nightly-0.1.0.dev20250724105301.dist-info/entry_points.txt,sha256=k3nG5wDFIJhNqsZWrHk4d0irIB2Ns9s47cjRWYsTCT8,48
|
|
99
|
+
konduktor_nightly-0.1.0.dev20250724105301.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|