oli-python 1.0.2__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oli-python
3
- Version: 1.0.2
3
+ Version: 1.2.0
4
4
  Summary: Python SDK for interacting with the Open Labels Initiative; A framework for address labels in the blockchain space. Read & write labels into the OLI Label Pool, check your labels for OLI compliance.
5
5
  Home-page: https://github.com/openlabelsinitiative/oli-python
6
6
  Author: Lorenz Lehmann
@@ -51,6 +51,13 @@ class OffchainAttestations:
51
51
  while response.status_code != 200 and retry > 0:
52
52
  retry -= 1
53
53
  time.sleep(2 ** (n0 - retry)) # exponential backoff
54
+ # rebuild the attestation (assigns new timestamp) to not get rate limited by EAS post endpoint
55
+ attestation = self.build_offchain_attestation(
56
+ recipient=address,
57
+ schema=self.oli.oli_label_pool_schema,
58
+ data=data,
59
+ ref_uid=ref_uid
60
+ )
54
61
  response = self.post_offchain_attestation(attestation)
55
62
 
56
63
  # if it fails after all retries, raise an error
@@ -19,7 +19,7 @@ class UtilsValidator:
19
19
  def fix_simple_tags_formatting(self, tags: dict) -> dict:
20
20
  """
21
21
  Fix basic formatting in the tags dictionary. This includes:
22
- - Ensuring all tag_ids and their value are lowercase
22
+ - Ensuring all tag_ids are lowercase
23
23
  - Booling values are converted from strings to booleans
24
24
  - Removing leading/trailing whitespace from string values
25
25
  - Checksum address (string(42)) tags
@@ -33,21 +33,22 @@ class UtilsValidator:
33
33
  # Convert tag_ids to lowercase
34
34
  tags = {k.lower(): v for k, v in tags.items()}
35
35
 
36
- # Convert all tag_values to lower case & strip whitespaces, then single boolean values from strings to booleans
36
+ # Strip whitespaces, then turn boolean values from strings to booleans
37
37
  for k, v in tags.items():
38
38
  if isinstance(v, str):
39
- tags[k] = v.strip().lower()
39
+ tags[k] = v.strip()
40
40
  if tags[k] == 'true':
41
41
  tags[k] = True
42
42
  elif tags[k] == 'false':
43
43
  tags[k] = False
44
44
  elif isinstance(v, list):
45
- tags[k] = [i.strip().lower() if isinstance(i, str) else i for i in v]
45
+ tags[k] = [i.strip() if isinstance(i, str) else i for i in v]
46
46
 
47
- # Checksum address (string(42)) and transaction hash (string(66)) tags
47
+ # Checksum address tags
48
48
  for k, v in tags.items():
49
- if k in self.oli.tag_definitions and self.oli.tag_definitions[k]['type'] == 'string(42)':
50
- tags[k] = self.oli.w3.to_checksum_address(v)
49
+ if k in self.oli.tag_definitions and 'minLength' in self.oli.tag_definitions[k]['schema']:
50
+ if self.oli.tag_definitions[k]['schema']['minLength'] == 42 and self.oli.tag_definitions[k]['schema']['maxLength'] == 42:
51
+ tags[k] = self.oli.w3.to_checksum_address(v)
51
52
 
52
53
  return tags
53
54
 
@@ -138,7 +139,7 @@ class UtilsValidator:
138
139
  print(tags)
139
140
  raise ValueError("Tags must be a dictionary with OLI compliant tags (e.g., {'contract_name': 'example', 'is_eoa': True})")
140
141
 
141
- # Check each tag_id in the dictionary
142
+ # Check each tag_id in the dictionary # TODO: redo this with tag_definitions 2.0 and schema, should be more efficient
142
143
  for tag_id in tags.keys():
143
144
 
144
145
  # Check if the tag_id is in the official OLI tag list
@@ -147,22 +148,36 @@ class UtilsValidator:
147
148
 
148
149
  # Check if the tag_id is in the correct format. So far implemented [boolean, string, integer, list, float, string(42), string(66), date (YYYY-MM-DD HH:MM:SS)]
149
150
  else:
150
- if self.oli.tag_definitions[tag_id]['type'] == 'boolean' and not isinstance(tags[tag_id], bool):
151
+ if self.oli.tag_definitions[tag_id]['schema']['type'] == 'boolean' and not isinstance(tags[tag_id], bool):
151
152
  print(f"WARNING: Tag value for {tag_id} must be a boolean (True/False).")
152
- elif self.oli.tag_definitions[tag_id]['type'] == 'string' and not isinstance(tags[tag_id], str):
153
+ elif self.oli.tag_definitions[tag_id]['schema']['type'] == 'string' and not isinstance(tags[tag_id], str):
153
154
  print(f"WARNING: Tag value for {tag_id} must be a string.")
154
- elif self.oli.tag_definitions[tag_id]['type'] == 'integer' and not isinstance(tags[tag_id], int):
155
+ elif self.oli.tag_definitions[tag_id]['schema']['type'] == 'integer' and not isinstance(tags[tag_id], int):
155
156
  print(f"WARNING: Tag value for {tag_id} must be an integer.")
156
- elif self.oli.tag_definitions[tag_id]['type'] == 'float' and not isinstance(tags[tag_id], float):
157
+ elif self.oli.tag_definitions[tag_id]['schema']['type'] == 'float' and not isinstance(tags[tag_id], float):
157
158
  print(f"WARNING: Tag value for {tag_id} must be a float.")
158
- elif self.oli.tag_definitions[tag_id]['type'] == 'list' and not isinstance(tags[tag_id], list):
159
- print(f"WARNING: Tag value for {tag_id} must be a list.")
160
- elif self.oli.tag_definitions[tag_id]['type'] == 'string(42)' and not self.oli.w3.is_address(tags[tag_id]):
159
+ elif self.oli.tag_definitions[tag_id]['schema']['type'] == 'array' and not isinstance(tags[tag_id], list):
160
+ print(f"WARNING: Tag value for {tag_id} must be an array.")
161
+ elif (
162
+ self.oli.tag_definitions[tag_id]['schema']['type'] == 'string' and
163
+ self.oli.tag_definitions[tag_id]['schema'].get('minLength') == 42 and
164
+ self.oli.tag_definitions[tag_id]['schema'].get('maxLength') == 42 and
165
+ not self.oli.w3.is_address(tags[tag_id])
166
+ ):
161
167
  print(f"WARNING: Tag value for {tag_id} must be a valid Ethereum address string with '0x'.")
162
- elif self.oli.tag_definitions[tag_id]['type'] == 'string(66)' and not (len(tags[tag_id]) == 66 and tags[tag_id].startswith('0x')):
168
+ elif (
169
+ self.oli.tag_definitions[tag_id]['schema']['type'] == 'string' and
170
+ self.oli.tag_definitions[tag_id]['schema'].get('minLength') == 66 and
171
+ self.oli.tag_definitions[tag_id]['schema'].get('maxLength') == 66 and
172
+ not (len(tags[tag_id]) == 66 and tags[tag_id].startswith('0x'))
173
+ ):
163
174
  print(f"WARNING: Tag value for {tag_id} must be a valid hex string with '0x' prefix and 64 hex characters (66 characters total).")
164
- elif self.oli.tag_definitions[tag_id]['type'] == 'date (YYYY-MM-DD HH:MM:SS)' and not isinstance(tags[tag_id], str):
165
- print(f"WARNING: Tag value for {tag_id} must be a string in the format 'YYYY-MM-DD HH:MM:SS'.")
175
+ elif (
176
+ self.oli.tag_definitions[tag_id]['schema']['type'] == 'string' and
177
+ self.oli.tag_definitions[tag_id]['schema'].get('format') == 'date-time' and
178
+ not isinstance(tags[tag_id], str)
179
+ ):
180
+ print(f"WARNING: Tag value for {tag_id} must be a string in date-time format (e.g., '2023-12-31 23:59:59').")
166
181
 
167
182
  # Check if the value is in the value set
168
183
  if tag_id in self.oli.tag_value_sets:
@@ -120,8 +120,8 @@ class OLI:
120
120
  return self.offchain.multi_revoke_attestations(uids, gas_limit)
121
121
 
122
122
  # Expose query methods
123
- def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None) -> dict:
124
- return self.graphql_client.graphql_query_attestations(address, attester, timeCreated, revocationTime)
123
+ def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None, take: int=None, id: str=None,expand_json: bool=True) -> dict:
124
+ return self.graphql_client.graphql_query_attestations(address, attester, timeCreated, revocationTime, take, id, expand_json)
125
125
 
126
126
  def get_full_raw_export_parquet(self, file_path: str="raw_labels.parquet") -> str:
127
127
  return self.data_fetcher.get_full_raw_export_parquet(file_path)
@@ -36,13 +36,26 @@ class DataFetcher:
36
36
  """
