rapidata 1.6.0__py3-none-any.whl → 1.6.2__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 rapidata might be problematic. Click here for more details.

@@ -27,10 +27,10 @@ class RapidataClient:
27
27
 
28
28
  def __init__(
29
29
  self,
30
- client_id: str,
31
- client_secret: str,
30
+ client_id: str | None = None,
31
+ client_secret: str | None = None,
32
32
  endpoint: str = "https://api.rapidata.ai",
33
- token_url: str = "https://auth.rapidata.ai/connect/token",
33
+ token_url: str = "https://auth.rapidata.ai",
34
34
  oauth_scope: str = "openid",
35
35
  cert_path: str | None = None,
36
36
  ):
@@ -0,0 +1,232 @@
1
+ import json
2
+ import os
3
+ import time
4
+ import webbrowser
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from socket import gethostname
8
+ from typing import Dict, List, Optional, Tuple
9
+
10
+ import requests
11
+ from pydantic import BaseModel
12
+
13
+
14
+ class ClientCredential(BaseModel):
15
+ display_name: str
16
+ client_id: str
17
+ client_secret: str
18
+ endpoint: str
19
+ created_at: datetime
20
+ last_used: datetime
21
+
22
+ @classmethod
23
+ def from_dict(cls, data: Dict):
24
+ return cls(
25
+ display_name=data["display_name"],
26
+ client_id=data["client_id"],
27
+ client_secret=data["client_secret"],
28
+ endpoint=data["endpoint"],
29
+ created_at=datetime.fromisoformat(data["created_at"]),
30
+ last_used=datetime.fromisoformat(data["last_used"]),
31
+ )
32
+
33
+ def to_dict(self) -> Dict:
34
+ return {
35
+ "display_name": self.display_name,
36
+ "client_id": self.client_id,
37
+ "client_secret": self.client_secret,
38
+ "endpoint": self.endpoint,
39
+ "created_at": self.created_at.isoformat(),
40
+ "last_used": self.last_used.isoformat(),
41
+ }
42
+
43
+ def get_display_string(self):
44
+ return f"{self.display_name} - Client ID: {self.client_id} (Created: {self.created_at.strftime('%Y-%m-%d %H:%M:%S')})"
45
+
46
+
47
+ class BridgeToken(BaseModel):
48
+ read_key: str
49
+ write_key: str
50
+
51
+
52
+ class CredentialManager:
53
+ def __init__(
54
+ self,
55
+ endpoint: str,
56
+ cert_path: str | None = None,
57
+ poll_timeout: int = 60,
58
+ poll_interval: int = 1,
59
+ ):
60
+ self.endpoint = endpoint
61
+ self.cert_path = cert_path
62
+ self.poll_timeout = poll_timeout
63
+ self.poll_interval = poll_interval
64
+
65
+ self.config_dir = Path.home() / ".config" / "rapidata"
66
+ self.config_path = self.config_dir / "credentials.json"
67
+
68
+ # Ensure config directory exists
69
+ self.config_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ def _read_credentials(self) -> Dict[str, List[ClientCredential]]:
72
+ """Read all stored credentials from the config file."""
73
+ if not self.config_path.exists():
74
+ return {}
75
+
76
+ try:
77
+ with open(self.config_path, "r") as f:
78
+ data = json.load(f)
79
+ return {
80
+ env: [ClientCredential.from_dict(cred) for cred in creds]
81
+ for env, creds in data.items()
82
+ }
83
+ except json.JSONDecodeError:
84
+ return {}
85
+
86
+ def _write_credentials(
87
+ self, credentials: Dict[str, List[ClientCredential]]
88
+ ) -> None:
89
+ data = {
90
+ env: [cred.to_dict() for cred in creds]
91
+ for env, creds in credentials.items()
92
+ }
93
+
94
+ with open(self.config_path, "w") as f:
95
+ json.dump(data, f, indent=2)
96
+
97
+ # Ensure file is only readable by the user
98
+ os.chmod(self.config_path, 0o600)
99
+
100
+ def _store_credential(self, credential: ClientCredential) -> None:
101
+ credentials = self._read_credentials()
102
+
103
+ if credential.endpoint not in credentials:
104
+ credentials[credential.endpoint] = []
105
+
106
+ credentials[credential.endpoint].append(credential)
107
+ self._write_credentials(credentials)
108
+
109
+ @staticmethod
110
+ def _select_credential(
111
+ credentials: List[ClientCredential],
112
+ ) -> Optional[ClientCredential]:
113
+ if not credentials:
114
+ return None
115
+
116
+ if len(credentials) == 1:
117
+ return credentials[0]
118
+
119
+ return max(credentials, key=lambda c: c.last_used)
120
+
121
+ def get_client_credentials(self) -> Optional[ClientCredential]:
122
+ """Gets stored client credentials or create new ones via browser auth."""
123
+ credentials = self._read_credentials()
124
+ env_credentials = credentials.get(self.endpoint, [])
125
+
126
+ if env_credentials:
127
+ credential = self._select_credential(env_credentials)
128
+ if credential:
129
+ credential.last_used = datetime.now(timezone.utc)
130
+ self._write_credentials(credentials)
131
+ return credential
132
+
133
+ return self._create_new_credentials()
134
+
135
+ def _get_bridge_tokens(self) -> Optional[BridgeToken]:
136
+ """Get bridge tokens from the identity endpoint."""
137
+ try:
138
+ bridge_endpoint = (
139
+ f"{self.endpoint}/Identity/CreateBridgeToken?clientId=rapidata-cli"
140
+ )
141
+ response = requests.post(bridge_endpoint, verify=self.cert_path)
142
+ if not response.ok:
143
+ print(f"Failed to get bridge tokens: {response.status_code}")
144
+ return None
145
+
146
+ data = response.json()
147
+ return BridgeToken(read_key=data["readKey"], write_key=data["writeKey"])
148
+ except requests.RequestException as e:
149
+ print(f"Failed to get bridge tokens: {e}")
150
+ return None
151
+
152
+ def _poll_read_key(self, read_key: str) -> Optional[str]:
153
+ """Poll the read key endpoint until we get an access token."""
154
+ read_endpoint = f"{self.endpoint}/Identity/ReadBridgeToken"
155
+ start_time = time.time()
156
+
157
+ while time.time() - start_time < self.poll_timeout:
158
+ try:
159
+ response = requests.get(
160
+ read_endpoint, params={"readKey": read_key}, verify=self.cert_path
161
+ )
162
+
163
+ if response.status_code == 200:
164
+ return response.json().get("accessToken")
165
+ elif response.status_code == 202:
166
+ # Still processing
167
+ time.sleep(self.poll_interval)
168
+ continue
169
+ else:
170
+ # Error occurred
171
+ print(f"Error polling read key: {response.status_code}")
172
+ return None
173
+
174
+ except requests.RequestException as e:
175
+ print(f"Error polling read key: {e}")
176
+ return None
177
+
178
+ print("Polling timed out")
179
+ return None
180
+
181
+ def _create_client(self, access_token: str) -> Optional[Tuple[str, str, str]]:
182
+ """Create a new client using the access token."""
183
+ try:
184
+ # set the display name to the hostname
185
+ display_name = f"{gethostname()} - CLI"
186
+ response = requests.post(
187
+ f"{self.endpoint}/Client",
188
+ headers={
189
+ "Authorization": f"Bearer {access_token}",
190
+ "Content-Type": "application/json",
191
+ "Accept": "*/*",
192
+ },
193
+ json={"displayName": display_name},
194
+ verify=self.cert_path,
195
+ )
196
+ response.raise_for_status()
197
+ data = response.json()
198
+ return data.get("clientId"), data.get("clientSecret"), display_name
199
+ except requests.RequestException as e:
200
+ print(f"Failed to create client: {e}")
201
+ return None
202
+
203
+ def _create_new_credentials(self) -> Optional[ClientCredential]:
204
+ bridge_endpoint = self._get_bridge_tokens()
205
+ if not bridge_endpoint:
206
+ return None
207
+
208
+ auth_url = f"{self.endpoint}/connect/authorize/external?clientId=rapidata-cli&scope=openid profile email&writeKey={bridge_endpoint.write_key}"
209
+ webbrowser.open(auth_url)
210
+
211
+ access_token = self._poll_read_key(bridge_endpoint.read_key)
212
+ if not access_token:
213
+ return None
214
+
215
+ client_state = self._create_client(access_token)
216
+
217
+ if not client_state:
218
+ raise ValueError("Failed to create client")
219
+
220
+ client_id, client_secret, display_name = client_state
221
+
222
+ credential = ClientCredential(
223
+ client_id=client_id,
224
+ client_secret=client_secret,
225
+ display_name=display_name,
226
+ endpoint=self.endpoint,
227
+ created_at=datetime.now(timezone.utc),
228
+ last_used=datetime.now(timezone.utc),
229
+ )
230
+
231
+ self._store_credential(credential)
232
+ return credential
@@ -1,7 +1,3 @@
1
- import json
2
- import time
3
- import requests
4
- import threading
5
1
  from rapidata.api_client.api.campaign_api import CampaignApi
