nuclia 4.8.8__py3-none-any.whl → 4.8.10__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.
nuclia/lib/kb.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import base64
2
- import csv
3
2
  import os
4
3
  from enum import Enum
5
4
  from typing import Dict, Optional, Union
@@ -15,17 +14,17 @@ from tqdm import tqdm
15
14
  from nuclia_models.events.activity_logs import ( # type: ignore
16
15
  ActivityLogsQuery,
17
16
  ActivityLogsSearchQuery,
18
- ActivityLogsChatQuery,
17
+ ActivityLogsAskQuery,
19
18
  DownloadActivityLogsQuery,
20
19
  DownloadActivityLogsSearchQuery,
21
- DownloadActivityLogsChatQuery,
20
+ DownloadActivityLogsAskQuery,
22
21
  EventType,
23
22
  DownloadFormat,
24
23
  )
25
24
  from nuclia_models.events.remi import RemiQuery
26
25
  from nuclia_models.worker.tasks import TaskStartKB
27
26
  from nuclia.exceptions import RateLimitError
28
- from nuclia.lib.utils import handle_http_errors
27
+ from nuclia.lib.utils import handle_http_sync_errors, handle_http_async_errors
29
28
  from datetime import datetime
30
29
  from nuclia.lib.utils import build_httpx_client, build_httpx_async_client, USER_AGENT
31
30
 
@@ -46,11 +45,9 @@ DOWNLOAD_EXPORT_URL = "/export/{export_id}"
46
45
  DOWNLOAD_URL = "/{uri}"
47
46
  TUS_UPLOAD_RESOURCE_URL = "/resource/{rid}/file/{field}/tusupload"
48
47
  TUS_UPLOAD_URL = "/tusupload"
49
- LEGACY_ACTIVITY_LOG_URL = "/activity/download?type={type}&month={month}"
50
48
  ACTIVITY_LOG_URL = "/activity/{type}/query/download"
51
49
  ACTIVITY_LOG_DOWNLOAD_REQUEST_URL = "/activity/download_request/{request_id}"
52
50
  ACTIVITY_LOG_QUERY_URL = "/activity/{type}/query"
53
- FEEDBACK_LOG_URL = "/feedback/{month}"
54
51
  NOTIFICATIONS = "/notifications"
55
52
  REMI_QUERY_URL = "/remi/query"
56
53
  REMI_EVENT_URL = "/remi/events/{event_id}"
@@ -73,23 +70,6 @@ class Environment(str, Enum):
73
70
  OSS = "OSS"
74
71
 
75
72
 
76
- class LogType(str, Enum):
77
- # Nucliadb
78
- VISITED = "visited"
79
- MODIFIED = "modified"
80
- DELETED = "deleted"
81
- NEW = "new"
82
- SEARCH = "search"
83
- SUGGEST = "suggest"
84
- INDEXED = "indexed"
85
- CHAT = "chat"
86
- # Tasks
87
- STARTED = "started"
88
- STOPPED = "stopped"
89
- # Processor
90
- PROCESSED = "processed"
91
-
92
-
93
73
  class BaseNucliaDBClient:
94
74
  environment: Environment
95
75
  base_url: str
@@ -220,7 +200,7 @@ class NucliaDBClient(BaseNucliaDBClient):
220
200
  raise Exception("KB not configured")
221
201
  url = f"{self.url}{NOTIFICATIONS}"
222
202
  response = self.stream_session.get(url, stream=True, timeout=3660)
223
- handle_http_errors(response)
203
+ handle_http_sync_errors(response)
224
204
  return response
225
205
 
226
206
  def ask(self, request: AskRequest, timeout: int = 1000):
@@ -233,7 +213,7 @@ class NucliaDBClient(BaseNucliaDBClient):
233
213
  stream=True,
234
214
  timeout=timeout,
235
215
  )
236
- handle_http_errors(response)
216
+ handle_http_sync_errors(response)
237
217
  return response
238
218
 
239
219
  def download_export(self, export_id: str, path: str, chunk_size: int):
@@ -276,7 +256,7 @@ class NucliaDBClient(BaseNucliaDBClient):
276
256
  new_uri = "/".join(uri_parts[3:])
277
257
  url = DOWNLOAD_URL.format(uri=new_uri)
278
258
  response: httpx.Response = self.reader_session.get(url)
279
- handle_http_errors(response)
259
+ handle_http_sync_errors(response)
280
260
  return response.content
281
261
 