37
37
  value_sets = {}
38
38
 
39
- # value sets from self.oli.tag_definitions (must be a list)
40
- additional_value_sets = {i['tag_id']: i['value_set'] for i in self.oli.tag_definitions.values() if 'value_set' in i}
41
- for tag_id, value_set in additional_value_sets.items():
42
- if isinstance(value_set, list):
43
- # convert all string values to lowercase and keep the rest as is
44
- value_set = [i.lower() if isinstance(i, str) else i for i in value_set]
45
- value_sets[tag_id] = value_set
39
+ # Extract value sets from tag definitions (must be a list)
40
+ for tag_def in self.oli.tag_definitions.values():
41
+ if 'schema' not in tag_def:
42
+ continue
43
+
44
+ schema = tag_def['schema']
45
+ tag_id = tag_def['tag_id']
46
+ value_set = None
47
+
48
+ # Get enum from direct schema or array items
49
+ if 'enum' in schema:
50
+ value_set = schema['enum']
51
+ elif (schema.get('type') == 'array' and
52
+ 'items' in schema and
53
+ 'enum' in schema['items']):
54
+ value_set = schema['items']['enum']
55
+
56
+ # Process and add to value_sets
57
+ if value_set and isinstance(value_set, list):
58
+ value_sets[tag_id] = [i.lower() if isinstance(i, str) else i for i in value_set]
46
59
 