6
2
  from rapidata.api_client.api.dataset_api import DatasetApi
7
3
  from rapidata.api_client.api.order_api import OrderApi
@@ -11,61 +7,44 @@ from rapidata.api_client.api.validation_api import ValidationApi
11
7
  from rapidata.api_client.api.workflow_api import WorkflowApi
12
8
  from rapidata.api_client.api_client import ApiClient
13
9
  from rapidata.api_client.configuration import Configuration
10
+ from rapidata.service.token_manager import TokenManager, TokenInfo
14
11
 
15
12
 
16
13
  class OpenAPIService:
17
-
18
- _TOKEN_EXPIRATION_MINUTES = 30
19
-
20
14
  def __init__(
21
15
  self,
22
- client_id: str,
23
- client_secret: str,
16
+ client_id: str | None,
17
+ client_secret: str | None,
24
18
  endpoint: str,
25
19
  token_url: str,
26
20
  oauth_scope: str,
27
- cert_path: str | None = None
21
+ cert_path: str | None = None,
28
22
  ):
23
+ token_manager = TokenManager(
24
+ client_id=client_id,
25
+ client_secret=client_secret,
26
+ endpoint=token_url,
27
+ oauth_scope=oauth_scope,
28
+ cert_path=cert_path
29
+ )
29
30
  client_configuration = Configuration(host=endpoint, ssl_ca_cert=cert_path)