282
262
  @backoff.on_exception(
@@ -320,7 +300,7 @@ class NucliaDBClient(BaseNucliaDBClient):
320
300
  headers["x-extract-strategy"] = extract_strategy
321
301
 
322
302
  response: httpx.Response = self.writer_session.post(url, headers=headers)
323
- handle_http_errors(response)
303
+ handle_http_sync_errors(response)
324
304
  return response.headers.get("Location")
325
305
 
326
306
  def patch_tus_upload(self, upload_url: str, data: bytes, offset: int) -> int:
@@ -338,7 +318,7 @@ class NucliaDBClient(BaseNucliaDBClient):
338
318
  response: httpx.Response = self.writer_session.patch(
339
319
  url, headers=headers, content=data
340
320
  )
341
- handle_http_errors(response)
321
+ handle_http_sync_errors(response)
342
322
  return int(response.headers.get("Upload-Offset"))
343
323
 
344
324
  def summarize(self, request: SummarizeRequest, timeout: int = 1000):
@@ -349,44 +329,13 @@ class NucliaDBClient(BaseNucliaDBClient):
349
329
  response = self.reader_session.post(
350
330
  url, json=request.model_dump(), timeout=timeout
351
331
  )
352
- handle_http_errors(response)
332
+ handle_http_sync_errors(response)
353
333
  return response
354
334
 
355
- def logs(self, type: LogType, month: str) -> list[list[str]]:
356
- if self.reader_session is None:
357
- raise Exception("KB not configured")
358
-
359
- if type != "feedback":
360
- url = LEGACY_ACTIVITY_LOG_URL.format(type=type.value, month=month)
361
- response: httpx.Response = self.reader_session.get(url)
362
- handle_http_errors(response)
363
- return [row for row in csv.reader(response.iter_lines())]
364
- else:
365
- feedback_url = f"{self.url}{FEEDBACK_LOG_URL.format(month=month)}"
366
- feedback_response: httpx.Response = self.reader_session.get(feedback_url)
367
- handle_http_errors(feedback_response)
368
- feedbacks = [row for row in csv.reader(feedback_response.iter_lines())]
369
- answers = self.logs(type=LogType.CHAT, month=month)
370
- # first row with the columns headers
371
- results = [[*feedbacks[0], *answers[0][:-1]]]
372
- for feedback in feedbacks[1:]:
373
- learning_id = feedback[1]
374
- # search for the corresponding question/answer
375
- # (the learning id is the same for both question/answer and feedback,
376
- # and is the second column in the Q/A csv)
377
- matching_answers = [
378
- answer for answer in answers if answer[1] == learning_id
379
- ]
380
- if len(matching_answers) > 0:
381
- results.append([*feedback, *matching_answers[0][:-1]])
382
- else:
383
- results.append(feedback)
384
- return results
385
-
386
335
  def logs_query(
387
336
  self,
388
337
  type: EventType,
389
- query: Union[ActivityLogsQuery, ActivityLogsSearchQuery, ActivityLogsChatQuery],
338
+ query: Union[ActivityLogsQuery, ActivityLogsSearchQuery, ActivityLogsAskQuery],
390
339
  ) -> requests.Response:
391
340
  if self.stream_session is None:
392
341
  raise Exception("KB not configured")
@@ -396,7 +345,7 @@ class NucliaDBClient(BaseNucliaDBClient):
396
345
  json=query.model_dump(mode="json", exclude_unset=True),
397
346
  stream=True,
398
347
  )
399
- handle_http_errors(response)
348
+ handle_http_sync_errors(response)
400
349
  return response
401
350
 
402
351
  def logs_download(
@@ -405,7 +354,7 @@ class NucliaDBClient(BaseNucliaDBClient):
405
354
  query: Union[
406
355
  DownloadActivityLogsQuery,
407
356
  DownloadActivityLogsSearchQuery,
408
- DownloadActivityLogsChatQuery,
357
+ DownloadActivityLogsAskQuery,
409
358
  ],
410
359
  download_format: DownloadFormat,
411
360
  ):
@@ -420,7 +369,7 @@ class NucliaDBClient(BaseNucliaDBClient):
420
369
  json=query.model_dump(mode="json", exclude_unset=True),
421
370
  headers={"accept": format_header_value},
422
371
  )
423
- handle_http_errors(response)
372
+ handle_http_sync_errors(response)
424
373
  return response
425
374
 
