llumo 0.1.8__py3-none-any.whl → 0.1.9b10__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.
llumo/client.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import requests
2
+ from docutils.nodes import subscript
3
+
2
4
  from .exceptions import LlumoAIError
3
5
  import time
4
6
  import re
5
7
  import json
6
8
  import uuid
7
9
  import threading
8
- from .helpingFuntions import *
9
10
  from dotenv import load_dotenv
10
11
  import os
11
12
  import itertools
@@ -13,6 +14,7 @@ import pandas as pd
13
14
  from typing import List, Dict
14
15
  from .models import AVAILABLEMODELS,getProviderFromModel
15
16
  from .execution import ModelExecutor
17
+ from .helpingFuntions import *
16
18
  from .sockets import LlumoSocketClient
17
19
  from .functionCalling import LlumoAgentExecutor
18
20
 
@@ -21,9 +23,9 @@ from .functionCalling import LlumoAgentExecutor
21
23
  envPath = os.path.join(os.path.dirname(__file__), '.env')
22
24
  load_dotenv(dotenv_path=envPath, override=False)# Automatically looks for .env in current directory
23
25
 
24
- postUrl = os.getenv("postUrl")
25
- fetchUrl = os.getenv("fetchUrl")
26
- validateUrl = os.getenv("validateUrl")
26
+ postUrl = os.getenv("POST_URL")
27
+ fetchUrl = os.getenv("FETCH_URL")
28
+ validateUrl = os.getenv("VALIDATE_URL")
27
29
  socketUrl = os.getenv("SOCKET_URL")
28
30
 
29
31
 
@@ -33,6 +35,7 @@ class LlumoClient:
33
35
  self.apiKey = api_key
34
36
  self.socket = LlumoSocketClient(socketUrl)
35
37
  self.processMapping = {}
38
+
36
39
 
37
40
 
38
41
  def validateApiKey(self, evalName = ""):
@@ -99,6 +102,9 @@ class LlumoClient:
99
102
  self.workspaceID = data["data"].get("workspaceID")
100
103
  self.evalDefinition = data["data"].get("analyticsMapping")
101
104
  self.socketToken = data["data"].get("token")
105
+ self.hasSubscribed = data["data"].get("hasSubscribed",False)
106
+ self.trialEndDate = data["data"].get("trialEndDate",None)
107
+ self.subscriptionEndDate = data["data"].get("subscriptionEndDate", None)
102
108
 
103
109
  # print(f"API key validation successful:")
104
110
  # print(f"- Remaining hits: {self.hitsAvailable}")
@@ -115,6 +121,7 @@ class LlumoClient:
115
121
  "runType": "EVAL",
116
122
  "workspaceID": workspaceID,
117
123
  }