30
31
  self.api_client = ApiClient(configuration=client_configuration)
31
32
 
32
33
  self._client_id = client_id
33
34
  self._client_secret = client_secret
34
35
  self._oauth_scope = oauth_scope
35
- self._token_url = token_url
36
+ self._token_url = f"{token_url}/connect/token"
36
37
  self._cert_path = cert_path
37
38
 
38
- self._api_client = ApiClient()
39
- self._order_api = OrderApi(self.api_client)
40
- self._dataset_api = DatasetApi(self.api_client)
41
-
42
- api_token = self.__fetch_token(
43
- self._client_id, self._client_secret, self._oauth_scope, self._token_url, self._cert_path
44
- )
45
- self.api_client.configuration.api_key["bearer"] = f"Bearer {api_token}"
46
-
47
- refresh_thread = threading.Thread(
48
- target=lambda: self.__refresh_token_periodically(self._TOKEN_EXPIRATION_MINUTES - 1)
49
- )
50
- refresh_thread.daemon = True
51
- refresh_thread.start()
52
-
53
- def __refresh_token_periodically(self, refresh_interval):
54
- while True:
55
- new_token = self.__fetch_token(
56
- self._client_id, self._client_secret, self._oauth_scope, self._token_url, self._cert_path
57
- )
58
- self.api_client.configuration.api_key["bearer"] = f"Bearer {new_token}"
59
-
60
- time.sleep(refresh_interval)
39
+ token_manager.start_token_refresh(token_callback=self._set_token)
61
40
 
62
41
  @property