426
375
  def get_download_request(
@@ -431,7 +380,7 @@ class NucliaDBClient(BaseNucliaDBClient):
431
380
  raise Exception("KB not configured")
432
381
  download_request_url = f"{self.url}{ACTIVITY_LOG_DOWNLOAD_REQUEST_URL.format(request_id=request_id)}"
433
382
  response: httpx.Response = self.reader_session.get(download_request_url)
434
- handle_http_errors(response)
383
+ handle_http_sync_errors(response)
435
384
  return response
436
385
 
437
386
  def remi_query(
@@ -446,7 +395,7 @@ class NucliaDBClient(BaseNucliaDBClient):
446
395
  json=query.model_dump(mode="json", exclude_unset=True),
447
396
  timeout=10,
448
397
  )
449
- handle_http_errors(response)
398
+ handle_http_sync_errors(response)
450
399
  return response
451
400
 
452
401
  def get_remi_event(
@@ -459,7 +408,7 @@ class NucliaDBClient(BaseNucliaDBClient):
459
408
  response: httpx.Response = self.reader_session.get(
460
409
  f"{self.url}{REMI_EVENT_URL.format(event_id=event_id)}"
461
410
  )
462
- handle_http_errors(response)
411
+ handle_http_sync_errors(response)
463
412
  return response
464
413
 
465
414
  def get_remi_scores(
@@ -476,7 +425,7 @@ class NucliaDBClient(BaseNucliaDBClient):
476
425
  response: httpx.Response = self.reader_session.get(
477
426
  f"{self.url}{REMI_SCORES_URL}", params=params, timeout=10
478
427
  )
479
- handle_http_errors(response)
428
+ handle_http_sync_errors(response)
480
429
  return response
481
430
 
482
431
  def list_tasks(self) -> httpx.Response:
@@ -484,7 +433,7 @@ class NucliaDBClient(BaseNucliaDBClient):
484
433
  raise Exception("KB not configured")
485
434
 
486
435
  response: httpx.Response = self.reader_session.get(f"{self.url}{LIST_TASKS}")
487
- handle_http_errors(response)
436
+ handle_http_sync_errors(response)
488
437
  return response
489
438
 
490
439
  def start_task(self, body: TaskStartKB) -> httpx.Response:
@@ -495,7 +444,7 @@ class NucliaDBClient(BaseNucliaDBClient):
495
444
  f"{self.url}{START_TASK}",
496
445
  json=body.model_dump(mode="json", exclude_unset=True),
497
446
  )
498
- handle_http_errors(response)
447
+ handle_http_sync_errors(response)
499
448
  return response
500
449
 
501
450
  def delete_task(self, task_id: str) -> httpx.Response:
@@ -505,7 +454,7 @@ class NucliaDBClient(BaseNucliaDBClient):
505
454
  response: httpx.Response = self.writer_session.delete(
506
455
  f"{self.url}{DELETE_TASK.format(task_id=task_id)}",
507
456
  )
508
- handle_http_errors(response)
457
+ handle_http_sync_errors(response)
509
458
  return response
510
459
 
511
460
  def stop_task(self, task_id: str) -> httpx.Response:
@@ -515,7 +464,7 @@ class NucliaDBClient(BaseNucliaDBClient):
515
464
  response: httpx.Response = self.writer_session.post(
516
465
  f"{self.url}{STOP_TASK.format(task_id=task_id)}",
517
466
  )
518
- handle_http_errors(response)
467
+ handle_http_sync_errors(response)
519
468
  return response
520
469
 
521
470
  def get_task(self, task_id: str) -> httpx.Response:
@@ -525,7 +474,7 @@ class NucliaDBClient(BaseNucliaDBClient):
525
474
  response: httpx.Response = self.reader_session.get(
526
475
  f"{self.url}{GET_TASK.format(task_id=task_id)}",
527
476
  )
528
- handle_http_errors(response)
477
+ handle_http_sync_errors(response)
529
478
  return response
530
479
 
531
480
  def restart_task(self, task_id: str) -> httpx.Response:
@@ -535,7 +484,7 @@ class NucliaDBClient(BaseNucliaDBClient):
535
484
  response: httpx.Response = self.writer_session.post(
536
485
  f"{self.url}{RESTART_TASK.format(task_id=task_id)}",
537
486
  )
538
- handle_http_errors(response)
487
+ handle_http_sync_errors(response)
539
488
  return response
540
489
 
541
490
 
@@ -583,7 +532,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
583
532
  url = f"{self.url}{NOTIFICATIONS}"
584
533
  req = self.reader_session.build_request("GET", url, timeout=3660)
585
534
  response = await self.reader_session.send(req, stream=True)
586
- handle_http_errors(response)
535
+ await handle_http_async_errors(response)
587
536
  return response
588
537
 
589
538
  async def ask(self, request: AskRequest, timeout: int = 1000):
@@ -594,7 +543,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
594
543
  "POST", url, json=request.model_dump(), timeout=timeout
595
544
  )
596
545
  response = await self.reader_session.send(req, stream=True)
597
- handle_http_errors(response)
546
+ await handle_http_async_errors(response)
598
547
  return response
599
548
 
600
549
  async def download_export(self, export_id: str, path: str, chunk_size: int):
@@ -635,7 +584,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
635
584
  new_uri = "/".join(uri_parts[3:])
636
585
  url = DOWNLOAD_URL.format(uri=new_uri)
637
586
  response = await self.reader_session.get(url)
638
- handle_http_errors(response)
587
+ await handle_http_async_errors(response)
639
588
  return response.content
640
589
 
641
590
  @backoff.on_exception(
@@ -679,7 +628,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
679
628
  headers["x-extract-strategy"] = extract_strategy
680
629
 
681
630
  response = await self.writer_session.post(url, headers=headers)
682
- handle_http_errors(response)
631
+ await handle_http_async_errors(response)
683
632
  return response.headers.get("Location")
684
633
 
685
634
  async def patch_tus_upload(self, upload_url: str, data: bytes, offset: int) -> int:
@@ -695,7 +644,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
695
644
  url = httpx.URL("/".join(url.path.split("/")[3:]))
696
645
 
697
646
  response = await self.writer_session.patch(url, headers=headers, content=data)
698
- handle_http_errors(response)
647
+ await handle_http_async_errors(response)
699
648
  return int(response.headers.get("Upload-Offset"))
700
649
 
701
650
  async def summarize(self, request: SummarizeRequest, timeout: int = 1000):
@@ -706,46 +655,13 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
706
655
  response = await self.reader_session.post(
707
656
  url, json=request.model_dump(), timeout=timeout
708
657
  )
709
- handle_http_errors(response)
658
+ await handle_http_async_errors(response)
710
659
  return response
711
660
 
712
- async def logs(self, type: LogType, month: str) -> list[list[str]]:
713
- if self.reader_session is None:
714
- raise Exception("KB not configured")
715
-
716
- if type != "feedback":
717
- url = LEGACY_ACTIVITY_LOG_URL.format(type=type.value, month=month)
718
- response: httpx.Response = await self.reader_session.get(url)
719
- handle_http_errors(response)
720
- return [row for row in csv.reader(response.iter_lines())]
721
- else:
722
- feedback_url = f"{self.url}{FEEDBACK_LOG_URL.format(month=month)}"
723
- feedback_response: httpx.Response = await self.reader_session.get(
724
- feedback_url
725
- )
726
- handle_http_errors(feedback_response)
727
- feedbacks = [row for row in csv.reader(feedback_response.iter_lines())]
728
- answers = await self.logs(type=LogType.CHAT, month=month)
729
- # first row with the columns headers
730
- results = [[*feedbacks[0], *answers[0][:-1]]]
731
- for feedback in feedbacks[1:]:
732
- learning_id = feedback[1]
733
- # search for the corresponding question/answer
734
- # (the learning id is the same for both question/answer and feedback,
735
- # and is the second column in the Q/A csv)
736
- matching_answers = [
737
- answer for answer in answers if answer[1] == learning_id
738
- ]
739
- if len(matching_answers) > 0:
740
- results.append([*feedback, *matching_answers[0][:-1]])
741
- else:
742
- results.append(feedback)
743
- return results
744
-
745
661
  async def logs_query(
746
662
  self,
747
663
  type: EventType,
748
- query: Union[ActivityLogsQuery, ActivityLogsSearchQuery, ActivityLogsChatQuery],
664
+ query: Union[ActivityLogsQuery, ActivityLogsSearchQuery, ActivityLogsAskQuery],
749
665
  ) -> httpx.Response:
750
666
  if self.reader_session is None:
751
667
  raise Exception("KB not configured")
@@ -754,7 +670,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
754
670
  f"{self.url}{ACTIVITY_LOG_QUERY_URL.format(type=type.value)}",
755
671
  json=query.model_dump(mode="json", exclude_unset=True),
756
672
  )
757
- handle_http_errors(response)
673
+ await handle_http_async_errors(response)
758
674
  return response
759
675
 
760
676
  async def logs_download(
@@ -763,7 +679,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
763
679
  query: Union[
764
680
  DownloadActivityLogsQuery,
765
681
  DownloadActivityLogsSearchQuery,
766
- DownloadActivityLogsChatQuery,
682
+ DownloadActivityLogsAskQuery,
767
683
  ],
768
684
  download_format: DownloadFormat,
769
685
  ):
@@ -778,7 +694,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
778
694
  json=query.model_dump(mode="json", exclude_unset=True),
779
695
  headers={"accept": format_header_value},
780
696
  )