47
60
  # value set for owner_project
48
61
  url = "https://api.growthepie.xyz/v1/labels/projects.json"
@@ -0,0 +1,156 @@
1
+ import requests
2
+ import json
3
+
4
+ class GraphQLClient:
5
+ def __init__(self, oli_client):
6
+ """
7
+ Initialize the GraphQLClient with an OLI client.
8
+
9
+ Args:
10
+ oli_client: The OLI client instance
11
+ """
12
+ self.oli = oli_client
13
+
14
+ def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None, take: int=None, id: str=None, expand_json: bool=True) -> dict:
15
+ """
16
+ Queries attestations from the EAS GraphQL API based on the specified filters.
17
+
18
+ Args:
19
+ address (str, optional): Ethereum address of the labeled contract
20
+ attester (str, optional): Ethereum address of the attester
21
+ timeCreated (int, optional): Filter for attestations created after this timestamp
22
+ revocationTime (int, optional): Filter for attestations with revocation time >= this timestamp
23
+ take (int, optional): Maximum number of attestations to return
24
+ id (str, optional): Specific attestation ID to filter by
25
+ expand_json (bool, default: True): Whether to expand decodedDataJson fields in the response
26
+
27
+ Returns:
28
+ dict: JSON response containing matching attestation data
29
+ """
30
+ query = """
31
+ query Attestations($take: Int, $where: AttestationWhereInput, $orderBy: [AttestationOrderByWithRelationInput!]) {
32
+ attestations(take: $take, where: $where, orderBy: $orderBy) {
33
+ attester
34
+ decodedDataJson
35
+ expirationTime
36
+ id
37
+ ipfsHash
38
+ isOffchain
39
+ recipient
40
+ refUID
41
+ revocable
42
+ revocationTime
43
+ revoked
44
+ time
45
+ timeCreated
46
+ txid
47
+ }
48
+ }
49
+ """
50
+
51
+ variables = {
52
+ "where": {
53
+ "schemaId": {
54
+ "equals": self.oli.oli_label_pool_schema
55
+ }
56
+ },
57
+ "orderBy": [
58
+ {
59
+ "timeCreated": "desc"
60
+ }
61
+ ]
62
+ }
63
+
64
+ # Add take to variables if not None
65
+ if take is not None:
66
+ variables["take"] = int(take)
67
+
68
+ # Add id to where clause if not None
69
+ if id is not None:
70
+ variables["where"]["id"] = {"equals": id}
71
+
72
+ # Add address to where clause if not None
73
+ if address is not None:
74
+ variables["where"]["recipient"] = {"equals": address}
75
+
76
+ # Add attester to where clause if not None
77
+ if attester is not None:
78
+ variables["where"]["attester"] = {"equals": attester}
79
+
80
+ # Add timeCreated to where clause if not None, ensuring it's an int
81
+ if timeCreated is not None:
82
+ timeCreated = int(timeCreated)
83
+ variables["where"]["timeCreated"] = {"gt": timeCreated}
84
+
85
+ # Add revocationTime to where clause if not None, ensuring it's an int
86
+ if revocationTime is not None:
87
+ revocationTime = int(revocationTime)
88
+ variables["where"]["revocationTime"] = {"gte": revocationTime}
89
+
90
+ headers = {
91
+ "Content-Type": "application/json"
92
+ }
93
+
94
+ response = requests.post(self.oli.graphql, json={"query": query, "variables": variables}, headers=headers)
95
+
96
+ if response.status_code == 200:
97
+ if expand_json:
98
+ # Expand decodedDataJson fields in the response
99
+ return self.graphql_expand_decoded_data_json(response.json())
100
+ else:
101
+ # Return raw response if no expansion is wanted
102
+ return response.json()
103
+ else:
104
+ raise Exception(f"GraphQL query failed with status code {response.status_code}: {response.text}")
105
+
106
+ def graphql_expand_decoded_data_json(self, attestations_data: dict) -> list:
107
+ """
108
+ Expand decodedDataJson fields in attestations data into separate columns.
109
+
110
+ Args:
111
+ attestations_data (dict): GraphQL response from oli.graphql_query_attestations()
112
+
113
+ Returns:
114
+ list: List of dictionaries with expanded decodedDataJson fields
115
+ """
116
+ expanded_data = []
117
+
118
+ for row in attestations_data['data']['attestations']:
119
+ # Start with the original row data
120
+ expanded_row = row.copy()
121
+
122
+ # Check if decodedDataJson exists and is not None
123
+ if 'decodedDataJson' in row and row['decodedDataJson']:
124
+ try:
125
+ # Parse the JSON string
126
+ if isinstance(row['decodedDataJson'], str):
127
+ decoded_data = json.loads(row['decodedDataJson'])
128
+ else:
129
+ decoded_data = row['decodedDataJson']
130
+
131
+ # Extract each field from the decoded data
132
+ for item in decoded_data:
133
+ field_name = item['name']
134
+
135
+ # Extract the actual value from the nested structure
136
+ if 'value' in item and 'value' in item['value']:
137
+ value = item['value']['value']
138
+
139
+ # Handle BigNumber hex values
140
+ if isinstance(value, dict) and value.get('type') == 'BigNumber':
141
+ expanded_row[field_name] = int(value['hex'], 16)
142
+ # Handle empty arrays or objects
143
+ elif isinstance(value, (list, dict)) and not value:
144
+ expanded_row[field_name] = value
145
+ else:
146
+ expanded_row[field_name] = value
147
+ else:
148
+ expanded_row[field_name] = None
149
+
150
+ except (json.JSONDecodeError, KeyError, TypeError) as e:
151
+ # If parsing fails, keep original row and add error info
152
+ expanded_row['_parsing_error'] = str(e)
153
+
154
+ expanded_data.append(expanded_row)
155
+
156
+ return {'data': {'attestations': expanded_data}}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oli-python
3
- Version: 1.0.2
3
+ Version: 1.2.0
4
4
  Summary: Python SDK for interacting with the Open Labels Initiative; A framework for address labels in the blockchain space. Read & write labels into the OLI Label Pool, check your labels for OLI compliance.