63
42
  def order_api(self) -> OrderApi:
64
- return self._order_api
43
+ return OrderApi(self.api_client)
65
44
 
66
45
  @property
67
46
  def dataset_api(self) -> DatasetApi:
68
- return self._dataset_api
47
+ return DatasetApi(self.api_client)
69
48
 
70
49
  @property
71
50
  def validation_api(self) -> ValidationApi:
@@ -87,23 +66,5 @@ class OpenAPIService:
87
66
  def workflow_api(self) -> WorkflowApi:
88
67
  return WorkflowApi(self.api_client)
89
68
 
90
- @staticmethod
91
- def __fetch_token(client_id: str, client_secret: str, scope: str, token_url: str, cert_path: str | None = None) -> str:
92
- try:
93
- return requests.post(
94
- token_url,
95
- data={
96
- 'grant_type': 'client_credentials',
97
- 'client_id': client_id,
98
- 'client_secret': client_secret,
99
- 'scope': scope,
100
- },
101
- verify=cert_path
102
- ).json()['access_token']
103
- except requests.RequestException as e:
104
- raise Exception(f"Failed to fetch token: {e}")
105
- except json.JSONDecodeError as e:
106
- raise Exception(f"Failed to parse token response: {e}")
107
- except KeyError as e:
108
- raise Exception(f"Failed to extract token from response: {e}")
109
-
69
+ def _set_token(self, token: TokenInfo):
70
+ self.api_client.configuration.api_key["bearer"] = f"Bearer {token.access_token}"
@@ -0,0 +1,175 @@
1
+ import json
2
+ import logging
3
+ import threading
4
+ from datetime import datetime, timedelta
5
+ from typing import Optional, Callable
6
+
7
+ import requests
8
+ from pydantic import BaseModel
9
+
10
+ from rapidata.service.credential_manager import CredentialManager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class TokenInfo(BaseModel):
16
+ access_token: str
17
+ expires_in: int
18
+ issued_at: datetime
19
+ token_type: str = "Bearer"
20
+
21
+ @property
22
+ def auth_header(self):
23
+ return f"{self.token_type} {self.access_token}"
24
+
25
+ @property
26
+ def time_remaining(self):
27
+ remaining = (
28
+ (self.issued_at + timedelta(seconds=self.expires_in)) - datetime.now()
29
+ ).total_seconds()
30
+ return max(0.0, remaining)
31
+
32
+
33
+ class TokenManager:
34
+ def __init__(
35
+ self,
36
+ client_id: str | None = None,
37
+ client_secret: str | None = None,
38
+ endpoint: str = "https://auth.rapidata.ai",
39
+ oauth_scope: str = "openid profile email",
40
+ cert_path: str | None = None,
41
+ refresh_threshold: float = 0.8,
42
+ max_sleep_time: float = 30,
43
+ ):
44
+ self._client_id = client_id
45
+ self._client_secret = client_secret
46
+
47
+ if not client_id or not client_secret:
48
+ credential_manager = CredentialManager(
49
+ endpoint=endpoint, cert_path=cert_path
50
+ )
51
+ credentials = credential_manager.get_client_credentials()
52
+ if not credentials:
53
+ raise ValueError("Failed to fetch client credentials")
54
+ self._client_id = credentials.client_id
55
+ self._client_secret = credentials.client_secret
56
+
57
+ self._endpoint = endpoint
58
+ self._oauth_scope = oauth_scope
59
+ self._cert_path = cert_path
60
+ self._refresh_threshold = refresh_threshold
61
+ self._max_sleep_time = max_sleep_time
62
+
63
+ self._token_lock = threading.Lock()
64
+ self._current_token: Optional[TokenInfo] = None
65
+ self._refresh_thread: Optional[threading.Thread] = None
66
+ self._should_stop = threading.Event()
67
+
68
+ def fetch_token(self):
69
+ try:
70
+ response = requests.post(
71
+ f"{self._endpoint}/connect/token",
72
+ data={
73
+ "grant_type": "client_credentials",
74
+ "client_id": self._client_id,
75
+ "client_secret": self._client_secret,
76
+ "scope": self._oauth_scope,
77
+ },
78
+ verify=self._cert_path,
79
+ )
80
+
81
+ if response.ok:
82
+ data = response.json()
83
+ return TokenInfo(
84
+ access_token=data["access_token"],
85
+ token_type=data["token_type"],
86
+ expires_in=data["expires_in"],
87
+ issued_at=datetime.now(),
88
+ )
89
+
90
+ else:
91
+ data = response.text
92
+ error_description = "An unknown error occurred"
93
+ if "error_description" in data:
94
+ error_description = (
95
+ data.split("error_description")[1].split("\n")[0].strip()
96
+ )
97
+ raise ValueError(f"Failed to fetch token: {error_description}")
98
+ except requests.RequestException as e:
99
+ raise ValueError(f"Failed to fetch token: {e}")
100
+ except json.JSONDecodeError as e:
101
+ raise ValueError(f"Failed to parse token response: {e}")
102
+ except KeyError as e:
103
+ raise ValueError(f"Failed to extract token from response: {e}")
104
+
105
+ def start_token_refresh(self, token_callback: Callable[[TokenInfo], None]) -> None:
106
+ if self._refresh_thread and self._refresh_thread.is_alive():
107
+ logger.error("Token refresh thread is already running")
108
+ return
109
+
110
+ def refresh_loop():
111
+ while not self._should_stop.is_set():
112
+ try:
113
+ with self._token_lock:
114
+ if self._should_refresh_token(self._current_token):
115
+ logger.debug("Refreshing token")
116
+ self._current_token = self.fetch_token()
117
+ token_callback(self._current_token)
118
+
119
+ if self._current_token:
120
+ time_until_refresh_threshold = (
121
+ self._current_token.time_remaining
122
+ - (
123
+ self._current_token.expires_in
124
+ * (1 - self._refresh_threshold)
125
+ )
126
+ )
127
+ logger.debug("Time until refresh threshold: %s", time_until_refresh_threshold)
128
+ sleep_time = min(
129
+ self._max_sleep_time, time_until_refresh_threshold
130
+ )
131
+ logger.debug(
132
+ f"Sleeping for {sleep_time} until checking the token again"
133
+ )
134
+ self._should_stop.wait(timeout=max(1.0, sleep_time))
135
+ else:
136
+ self._should_stop.wait(timeout=self._max_sleep_time)
137
+ except Exception as e:
138
+ logger.error("Failed to refresh token: %s", e)
139
+ self._should_stop.wait(timeout=5)
140
+
141
+ self._should_stop.clear()
142
+ self._refresh_thread = threading.Thread(target=refresh_loop, daemon=True)
143
+ self._refresh_thread.start()
144
+
145
+ def stop_token_refresh(self):
146
+ self._should_stop.set()
147
+ if self._refresh_thread:
148
+ self._refresh_thread.join(timeout=1)
149
+ self._refresh_thread = None
150
+
151
+ def get_current_token(self) -> Optional[TokenInfo]:
152
+ with self._token_lock:
153
+ return self._current_token
154
+
155
+ def _should_refresh_token(self, token: TokenInfo | None) -> bool:
156
+ if not token:
157
+ return True
158
+
159
+ limit = token.expires_in * (1 - self._refresh_threshold)
160
+
161
+ logger.debug(
162
+ "The token was issued at %s, it expires in %s. It has %s seconds remaining and we refresh the token when it has %s seconds remaining",
163
+ token.issued_at,
164
+ token.expires_in,
165
+ token.time_remaining,
166
+ limit,
167
+ )
168
+ return token.time_remaining < limit
169
+
170
+ def __enter__(self):
171
+ return self
172
+
173
+ def __exit__(self, exc_type, exc_val, exc_tb):
174
+ self.stop_token_refresh()
175
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rapidata
3
- Version: 1.6.0
3
+ Version: 1.6.2
4
4
  Summary: Rapidata package containing the Rapidata Python Client to interact with the Rapidata Web API in an easy way.