781
- handle_http_errors(response)
697
+ await handle_http_async_errors(response)
782
698
  return response
783
699
 
784
700
  async def get_download_request(
@@ -789,7 +705,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
789
705
  raise Exception("KB not configured")
790
706
  download_request_url = f"{self.url}{ACTIVITY_LOG_DOWNLOAD_REQUEST_URL.format(request_id=request_id)}"
791
707
  response: httpx.Response = await self.reader_session.get(download_request_url)
792
- handle_http_errors(response)
708
+ await handle_http_async_errors(response)
793
709
  return response
794
710
 
795
711
  async def remi_query(
@@ -804,7 +720,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
804
720
  json=query.model_dump(mode="json", exclude_unset=True),
805
721
  timeout=10,
806
722
  )
807
- handle_http_errors(response)
723
+ await handle_http_async_errors(response)
808
724
  return response
809
725
 
810
726
  async def get_remi_event(
@@ -817,7 +733,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
817
733
  response: httpx.Response = await self.reader_session.get(
818
734
  f"{self.url}{REMI_EVENT_URL.format(event_id=event_id)}"
819
735
  )
820
- handle_http_errors(response)
736
+ await handle_http_async_errors(response)
821
737
  return response
822
738
 
823
739
  async def get_remi_scores(
@@ -834,7 +750,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
834
750
  response: httpx.Response = await self.reader_session.get(
835
751
  f"{self.url}{REMI_SCORES_URL}", params=params, timeout=10
836
752
  )
837
- handle_http_errors(response)
753
+ await handle_http_async_errors(response)
838
754
  return response
839
755
 
840
756
  async def list_tasks(self) -> httpx.Response:
@@ -844,7 +760,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
844
760
  response: httpx.Response = await self.reader_session.get(
845
761
  f"{self.url}{LIST_TASKS}"
846
762
  )
847
- handle_http_errors(response)
763
+ await handle_http_async_errors(response)
848
764
  return response
849
765
 
850
766
  async def start_task(self, body: TaskStartKB) -> httpx.Response:
@@ -855,7 +771,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
855
771
  f"{self.url}{START_TASK}",
856
772
  json=body.model_dump(mode="json", exclude_unset=True),
857
773
  )
858
- handle_http_errors(response)
774
+ await handle_http_async_errors(response)
859
775
  return response
860
776
 
861
777
  async def delete_task(self, task_id: str) -> httpx.Response:
@@ -865,7 +781,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
865
781
  response: httpx.Response = await self.writer_session.delete(
866
782
  f"{self.url}{DELETE_TASK.format(task_id=task_id)}",
867
783
  )
868
- handle_http_errors(response)
784
+ await handle_http_async_errors(response)
869
785
  return response
870
786
 
871
787
  async def stop_task(self, task_id: str) -> httpx.Response:
@@ -875,7 +791,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
875
791
  response: httpx.Response = await self.writer_session.post(
876
792
  f"{self.url}{STOP_TASK.format(task_id=task_id)}",
877
793
  )
878
- handle_http_errors(response)
794
+ await handle_http_async_errors(response)
879
795
  return response
880
796
 
881
797
  async def get_task(self, task_id: str) -> httpx.Response:
@@ -885,7 +801,7 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
885
801
  response: httpx.Response = await self.reader_session.get(
886
802
  f"{self.url}{GET_TASK.format(task_id=task_id)}",
887
803
  )
888
- handle_http_errors(response)
804
+ await handle_http_async_errors(response)
889
805
  return response
890
806
 
891
807
  async def restart_task(self, task_id: str) -> httpx.Response:
@@ -895,5 +811,5 @@ class AsyncNucliaDBClient(BaseNucliaDBClient):
895
811
  response: httpx.Response = await self.writer_session.post(
896
812
  f"{self.url}{RESTART_TASK.format(task_id=task_id)}",
897
813
  )
898
- handle_http_errors(response)
814
+ await handle_http_async_errors(response)
899
815
  return response
nuclia/lib/utils.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import json
2
- from typing import Union
3
2
 
4
3
  import httpx
5
4
  import importlib.metadata
6
5
  import requests
6
+ from httpx import Response as HttpxResponse, HTTPStatusError
7
7
  from tabulate import tabulate
8
8
  from typing import Optional
9
9
 
@@ -18,24 +18,67 @@ from nucliadb_models.search import SyncAskResponse
18
18
  from nuclia.lib.models import ActivityLogsOutput
19
19
  from nuclia_models.worker.tasks import TaskDefinition, TaskList
20
20
  from nucliadb_models.resource import KnowledgeBoxList
21
+ from requests import Response as RequestsResponse, HTTPError as RequestsHTTPError
22
+
21
23
 
22
24
  USER_AGENT = f"nuclia.py/{importlib.metadata.version('nuclia')}"
23
25
 
24
26
 
25
- def handle_http_errors(response: Union[httpx.Response, requests.models.Response]):
26
- if (
27
- response.status_code == 403
28
- and "Hydra token is either unexistent or revoked" in response.text
29
- ):
27
+ def handle_http_sync_errors(response):
28
+ try:
29
+ content = response.text
30
+ except (httpx.ResponseNotRead, requests.exceptions.RequestException):
31
+ content = "<streaming content not read>"
32
+ except Exception as e:
33
+ content = f"<error decoding content: {e}>"
34
+
35
+ _raise_for_status(response.status_code, content, response=response)
36
+
37
+
38
+ async def handle_http_async_errors(response: httpx.Response):
39
+ try:
40
+ if not response.is_closed and not response.is_stream_consumed:
41
+ await response.aread()
42
+ except Exception as e:
43
+ content = f"<failed to read stream: {e}>"
44
+ _raise_for_status(
45
+ response.status_code, content, response=response, request=response.request
46
+ )
47
+ return # Defensive
48
+ try:
49
+ content = response.text
50
+ except httpx.ResponseNotRead:
51
+ content = "<streaming content not read>"
52
+ except Exception as e:
53
+ content = f"<error decoding content: {e}>"
54
+
55
+ _raise_for_status(
56
+ response.status_code, content, response=response, request=response.request
57
+ )
58
+
59
+
60
+ def _raise_for_status(status_code: int, content: str, response=None, request=None):
61
+ if status_code == 403 and "Hydra token is either unexistent or revoked" in content:
30
62
  raise UserTokenExpired()
31
- elif response.status_code == 429:
32
- raise RateLimitError(f"Rate limited: {response.text}")
33
- elif response.status_code == 409:
34
- raise DuplicateError(f"Duplicate resource: {response.text}")
35
- elif response.status_code == 422:
36
- raise InvalidPayload(f"Invalid payload: {response.text}")
37
- elif response.status_code >= 400:
38
- raise httpx.HTTPError(f"Status code {response.status_code}: {response.text}")
63
+ elif status_code == 429:
64
+ raise RateLimitError(f"Rate limited: {content}")
65
+ elif status_code == 409:
66
+ raise DuplicateError(f"Duplicate resource: {content}")
67
+ elif status_code == 422:
68
+ raise InvalidPayload(f"Invalid payload: {content}")
69
+ elif status_code >= 400:
70
+ if isinstance(response, HttpxResponse):
71
+ raise HTTPStatusError(
72
+ f"Status code {status_code}: {content}",
73
+ request=request,
74
+ response=response,
75
+ )
76
+ elif isinstance(response, RequestsResponse):
77
+ raise RequestsHTTPError(
78
+ f"Status code {status_code}: {content}", response=response
79
+ )
80
+ else:
81
+ raise Exception(f"Status code {status_code}: {content}")
39
82
 
40
83
 
41
84
  def serialize(obj):
nuclia/sdk/logs.py CHANGED
@@ -1,11 +1,11 @@
1
1
  from nuclia.decorators import kb
2
- from nuclia.lib.kb import LogType, NucliaDBClient, AsyncNucliaDBClient
2
+ from nuclia.lib.kb import NucliaDBClient, AsyncNucliaDBClient
3
3
  from nuclia_models.events.activity_logs import ( # type: ignore
4
4
  ActivityLogsQuery,
5
- ActivityLogsChatQuery,
5
+ ActivityLogsAskQuery,
6
6
  ActivityLogsSearchQuery,
7
7
  DownloadActivityLogsQuery,
8
- DownloadActivityLogsChatQuery,
8
+ DownloadActivityLogsAskQuery,
9
9
  DownloadActivityLogsSearchQuery,
10
10
  DownloadFormat,
11
11
  EventType,
@@ -25,23 +25,6 @@ WAIT_FOR_DOWNLOAD_TIMEOUT = 120
25
25
 
26
26
 
27
27
  class NucliaLogs:
28
- @kb
29
- def get(
30
- self, *args, type: Union[LogType, str], month: str, **kwargs
31
- ) -> list[list[str]]:
32
- """
33
- Download activity logs.
34
-
35
- :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, STARTED, STOPPED, PROCESSED
36
- :param month: YYYY-MM
37
- """
38
- if isinstance(type, str):
39
- type = LogType[type.upper()]
40
-
41
- ndb: NucliaDBClient = kwargs["ndb"]
42
- resp = ndb.logs(type=type, month=month)
43
- return resp
44
-
45
28
  @kb
46
29
  def query(
47
30
  self,
@@ -51,25 +34,25 @@ class NucliaLogs:
51
34
  dict,
52
35
  ActivityLogsQuery,
53
36
  ActivityLogsSearchQuery,
54
- ActivityLogsChatQuery,
37
+ ActivityLogsAskQuery,
55
38
  ],
56
39
  **kwargs,
57
40
  ) -> ActivityLogsOutput:
58
41
  """
59
42
  Query activity logs.
60
43
 
61
- :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, STARTED, STOPPED, PROCESSED
44
+ :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, ASK, STARTED, STOPPED, PROCESSED
62
45
  :param query: ActivityLogsQuery
63
46
  """
64
47
  _type = EventType[type.upper()] if isinstance(type, str) else type
65
48
  _query: Union[
66
49
  ActivityLogsQuery,
67
50
  ActivityLogsSearchQuery,
68
- ActivityLogsChatQuery,
51
+ ActivityLogsAskQuery,
69
52
  ]
70
53
  if isinstance(query, dict):
71
- if _type is EventType.CHAT:
72
- _query = ActivityLogsChatQuery.model_validate(query)
54
+ if _type in (EventType.ASK, EventType.CHAT): # TODO: deprecate chat event
55
+ _query = ActivityLogsAskQuery.model_validate(query)
73
56
  elif type is EventType.SEARCH:
74
57
  _query = ActivityLogsSearchQuery.model_validate(query)
75
58
  else:
@@ -95,7 +78,7 @@ class NucliaLogs:
95
78
  dict,
96
79
  DownloadActivityLogsQuery,
97
80
  DownloadActivityLogsSearchQuery,
98
- DownloadActivityLogsChatQuery,
81
+ DownloadActivityLogsAskQuery,
99
82
  ],
100
83
  download_format: Union[DownloadFormat, str],
101
84
  wait: bool = False,
@@ -104,8 +87,8 @@ class NucliaLogs:
104
87
  """
105
88
  Download activity logs.
106
89
 
107
- :param type: SEARCH, CHAT, VISITED, MODIFIED, DELETED, NEW, STARTED, STOPPED, PROCESSED
108
- :param type: NDJSON, CSV
90
+ :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, ASK, STARTED, STOPPED, PROCESSED
91
+ :param download_format: NDJSON, CSV
109
92
  :param query: DownloadActivityLogsQuery
110
93
  """
111
94
  _type = EventType[type.upper()] if isinstance(type, str) else type
@@ -119,11 +102,11 @@ class NucliaLogs:
119
102
  dict,
120
103
  DownloadActivityLogsQuery,
121
104
  DownloadActivityLogsSearchQuery,
122
- DownloadActivityLogsChatQuery,
105
+ DownloadActivityLogsAskQuery,
123
106
  ]