5
5
  Home-page: https://github.com/openlabelsinitiative/oli-python
6
6
  Author: Lorenz Lehmann
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="oli-python",
8
- version="1.0.2",
8
+ version="1.2.0",
9
9
  author="Lorenz Lehmann",
10
10
  author_email="lorenz@growthepie.xyz",
11
11
  description="Python SDK for interacting with the Open Labels Initiative; A framework for address labels in the blockchain space. Read & write labels into the OLI Label Pool, check your labels for OLI compliance.",
@@ -1,87 +0,0 @@
1
- import requests
2
-
3
- class GraphQLClient:
4
- def __init__(self, oli_client):
5
- """
6
- Initialize the GraphQLClient with an OLI client.
7
-
8
- Args:
9
- oli_client: The OLI client instance
10
- """
11
- self.oli = oli_client
12
-
13
- def graphql_query_attestations(self, address: str=None, attester: str=None, timeCreated: int=None, revocationTime: int=None) -> dict:
14
- """
15
- Queries attestations from the EAS GraphQL API based on the specified filters.
16
-
17
- Args:
18
- address (str, optional): Ethereum address of the labeled contract
19
- attester (str, optional): Ethereum address of the attester
20
- timeCreated (int, optional): Filter for attestations created after this timestamp
21
- revocationTime (int, optional): Filter for attestations with revocation time >= this timestamp
22
-
23
- Returns:
24
- dict: JSON response containing matching attestation data
25
- """
26
- query = """
27
- query Attestations($take: Int, $where: AttestationWhereInput, $orderBy: [AttestationOrderByWithRelationInput!]) {
28
- attestations(take: $take, where: $where, orderBy: $orderBy) {
29
- attester
30
- decodedDataJson
31
- expirationTime
32
- id
33
- ipfsHash
34
- isOffchain
35
- recipient
36
- refUID
37
- revocable
38
- revocationTime
39
- revoked
40
- time
41
- timeCreated
42
- txid
43
- }
44
- }
45
- """
46
-
47
- variables = {
48
- "where": {
49
- "schemaId": {
50
- "equals": self.oli.oli_label_pool_schema
51
- }
52
- },
53
- "orderBy": [
54
- {
55
- "timeCreated": "desc"
56
- }
57
- ]
58
- }
59
-
60
- # Add address to where clause if not None
61
- if address is not None:
62
- variables["where"]["recipient"] = {"equals": address}
63
-
64
- # Add attester to where clause if not None
65
- if attester is not None:
66
- variables["where"]["attester"] = {"equals": attester}
67
-
68
- # Add timeCreated to where clause if not None, ensuring it's an int
69
- if timeCreated is not None:
70
- timeCreated = int(timeCreated)
71
- variables["where"]["timeCreated"] = {"gt": timeCreated}
72
-
73
- # Add revocationTime to where clause if not None, ensuring it's an int
74
- if revocationTime is not None:
75
- revocationTime = int(revocationTime)
76
- variables["where"]["revocationTime"] = {"gte": revocationTime}
77
-
78
- headers = {
79
- "Content-Type": "application/json"
80
- }
81
-
82
- response = requests.post(self.oli.graphql, json={"query": query, "variables": variables}, headers=headers)
83
-
84
- if response.status_code == 200:
85
- return response.json()
86
- else:
87
- raise Exception(f"GraphQL query failed with status code {response.status_code}: {response.text}")
File without changes
File without changes
File without changes