5
5
  License: Apache-2.0
6
6
  Author: Rapidata AG
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: deprecated (>=1.2.14,<2.0.0)
15
16
  Requires-Dist: pillow (>=10.4.0,<11.0.0)
16
17
  Requires-Dist: pydantic (>=2.8.2,<3.0.0)
17
18
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
@@ -345,7 +345,7 @@ rapidata/rapidata_client/metadata/transcription_metadata.py,sha256=THtDEVCON4Ulc
345
345
  rapidata/rapidata_client/order/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
346
346
  rapidata/rapidata_client/order/rapidata_order.py,sha256=t5ddz_6Dk2DOpsDQ2EiZHJJB1RzU-etKwBL0RErEsSY,4567
347
347
  rapidata/rapidata_client/order/rapidata_order_builder.py,sha256=b--9byhsiAW1fL0mVSPzGJT0X4MQ-tCC0BNjIm2vu-Q,16406
348
- rapidata/rapidata_client/rapidata_client.py,sha256=DwWi7gtACV_OxQDEBHCILv8921tWQy3UtXzJ0ZcjjTo,8018
348
+ rapidata/rapidata_client/rapidata_client.py,sha256=a_OZBd3sldws3ElqZt_eyqrSzdUNLxynjG5XCNLmmnY,8032
349
349
  rapidata/rapidata_client/referee/__init__.py,sha256=E1VODxTjoQRnxzdgMh3aRlDLouxe1nWuvozEHXD2gq4,150