124
107
  if isinstance(query, dict):
125
- if _type is EventType.CHAT:
126
- _query = DownloadActivityLogsChatQuery.model_validate(query)
108
+ if _type in (EventType.ASK, EventType.CHAT): # TODO: deprecate chat event
109
+ _query = DownloadActivityLogsAskQuery.model_validate(query)
127
110
  elif type is EventType.SEARCH:
128
111
  _query = DownloadActivityLogsSearchQuery.model_validate(query)
129
112
  else:
@@ -168,23 +151,6 @@ class NucliaLogs:
168
151
 
169
152
 
170
153
  class AsyncNucliaLogs:
171
- @kb
172
- async def get(
173
- self, *args, type: Union[LogType, str], month: str, **kwargs
174
- ) -> list[list[str]]:
175
- """
176
- Download activity logs.
177
-
178
- :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, STARTED, STOPPED, PROCESSED
179
- :param month: YYYY-MM
180
- """
181
- if isinstance(type, str):
182
- type = LogType[type.upper()]
183
-
184
- ndb: AsyncNucliaDBClient = kwargs["ndb"]
185
- resp = await ndb.logs(type=type, month=month)
186
- return resp
187
-
188
154
  @kb
189
155
  async def query(
190
156
  self,
@@ -194,25 +160,25 @@ class AsyncNucliaLogs:
194
160
  dict,
195
161
  ActivityLogsQuery,
196
162
  ActivityLogsSearchQuery,
197
- ActivityLogsChatQuery,
163
+ ActivityLogsAskQuery,
198
164
  ],