124
+ # socketToken here if the "JWD" token
118
125
  headers = {
119
126
  "Authorization": f"Bearer {self.socketToken}",
120
127
  "Content-Type": "application/json",
@@ -127,6 +134,28 @@ class LlumoClient:
127
134
 
128
135
  except Exception as e:
129
136
  print(f"Error in posting batch: {e}")
137
+
138
+
139
+ def postDataStream(self, batch, workspaceID):
140
+ payload = {
141
+ "batch": json.dumps(batch),
142
+ "runType": "DATA_STREAM",
143
+ "workspaceID": workspaceID,
144
+ }
145
+ # socketToken here if the "JWD" token
146
+ headers = {
147
+ "Authorization": f"Bearer {self.socketToken}",
148
+ "Content-Type": "application/json",
149
+ }
150
+ try:
151
+ # print(postUrl)
152
+ response = requests.post(postUrl, json=payload, headers=headers)
153
+ # print(f"Post API Status Code: {response.status_code}")
154
+ # print(response.text)
155
+
156
+ except Exception as e:
157
+ print(f"Error in posting batch: {e}")
158
+
130
159
 
131
160
  def AllProcessMapping(self):
132
161
  for batch in self.allBatches:
@@ -143,6 +172,7 @@ class LlumoClient:
143
172
 
144
173
  for item in results:
145
174
  for rowID in item: # Each item has only one key
175
+ # for rowID in item["data"]:
146
176
  if rowID not in seen:
147
177
  seen.add(rowID)
148
178
  uniqueResults.append(item)
@@ -175,9 +205,14 @@ class LlumoClient:
175
205
  if hasattr(e, "response") and getattr(e, "response", None) is not None:
176
206
  pass
177
207
  raise
208
+ userHits = checkUserHits(self.workspaceID,self.hasSubscribed,self.trialEndDate,self.subscriptionEndDate,self.hitsAvailable,len(dataframe))
209
+
178
210
 
179
- if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
180
- raise LlumoAIError.InsufficientCredits()
211
+ if not userHits["success"]:
212
+ raise LlumoAIError.InsufficientCredits(userHits["message"])
213
+
214
+ # if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
215
+ # raise LlumoAIError.InsufficientCredits()
181
216
 
182
217
  evalDefinition = self.evalDefinition[eval]
183
218
  model = "GPT_4"
@@ -206,6 +241,7 @@ class LlumoClient:
206
241
  rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
207
242
  columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
208
243
 
244
+ # storing the generated rowID and the row index (dataframe) for later lookkup
209
245
  rowIdMapping[rowID] = index
210
246
 
211
247
  templateData = {
@@ -298,10 +334,17 @@ class LlumoClient:
298
334
  dataframe[evalName] = None
299
335
  for item in records:
300
336
  for compound_key, value in item.items():
337
+ # for compound_key, value in item['data'].items():
338
+
301
339
  rowID = compound_key.split('-')[0]
340
+ # looking for the index of each rowID , in the original dataframe
302
341
  if rowID in rowIdMapping:
303
342
  index = rowIdMapping[rowID]
304
- dataframe.at[index, evalName] = value
343
+ # dataframe.at[index, evalName] = value
344
+ dataframe.at[index, evalName] = value["value"]
345
+ dataframe.at[index, f'{evalName} Reason'] = value["reasoning"]
346
+
347
+
305
348
  else:
306
349
  pass
307
350
  # print(f"⚠️ Warning: Could not find rowID {rowID} in mapping")
@@ -338,8 +381,16 @@ class LlumoClient:
338
381
  print(f"Response content: {e.response.text[:500]}...")
339
382
  raise
340
383
 
341
- if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
342
- raise LlumoAIError.InsufficientCredits()
384
+ # check for available hits and trial limit
385
+ userHits = checkUserHits(self.workspaceID, self.hasSubscribed, self.trialEndDate, self.subscriptionEndDate,
386
+ self.hitsAvailable, len(dataframe))
387
+
388
+ # do not proceed if subscription or trial limit has exhausted
389
+ if not userHits["success"]:
390
+ raise LlumoAIError.InsufficientCredits(userHits["message"])
391
+
392
+ # if self.hitsAvailable == 0 or len(dataframe) > self.hitsAvailable:
393
+ # raise LlumoAIError.InsufficientCredits()
343
394
 
344
395
  model = "GPT_4"
345
396
  provider = "OPENAI"
@@ -555,7 +606,177 @@ class LlumoClient:
555
606
  )
556
607
  return toolResponseDf
557
608
 
609
+ def evaluateAgentResponses(self, dataframe, prompt_template="Give answer for the given query: {{query}}"):
610
+ try:
611
+ if "query" and "messageHistory" and "tools" not in dataframe.columns:
612
+ raise ValueError("DataFrame must contain 'query', 'messageHistory', and 'tools' columns")
613
+ evals = ["Tool Reliability", "Stepwise Progression", "Tool Selection Accuracy", "Final Task Alignment"]
614
+ toolResponseDf = dataframe.copy()
615
+ for eval in evals:
616
+ # Perform evaluation
617
+ toolResponseDf = self.evaluate(
618
+ toolResponseDf,
619
+ eval = eval,
620
+ prompt_template=prompt_template
621
+ )
622
+ return toolResponseDf
623
+
624
+ except Exception as e:
625
+ raise e
626
+
627
+ def runDataStream(self, dataframe, streamName:str,queryColName:str="query"):
628
+ results = {}
629
+
630
+ try:
631
+ socketID = self.socket.connect(timeout=150)
632
+ # Ensure full connection before proceeding
633
+ max_wait_secs = 20
634
+ waited_secs = 0
635
+ while not self.socket._connection_established.is_set():
636
+ time.sleep(0.1)
637
+ waited_secs += 0.1
638
+ if waited_secs >= max_wait_secs:
639
+ raise RuntimeError("Timeout waiting for server 'connection-established' event.")
640
+ # print(f"Connected with socket ID: {socketID}")
641
+ rowIdMapping = {}
642
+ try:
643
+ # print(f"Validating API key...")
644
+ self.validateApiKey()
645
+ # print(f"API key validation successful. Hits available: {self.hitsAvailable}")
646
+ except Exception as e:
647
+ print(f"Error during API key validation: {str(e)}")
648
+ if hasattr(e, "response") and getattr(e, "response", None) is not None:
649
+ print(f"Status code: {e.response.status_code}")
650
+ print(f"Response content: {e.response.text[:500]}...")
651
+ raise
652
+ # check for available hits and trial limit
653
+ userHits = checkUserHits(self.workspaceID, self.hasSubscribed, self.trialEndDate, self.subscriptionEndDate,
654
+ self.hitsAvailable, len(dataframe))
655
+
656
+ # do not proceed if subscription or trial limit has exhausted
657
+ if not userHits["success"]:
658
+ raise LlumoAIError.InsufficientCredits(userHits["message"])
659
+
660
+
558
661
 
662
+ print("====🚀Sit back while we fetch data from the stream 🚀====")
663
+ workspaceID = self.workspaceID
664
+ streamId=getStreamId(workspaceID,self.apiKey,streamName)
665
+ # Prepare all batches before sending
666
+ # print("Preparing batches...")
667
+ self.allBatches = []
668
+ currentBatch = []
669
+
670
+ for index, row in dataframe.iterrows():
671
+ activePlayground = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
672
+ rowID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
673
+ columnID = f"{int(time.time() * 1000)}{uuid.uuid4()}".replace("-", "")
674
+
675
+ rowIdMapping[rowID] = index
676
+ # Use the server-provided socket ID here
677
+ templateData = {
678
+ "processID": getProcessID(),
679
+ "socketID": socketID,
680
+ "processData": {
681
+ "executionDependency": {
682
+ "query": row[queryColName]
683
+ },
684
+ "dataStreamID": streamId
685
+ },
686
+ "workspaceID": workspaceID,
687
+ "email": "",
688
+ "type": "DATA_STREAM",
689
+ "playgroundID": activePlayground,
690
+ "processType": "DATA_STREAM",
691
+ "rowID": rowID,
692
+ "columnID": columnID,
693
+ "source": "SDK"
694
+
695
+ }
696
+
697
+
698
+ currentBatch.append(templateData)
699
+
700
+ if len(currentBatch) == 10 or index == len(dataframe) - 1:
701
+ self.allBatches.append(currentBatch)
702
+ currentBatch = []
703
+
704
+ # Post all batches
705
+ total_items = sum(len(batch) for batch in self.allBatches)
706
+ # print(f"Posting {len(self.allBatches)} batches ({total_items} items total)")
707
+
708
+ for cnt, batch in enumerate(self.allBatches):
709
+ # print(f"Posting batch {cnt + 1}/{len(self.allBatches)} for eval '{eval}'")
710
+ try:
711
+ self.postDataStream(batch=batch, workspaceID=workspaceID)
712
+ # print(f"Batch {cnt + 1} posted successfully")
713
+ except Exception as e:
714
+ print(f"Error posting batch {cnt + 1}: {str(e)}")
715
+ continue
716
+
717
+ # Small delay between batches to prevent overwhelming the server
718
+ time.sleep(1)
719
+
720
+ # updating the dict for row column mapping
721
+ self.AllProcessMapping()
722
+ # Calculate a reasonable timeout based on the data size
723
+ timeout = max(60, min(600, total_items * 10))
724
+ # print(f"All batches posted. Waiting up to {timeout} seconds for results...")
725
+
726
+ # Listen for results
727
+ self.socket.listenForResults(min_wait=20, max_wait=timeout, inactivity_timeout=30,expected_results=None)
728
+
729
+ # Get results for this evaluation
730
+ eval_results = self.socket.getReceivedData()
731
+ # print(f"Received {len(eval_results)} results for evaluation '{eval}'")
732
+
733
+ # Add these results to our overall results
734
+ results["Data Stream"] = self.finalResp(eval_results)
735
+ print(f"=======You are all set! continue your expectations 🚀======\n")
736
+
737
+
738
+ # print("All evaluations completed successfully")
739
+
740
+ except Exception as e:
741
+ print(f"Error during evaluation: {e}")
742
+ raise
743
+ finally:
744
+ # Always disconnect the socket when done
745
+ try:
746
+ self.socket.disconnect()
747
+ # print("Socket disconnected")
748
+ except Exception as e:
749
+ print(f"Error disconnecting socket: {e}")
750
+
751
+ for streamName, records in results.items():
752
+ dataframe[streamName] = None
753
+ for item in records:
754
+ for compound_key, value in item.items():
755
+ # for compound_key, value in item['data'].items():
756
+
757
+ rowID = compound_key.split('-')[0]
758
+ # looking for the index of each rowID , in the original dataframe
759
+ if rowID in rowIdMapping:
760
+ index = rowIdMapping[rowID]
761
+ # dataframe.at[index, evalName] = value
762
+ dataframe.at[index, streamName] = value["value"]
763
+
764
+
765
+
766
+ else:
767
+ pass
768
+ # print(f"⚠️ Warning: Could not find rowID {rowID} in mapping")
769
+
770
+ return dataframe
771
+
772
+
773
+
774
+ def getId(self,workspaceID,streamName):
775
+ streamId=getStreamId(workspaceID,self.apiKey,streamName)
776
+ return streamId
777
+
778
+
779
+
559
780
  class SafeDict(dict):
560
781
  def __missing__(self, key):
561
782
  return ""
llumo/exceptions.py CHANGED
@@ -30,8 +30,10 @@ class LlumoAIError(Exception):
30
30
  return LlumoAIError(f"error: {detail}")
31
31
 
32
32
  @staticmethod
33
- def InsufficientCredits():
34
- return LlumoAIError("LLumo hits exhausted")
33
+ def InsufficientCredits(details):
34
+ return LlumoAIError(details)
35
+
36
+ # return LlumoAIError("LLumo hits exhausted")
35
37
 
36
38
  @staticmethod
37
39
  def InvalidPromptTemplate():
@@ -42,4 +44,8 @@ class LlumoAIError(Exception):
42
44
 
43
45
  @staticmethod
44
46
  def modelHitsExhausted(details = "Your credits for the selected model exhausted."):
45
- return LlumoAIError(details)
47
+ return LlumoAIError(details)
48
+
49
+ # @staticmethod
50
+ # def dateNotFound():
51
+ # return LlumoAIError("Trial end date or subscription end date not found for the given user.")
llumo/helpingFuntions.py CHANGED
@@ -1,6 +1,19 @@
1
1
  import time
2
2
  import uuid
3
3
  import numpy as np
4
+ from datetime import datetime
5
+ from dateutil import parser
6
+ import requests
7
+ import json
8
+ import base64
9
+ import os
10
+ from dotenv import load_dotenv
11
+ load_dotenv()
12
+
13
+ subscriptionUrl = os.getenv("SUBSCRIPTION_URL")
14
+ getStreamdataUrl = os.getenv("DATA_STREAM_URL")
15
+
16
+
4
17
 
5
18
  def getProcessID():
6
19
  return f"{int(time.time() * 1000)}{uuid.uuid4()}"
@@ -48,3 +61,77 @@ def costColumnMapping(costResults, allProcess):
48
61
  cost_saving.append("error occured")
49
62
 
50
63
  return compressed_prompt, compressed_prompt_output, cost, cost_saving
64
+
65
+
66
+ def checkUserHits(workspaceID, hasSubscribed, trialEndDate, subscriptionEndDate, remainingHits, datasetLength):
67
+ # Get the current date (only the date part)
68
+ current_date = datetime.now().date()
69
+
70
+ # Parse trialEndDate if provided
71
+ if trialEndDate is not None:
72
+ try:
73
+ trialEndDate = parser.parse(trialEndDate).date()
74
+ except Exception:
75
+ return {"success": False, "message": "Invalid trialEndDate format"}
76
+
77
+ # Parse subscriptionEndDate if provided
78
+ if subscriptionEndDate is not None:
79
+ try:
80
+ subscriptionEndDate = parser.parse(subscriptionEndDate).date()
81
+ except Exception:
82
+ return {"success": False, "message": "Invalid subscriptionEndDate format"}
83
+
84
+ # If user is on a free trial
85
+ if not hasSubscribed and trialEndDate is not None:
86
+ if current_date > trialEndDate:
87
+ return {"success": False, "message": "Trial expired. Access denied"}
88
+
89
+ if remainingHits < datasetLength or remainingHits <= 0:
90
+ return {"success": False, "message": "Trial Hits Exhausted"}
91
+
92
+ else:
93
+ if subscriptionEndDate and current_date > subscriptionEndDate:
94
+ return {"success": False, "message": "Subscription expired. Access denied."}
95
+
96
+ if remainingHits <= 0 or remainingHits < datasetLength:
97
+ headers = {
98
+ "Authorization": f"Bearer {base64.b64encode(workspaceID.encode()).decode()}",
99
+ "Content-Type": "application/json",
100
+ }
101
+ reqBody = {"unitsToSet": 1}
102
+ responseBody = requests.post(url=subscriptionUrl, json=reqBody, headers=headers)
103
+ response = json.loads(responseBody.text)
104
+
105
+ proceed = response.get("execution", "")
106
+ print(proceed)
107
+
108
+ if proceed:
109
+ return {"success": True, "message": "Hits added and access granted."}
110
+
111
+ return {"success": True, "message": "Access granted."}
112
+
113
+ def getStreamId(workspaceID: str, token, dataStreamName):
114
+ headers = {
115
+ "Authorization": f"Bearer {token}",
116
+ "Content-Type": "application/json",
117
+ }
118
+ reqBody = {"workspaceID": workspaceID}
119
+ response = requests.post(url=getStreamdataUrl, json=reqBody, headers=headers)
120
+
121
+ if response.status_code == 200:
122
+ responseJson = response.json()
123
+ data = responseJson.get("data", [])
124
+
125
+ # Find stream by name
126
+ matchedStream = next((stream for stream in data if stream.get("name") == dataStreamName), None)
127
+
128
+ if matchedStream:
129
+
130
+ return matchedStream.get("dataStreamID")
131
+
132
+ else:
133
+ print(f"No stream found with name: {dataStreamName}")
134
+ return None
135
+ else:
136
+ print("Error:", response.status_code, response.text)
137
+ return None
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: llumo
3
+ Version: 0.1.9b10
4
+ Summary: Python SDK for interacting with the Llumo ai API.
5
+ Home-page: https://www.llumo.ai/
6
+ Author: Llumo
7
+ Author-email: product@llumo.ai
8
+ Requires-Python: >=3.7
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: author
12
+ Dynamic: author-email
13
+ Dynamic: description-content-type
14
+ Dynamic: home-page
15
+ Dynamic: license-file
16
+ Dynamic: requires-python
17
+ Dynamic: summary
@@ -0,0 +1,13 @@
1
+ llumo/__init__.py,sha256=O04b4yW1BnOvcHzxWFddAKhtdBEhBNhLdb6xgnpHH_Q,205
2
+ llumo/client.py,sha256=F4zabvAjLnu0N61qSw5DqerNlV5ybC2DbxaI55olldg,31916
3
+ llumo/exceptions.py,sha256=iCj7HhtO_ckC2EaVBdXbAudNpuMDsYmmMEV5lwynZ-E,1854
4
+ llumo/execution.py,sha256=x88wQV8eL99wNN5YtjFaAMCIfN1PdfQVlAZQb4vzgQ0,1413
5
+ llumo/functionCalling.py,sha256=QtuTtyoz5rnfNUrNT1kzegNPOrMFjrlgxZfwTqRMdiA,7190
6
+ llumo/helpingFuntions.py,sha256=9w1J4wMnJV1v_5yFMyxIHcvc16sEq5MZzOFBPagij5Q,4467
7
+ llumo/models.py,sha256=YH-qAMnShmUpmKE2LQAzQdpRsaXkFSlOqMxHwU4zBUI,1560
8
+ llumo/sockets.py,sha256=Qxxqtx3Hg07HLhA4QfcipK1ChiOYhHZBu02iA6MfYlQ,5579
9
+ llumo-0.1.9b10.dist-info/licenses/LICENSE,sha256=tF9yAcfPV9xGT3ViWmC8hPvOo8BEk4ZICbUfcEo8Dlk,182
10
+ llumo-0.1.9b10.dist-info/METADATA,sha256=QkiIW2Z2Lk86q5v9Reg4Pq0bNPTpWXR6NWGT0aW-Esk,429
11
+ llumo-0.1.9b10.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
12
+ llumo-0.1.9b10.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
13
+ llumo-0.1.9b10.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,26 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: llumo
3
- Version: 0.1.8
4
- Summary: Python SDK for interacting with the Llumo ai API.
5
- Home-page: https://www.llumo.ai/
6
- Author: Llumo
7
- Author-email: product@llumo.ai
8
- License: Proprietary
9
- Requires-Python: >=3.7
10
- License-File: LICENSE
11
- Requires-Dist: requests>=2.0.0
12
- Requires-Dist: websocket-client>=1.0.0
13
- Requires-Dist: pandas>=1.0.0
14
- Requires-Dist: numpy>=1.0.0
15
- Requires-Dist: python-socketio[client]==5.13.0
16
- Requires-Dist: python-dotenv==1.1.0
17
- Requires-Dist: openai==1.75.0
18
- Requires-Dist: google-generativeai==0.8.5
19
- Dynamic: author
20
- Dynamic: author-email
21
- Dynamic: home-page
22
- Dynamic: license
23
- Dynamic: license-file
24
- Dynamic: requires-dist
25
- Dynamic: requires-python
26
- Dynamic: summary
@@ -1,13 +0,0 @@
1
- llumo/__init__.py,sha256=O04b4yW1BnOvcHzxWFddAKhtdBEhBNhLdb6xgnpHH_Q,205
2
- llumo/client.py,sha256=Qc-LTgAiW4D8Q18oZlkNTDP8Csd_fPDI41zYaXkFD1M,22437
3
- llumo/exceptions.py,sha256=KGmztP61dkkzCTBEiRv5Xe9HrLNv1s_fYioRCG64GUU,1656
4
- llumo/execution.py,sha256=x88wQV8eL99wNN5YtjFaAMCIfN1PdfQVlAZQb4vzgQ0,1413
5
- llumo/functionCalling.py,sha256=QtuTtyoz5rnfNUrNT1kzegNPOrMFjrlgxZfwTqRMdiA,7190
6
- llumo/helpingFuntions.py,sha256=BWRoAAXG3Dsovc9bk-pDD2feQM_3ERXz_MwNgux0e7Q,1418
7
- llumo/models.py,sha256=YH-qAMnShmUpmKE2LQAzQdpRsaXkFSlOqMxHwU4zBUI,1560
8
- llumo/sockets.py,sha256=Qxxqtx3Hg07HLhA4QfcipK1ChiOYhHZBu02iA6MfYlQ,5579
9
- llumo-0.1.8.dist-info/licenses/LICENSE,sha256=tF9yAcfPV9xGT3ViWmC8hPvOo8BEk4ZICbUfcEo8Dlk,182
10
- llumo-0.1.8.dist-info/METADATA,sha256=nRQD4QA9Bf5OPisfsG4tjJwXN4fg5eADW1YuDxvZ-dg,695
11
- llumo-0.1.8.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
12
- llumo-0.1.8.dist-info/top_level.txt,sha256=d5zUTMI99llPtLRB8rtSrqELm_bOqX-bNC5IcwlDk88,6
13
- llumo-0.1.8.dist-info/RECORD,,