350
350
  rapidata/rapidata_client/referee/base_referee.py,sha256=bMy7cw0a-pGNbFu6u_1_Jplu0A483Ubj4oDQzh8vu8k,493
351
351
  rapidata/rapidata_client/referee/early_stopping_referee.py,sha256=Dg2Kk7OiLBtS3kknsLxyJIlS27xmPvsikFR6g4xlbTE,1862
@@ -371,9 +371,11 @@ rapidata/rapidata_client/workflow/evaluation_workflow.py,sha256=IBQoVFxOaeCDIBfa
371
371
  rapidata/rapidata_client/workflow/free_text_workflow.py,sha256=VaypoG3yKgsbtVyqxta3W28eDwdnGebCy2xDWPCBMyo,1566
372
372
  rapidata/rapidata_client/workflow/transcription_workflow.py,sha256=_KDtGCdRhauJm3jQHpwhY-Hq79CLg5I8q2RgOz5lo1g,1404
373
373
  rapidata/service/__init__.py,sha256=s9bS1AJZaWIhLtJX_ZA40_CK39rAAkwdAmymTMbeWl4,68
374
+ rapidata/service/credential_manager.py,sha256=qLfDnVaDXBgCSVTERik9-Jv952EXGHTtUz0yWKuKN0E,7923
374
375
  rapidata/service/local_file_service.py,sha256=pgorvlWcx52Uh3cEG6VrdMK_t__7dacQ_5AnfY14BW8,877
375
- rapidata/service/openapi_service.py,sha256=l-ga9TLKWR6Gwx9nbDSQ2J1HZRPhgk_mGgnC3kud3uw,3688
376
- rapidata-1.6.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
377
- rapidata-1.6.0.dist-info/METADATA,sha256=lQFu7Zzja9wuFSOzxsCrZ0dezR2YodP6snRFkjXscEo,1012
378
- rapidata-1.6.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
379
- rapidata-1.6.0.dist-info/RECORD,,
376
+ rapidata/service/openapi_service.py,sha256=ejAIilSjRweGcR4nN4txqZNDuoI-WIJEuagdN2oSd58,2335
377
+ rapidata/service/token_manager.py,sha256=JZ5YbR5Di8dO3H4kK11d0kzWlrXxjgCmeNkHA4AapCM,6425
378
+ rapidata-1.6.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
379
+ rapidata-1.6.2.dist-info/METADATA,sha256=9UB4WjrKGvwJrzNtM2JaA4UkIPGBx5u-jP3yLUAl_48,1056
380
+ rapidata-1.6.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
381
+ rapidata-1.6.2.dist-info/RECORD,,