199
165
  **kwargs,
200
166
  ) -> ActivityLogsOutput:
201
167
  """
202
168
  Query activity logs.
203
169
 
204
- :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, STARTED, STOPPED, PROCESSED
170
+ :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, ASK, STARTED, STOPPED, PROCESSED
205
171
  :param query: ActivityLogsQuery
206
172
  """
207
173
  _type = EventType[type.upper()] if isinstance(type, str) else type
208
174
  _query: Union[
209
175
  ActivityLogsQuery,
210
176
  ActivityLogsSearchQuery,
211
- ActivityLogsChatQuery,
177
+ ActivityLogsAskQuery,
212
178
  ]
213
179
  if isinstance(query, dict):
214
- if _type is EventType.CHAT:
215
- _query = ActivityLogsChatQuery.model_validate(query)
180
+ if _type in (EventType.ASK, EventType.CHAT): # TODO: deprecate chat event
181
+ _query = ActivityLogsAskQuery.model_validate(query)
216
182
  elif type is EventType.SEARCH:
217
183
  _query = ActivityLogsSearchQuery.model_validate(query)
218
184
  else:
@@ -238,7 +204,7 @@ class AsyncNucliaLogs:
238
204
  dict,
239
205
  DownloadActivityLogsQuery,
240
206
  DownloadActivityLogsSearchQuery,
241
- DownloadActivityLogsChatQuery,
207
+ DownloadActivityLogsAskQuery,
242
208
  ],
243
209
  download_format: Union[DownloadFormat, str],
244
210
  wait: bool = False,
@@ -247,8 +213,8 @@ class AsyncNucliaLogs:
247
213
  """
248
214
  Download activity logs.
249
215
 
250
- :param type: SEARCH, CHAT, VISITED, MODIFIED, DELETED, NEW, STARTED, STOPPED, PROCESSED
251
- :param type: NDJSON, CSV
216
+ :param type: VISITED, MODIFIED, DELETED, NEW, SEARCH, SUGGEST, INDEXED, CHAT, ASK, STARTED, STOPPED, PROCESSED
217
+ :param download_format: NDJSON, CSV
252
218
  :param query: DownloadActivityLogsQuery
253
219
  """
254
220
  _type = EventType[type.upper()] if isinstance(type, str) else type
@@ -262,11 +228,11 @@ class AsyncNucliaLogs:
262
228
  dict,
263
229
  DownloadActivityLogsQuery,
264
230
  DownloadActivityLogsSearchQuery,
265
- DownloadActivityLogsChatQuery,
231
+ DownloadActivityLogsAskQuery,
266
232
  ]
267
233
  if isinstance(query, dict):
268
- if _type is EventType.CHAT:
269
- _query = DownloadActivityLogsChatQuery.model_validate(query)
234
+ if _type in (EventType.ASK, EventType.CHAT): # TODO: deprecate chat event
235
+ _query = DownloadActivityLogsAskQuery.model_validate(query)
270
236
  elif type is EventType.SEARCH:
271
237
  _query = DownloadActivityLogsSearchQuery.model_validate(query)
272
238
  else:
@@ -1,24 +1,15 @@
1
1
  from nuclia.sdk.kb import NucliaKB, AsyncNucliaKB
2
2
  from nuclia.tests.fixtures import IS_PROD
3
- from nuclia.lib.kb import LogType
4
3
  from nuclia_models.events.activity_logs import (
5
4
  ActivityLogsQuery,
6
5
  Pagination,
7
6
  DownloadActivityLogsQuery,
8
7
  DownloadFormat,
8
+ EventType,
9
9
  )
10
10
  import pytest
11
11
 
12
12
 
13
- def test_logs(testing_config):
14
- if not IS_PROD:
15
- assert True
16
- return
17
- nkb = NucliaKB()
18
- logs = nkb.logs.get(type=LogType.NEW, month="2024-06")
19
- assert len(logs) == 23
20
-
21
-
22
13
  def test_activity_logs_query(testing_config):
23
14
  if not IS_PROD:
24
15
  assert True
@@ -30,7 +21,7 @@ def test_activity_logs_query(testing_config):
30
21
  pagination=Pagination(limit=10),
31
22
  )
32
23
  nkb = NucliaKB()
33
- output = nkb.logs.query(type=LogType.CHAT, query=query)
24
+ output = nkb.logs.query(type=EventType.CHAT, query=query)
34
25
  assert len(output.data) == 10
35
26
  assert output.has_more
36
27
 
@@ -46,24 +37,48 @@ def test_activity_logs_download(testing_config):
46
37
  )
47
38
  nkb = NucliaKB()
48
39
  output = nkb.logs.download(
49
- type=LogType.CHAT, query=query, download_format=DownloadFormat.NDJSON
40
+ type=EventType.CHAT, query=query, download_format=DownloadFormat.NDJSON
50
41
  )
51
42
  assert output.request_id
52
43
  assert output.download_url is None
53
44
 
54
45
 
55
46
  @pytest.mark.asyncio
56
- async def test_logs_async(testing_config):
47
+ async def test_activity_logs_query_async(testing_config):
57
48
  if not IS_PROD:
58
49
  assert True
59
50
  return
51
+ query = ActivityLogsQuery(
52
+ year_month="2024-10",
53
+ show=["id", "date", "client_type", "total_duration"],
54
+ filters={},
55
+ pagination=Pagination(limit=10),
56
+ )
60
57
  nkb = AsyncNucliaKB()
61
- logs = await nkb.logs.get(type=LogType.NEW, month="2024-06")
62
- assert len(logs) == 23
58
+ output = await nkb.logs.query(type=EventType.CHAT, query=query)
59
+ assert len(output.data) == 10
60
+ assert output.has_more
63
61
 
64
62
 
65
63
  @pytest.mark.asyncio
66
- async def test_activity_logs_query_async(testing_config):
64
+ async def test_activity_logs_download_async(testing_config):
65
+ if not IS_PROD:
66
+ assert True
67
+ return
68
+ query = DownloadActivityLogsQuery(
69
+ year_month="2024-10",
70
+ show=["id", "date", "client_type", "total_duration"],
71
+ filters={},
72
+ )
73
+ nkb = AsyncNucliaKB()
74
+ output = await nkb.logs.download(
75
+ type=EventType.CHAT, query=query, download_format=DownloadFormat.NDJSON
76
+ )
77
+ assert output.request_id
78
+ assert output.download_url is None
79
+
80
+
81
+ def test_activity_logs_ask_query(testing_config):
67
82
  if not IS_PROD:
68
83
  assert True
69
84
  return
@@ -73,14 +88,14 @@ async def test_activity_logs_query_async(testing_config):
73
88
  filters={},
74
89
  pagination=Pagination(limit=10),
75
90
  )
76
- nkb = AsyncNucliaKB()
77
- output = await nkb.logs.query(type=LogType.CHAT, query=query)
91
+ nkb = NucliaKB()
92
+ output = nkb.logs.query(type=EventType.ASK, query=query)
78
93
  assert len(output.data) == 10
79
94
  assert output.has_more
80
95
 
81
96
 
82
97
  @pytest.mark.asyncio
83
- async def test_activity_logs_download_async(testing_config):
98
+ async def test_activity_logs_ask_download_async(testing_config):
84
99
  if not IS_PROD:
85
100
  assert True
86
101
  return
@@ -91,7 +106,7 @@ async def test_activity_logs_download_async(testing_config):
91
106
  )
92
107
  nkb = AsyncNucliaKB()
93
108
  output = await nkb.logs.download(
94
- type=LogType.CHAT, query=query, download_format=DownloadFormat.NDJSON
109
+ type=EventType.ASK, query=query, download_format=DownloadFormat.NDJSON
95
110
  )
96
111
  assert output.request_id
97
112
  assert output.download_url is None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nuclia
3
- Version: 4.8.8
3
+ Version: 4.8.10
4
4
  Summary: Nuclia Python SDK
5
5
  Author-email: Nuclia <info@nuclia.com>
6
6
  License-Expression: MIT
@@ -26,7 +26,7 @@ Requires-Dist: httpcore>=1.0.0
26
26
  Requires-Dist: prompt_toolkit
27
27
  Requires-Dist: nucliadb_sdk<7,>=6.4
28
28
  Requires-Dist: nucliadb_models<7,>=6.4
29
- Requires-Dist: nuclia-models>=0.38.1
29
+ Requires-Dist: nuclia-models>=0.41.1
30
30
  Requires-Dist: tqdm
31
31
  Requires-Dist: aiofiles
32
32
  Requires-Dist: backoff
@@ -9,12 +9,12 @@ nuclia/cli/run.py,sha256=B1hP0upSbSCqqT89WAwsd93ZxkAoF6ajVyLOdYmo8fU,1560
9
9
  nuclia/cli/utils.py,sha256=iZ3P8juBdAGvaRUd2BGz7bpUXNDHdPrC5p876yyZ2Cs,1223
10
10
  nuclia/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  nuclia/lib/conversations.py,sha256=M6qhL9NPEKroYF767S-Q2XWokRrjX02kpYTzRvZKwUE,149
12
- nuclia/lib/kb.py,sha256=98zpJ8Dp4rQYMNKm4nhpOms3PY-PFOd8acNgDmBud-o,31819
12
+ nuclia/lib/kb.py,sha256=jV-L8-a5oFVHIbFfeGO17hXf2IQk9UuW-9fq5D3LO6Y,28508
13
13
  nuclia/lib/models.py,sha256=lO3TZa4jTQ6C8ptGO1oX-5pxDgv8ocO13z7j5jXOC0Y,1554
14
14
  nuclia/lib/nua.py,sha256=sUVFdCjvLigTqUUhILywdHpiC0qKCtKPABn5kUXfuxQ,27064
15
15
  nuclia/lib/nua_chat.py,sha256=ApL1Y1FWvAVUt-Y9a_8TUSJIhg8-UmBSy8TlDPn6tD8,3874
16
16
  nuclia/lib/nua_responses.py,sha256=9WRGpvvhA2ZOyOv9kzX1zI7C3ypM8ySqciVYCSibUjo,12724
17
- nuclia/lib/utils.py,sha256=XAVRRICPem-qOIEcssqDuv-vqD-9PICk6Iuj6DK5GKk,4808
17
+ nuclia/lib/utils.py,sha256=9l6DxBk-11WqUhXRn99cqeuVTUOJXj-1S6zckal7wOk,6312
18
18
  nuclia/sdk/__init__.py,sha256=-nAw8i53XBdmbfTa1FJZ0FNRMNakimDVpD6W4OdES-c,1374
19
19
  nuclia/sdk/accounts.py,sha256=7XQ3K9_jlSuk2Cez868FtazZ05xSGab6h3Mt1qMMwIE,647
20
20
  nuclia/sdk/agent.py,sha256=ot_oA4yi7TkXahPnhcRIxztq9Dtskc85-A57WN1BNGg,1961
@@ -24,7 +24,7 @@ nuclia/sdk/export_import.py,sha256=y5cTOxhILwRPIvR2Ya12bk-ReGbeDzA3C9TPxgnOHD4,9
24
24
  nuclia/sdk/kb.py,sha256=iDJimbzw_R9VeUu7a4F5wXoLdLxCim7W3UeEaAQ5dGI,26820
25
25
  nuclia/sdk/kbs.py,sha256=nXEvg5ddZYdDS8Kie7TrN-s1meU9ecYLf9FlT5xr-ro,9131
26
26
  nuclia/sdk/logger.py,sha256=UHB81eS6IGmLrsofKxLh8cmF2AsaTj_HXP0tGqMr_HM,57
27
- nuclia/sdk/logs.py,sha256=P7PJwrSfJMTJqykWZZm9yhtWtIODZYvPqsaStKoPnmo,10400
27
+ nuclia/sdk/logs.py,sha256=3jfORpo8fzZiXFFSbGY0o3Bre1ZgJaKQCXgxP1keNHw,9614
28
28
  nuclia/sdk/nua.py,sha256=6t0m0Sx-UhqNU2Hx9v6vTwy0m3a30K4T0KmP9G43MzY,293
29
29
  nuclia/sdk/nucliadb.py,sha256=bOESIppPgY7IrNqrYY7T3ESoxwttbOSTm5zj1xUS1jI,1288
30
30
  nuclia/sdk/predict.py,sha256=KF7iT2aasaB9DIEAwqktXbOl2H_Y_ne-6-SEErN7YOk,9095
@@ -44,7 +44,7 @@ nuclia/tests/test_kb/test_conversation.py,sha256=L__KMBucqChPGeYR_XwXl7Elt4LDlp1
44
44
  nuclia/tests/test_kb/test_export_import.py,sha256=lQEww2jFNHZYcudFJqcHhoWAPrmtvvnPvcFqrijxLbo,1019
45
45
  nuclia/tests/test_kb/test_graph.py,sha256=PQSdqvPvOHr71pfdbUpsAK-_DxBQh7spxtJaaleF87A,1434
46
46
  nuclia/tests/test_kb/test_labels.py,sha256=IUdTq4mzv0OrOkwBWWy4UwKGKyJybtoHrgvXr676vyY,961
47
- nuclia/tests/test_kb/test_logs.py,sha256=Iw0v-R5QLlfHKxL8roPeOQKjTylgpYgot0wG4vsXQTE,2609
47
+ nuclia/tests/test_kb/test_logs.py,sha256=Z9ELtiiU9NniITJzeWt92GCcERKYy9Nwc_fUVPboRU0,3121
48
48
  nuclia/tests/test_kb/test_remi.py,sha256=OX5N-MHbgcwpLg6fBjrAK_KhqkMspJo_VKQHCBCayZ8,2080
49
49
  nuclia/tests/test_kb/test_resource.py,sha256=05Xgmg5fwcPW2PZKnUSSjr6MPXp5w8XDgx8plfNcR68,1102
50
50
  nuclia/tests/test_kb/test_search.py,sha256=bsQhfB6-NYFwY3gqkrVJJnru153UgZqEhV22ho4VeWM,4660
@@ -61,9 +61,9 @@ nuclia/tests/test_nucliadb/test_crud.py,sha256=GuY76HRvt2DFaNgioKm5n0Aco1HnG7zzV
61
61
  nuclia/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
62
  nuclia/tests/unit/test_export_import.py,sha256=xo_wVbjUnNlVV65ZGH7LtZ38qy39EkJp2hjOuTHC1nU,980
63
63
  nuclia/tests/unit/test_nua_responses.py,sha256=t_hIdVztTi27RWvpfTJUYcCL0lpKdZFegZIwLdaPNh8,319
64
- nuclia-4.8.8.dist-info/licenses/LICENSE,sha256=Ops2LTti_HJtpmWcanuUTdTY3vKDR1myJ0gmGBKC0FA,1063
65
- nuclia-4.8.8.dist-info/METADATA,sha256=EI0YpmuSdlgl0IM4hDq9C0DJSW7mgmFoIzYz2KQh-Qo,2337
66
- nuclia-4.8.8.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
67
- nuclia-4.8.8.dist-info/entry_points.txt,sha256=iZHOyXPNS54r3eQmdi5So20xO1gudI9K2oP4sQsCJRw,46
68
- nuclia-4.8.8.dist-info/top_level.txt,sha256=cqn_EitXOoXOSUvZnd4q6QGrhm04pg8tLAZtem-Zfdo,7
69
- nuclia-4.8.8.dist-info/RECORD,,
64
+ nuclia-4.8.10.dist-info/licenses/LICENSE,sha256=Ops2LTti_HJtpmWcanuUTdTY3vKDR1myJ0gmGBKC0FA,1063
65
+ nuclia-4.8.10.dist-info/METADATA,sha256=Vcg3SQFhjDTSZ9akWQevdKqRVZyt8ModTnX5BcwgmsI,2338
66
+ nuclia-4.8.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
67
+ nuclia-4.8.10.dist-info/entry_points.txt,sha256=iZHOyXPNS54r3eQmdi5So20xO1gudI9K2oP4sQsCJRw,46
68
+ nuclia-4.8.10.dist-info/top_level.txt,sha256=cqn_EitXOoXOSUvZnd4q6QGrhm04pg8tLAZtem-Zfdo,7
69
+ nuclia-4.8.10.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.4.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5