unitysvc-services 0.3.0__py3-none-any.whl → 0.3.1__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.
@@ -18,7 +18,11 @@ from .validator import DataValidator
18
18
 
19
19
 
20
20
  class ServiceDataPublisher:
21
- """Publishes service data to UnitySVC backend endpoints."""
21
+ """Publishes service data to UnitySVC backend endpoints.
22
+
23
+ Uses httpx by default, with automatic fallback to curl for systems
24
+ with network restrictions (e.g., macOS with conda Python).
25
+ """
22
26
 
23
27
  def __init__(self) -> None:
24
28
  self.base_url = os.environ.get("UNITYSVC_BASE_URL")
@@ -30,6 +34,7 @@ class ServiceDataPublisher:
30
34
  raise ValueError("UNITYSVC_API_KEY environment variable not set")
31
35
 
32
36
  self.base_url = self.base_url.rstrip("/")
37
+ self.use_curl_fallback = False
33
38
  self.async_client = httpx.AsyncClient(
34
39
  headers={
35
40
  "X-API-Key": self.api_key,
@@ -97,25 +102,128 @@ class ServiceDataPublisher:
97
102
 
98
103
  return result
99
104
 
100
- async def _poll_task_status(
101
- self,
102
- task_id: str,
103
- entity_type: str,
104
- entity_name: str,
105
- context_info: str = "",
106
- poll_interval: float = 2.0,
107
- timeout: float = 300.0,
108
- ) -> dict[str, Any]:
105
+ async def _make_request_curl_async(
106
+ self, endpoint: str, method: str = "GET", data: dict[str, Any] | None = None
107
+ ) -> tuple[dict[str, Any], int]:
108
+ """Make HTTP request using curl (async version).
109
+
110
+ Args:
111
+ endpoint: API endpoint path
112
+ method: HTTP method (GET or POST)
113
+ data: JSON data for POST requests
114
+
115
+ Returns:
116
+ Tuple of (JSON response, HTTP status code)
117
+
118
+ Raises:
119
+ RuntimeError: If curl command fails
109
120
  """
110
- Poll task status until completion or timeout.
121
+ url = f"{self.base_url}{endpoint}"
122
+
123
+ cmd = [
124
+ "curl",
125
+ "-s", # Silent mode
126
+ "-w",
127
+ "\n%{http_code}", # Write HTTP status code on new line
128
+ "-X",
129
+ method,
130
+ "-H",
131
+ f"X-API-Key: {self.api_key}",
132
+ "-H",
133
+ "Content-Type: application/json",
134
+ ]
135
+
136
+ if data:
137
+ cmd.extend(["-d", json.dumps(data)])
138
+
139
+ cmd.append(url)
140
+
141
+ try:
142
+ proc = await asyncio.create_subprocess_exec(
143
+ *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
144
+ )
145
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
146
+
147
+ if proc.returncode != 0:
148
+ error_msg = stderr.decode().strip() if stderr else "Unknown error"
149
+ raise RuntimeError(f"curl command failed: {error_msg}")
150
+
151
+ output = stdout.decode().strip()
152
+ lines = output.split("\n")
153
+ status_code = int(lines[-1])
154
+ body = "\n".join(lines[:-1])
155
+
156
+ response_data = json.loads(body) if body else {}
157
+ return (response_data, status_code)
158
+
159
+ except TimeoutError:
160
+ raise RuntimeError("Request timed out after 30 seconds")
161
+ except json.JSONDecodeError as e:
162
+ raise RuntimeError(f"Invalid JSON response: {e}")
163
+
164
+ async def post(self, endpoint: str, data: dict[str, Any], check_status: bool = True) -> tuple[dict[str, Any], int]:
165
+ """Make a POST request to the backend API with automatic curl fallback.
166
+
167
+ Public utility method for making POST requests. Tries httpx first for performance,
168
+ automatically falls back to curl if network restrictions are detected (e.g., macOS
169
+ with conda Python).
170
+
171
+ Can be used by other packages that need the same fallback behavior.
111
172
 
112
173
  Args:
113
- task_id: Celery task ID
114
- entity_type: Type of entity being published (for error messages)
115
- entity_name: Name of the entity being published (for error messages)
116
- context_info: Additional context for error messages
117
- poll_interval: Seconds between status checks
118
- timeout: Maximum seconds to wait
174
+ endpoint: API endpoint path (e.g., "/publish/seller")
175
+ data: JSON data to post
176
+ check_status: Whether to raise on non-2xx status codes (default: True)
177
+
178
+ Returns:
179
+ Tuple of (JSON response, HTTP status code)
180
+
181
+ Raises:
182
+ RuntimeError: If both httpx and curl fail
183
+ """
184
+
185
+ # Helper class to simulate httpx response for curl
186
+ class CurlResponse:
187
+ def __init__(self, json_data: dict[str, Any], status: int):
188
+ self._json_data = json_data
189
+ self.status_code = status
190
+ self.is_success = 200 <= status < 300
191
+ self.text = json.dumps(json_data)
192
+
193
+ def json(self) -> dict[str, Any]:
194
+ return self._json_data
195
+
196
+ def raise_for_status(self) -> None:
197
+ if not self.is_success:
198
+ raise RuntimeError(f"HTTP {self.status_code}: {self.text}")
199
+
200
+ # If we already know curl is needed, use it directly
201
+ if self.use_curl_fallback:
202
+ response_data, status_code = await self._make_request_curl_async(endpoint, method="POST", data=data)
203
+ response = CurlResponse(response_data, status_code)
204
+ else:
205
+ try:
206
+ response = await self.async_client.post(f"{self.base_url}{endpoint}", json=data)
207
+ except (httpx.ConnectError, OSError):
208
+ # Connection failed - switch to curl fallback and retry
209
+ self.use_curl_fallback = True
210
+ response_data, status_code = await self._make_request_curl_async(endpoint, method="POST", data=data)
211
+ response = CurlResponse(response_data, status_code)
212
+
213
+ if check_status:
214
+ response.raise_for_status()
215
+
216
+ return (response.json(), response.status_code)
217
+
218
+ async def check_task(self, task_id: str, poll_interval: float = 2.0, timeout: float = 300.0) -> dict[str, Any]:
219
+ """Check and wait for task completion.
220
+
221
+ Utility function to poll a Celery task until it completes or times out.
222
+
223
+ Args:
224
+ task_id: Celery task ID to poll
225
+ poll_interval: Seconds between status checks (default: 2.0)
226
+ timeout: Maximum seconds to wait (default: 300.0)
119
227
 
120
228
  Returns:
121
229
  Task result dictionary
@@ -130,15 +238,22 @@ class ServiceDataPublisher:
130
238
  while True:
131
239
  elapsed = time.time() - start_time
132
240
  if elapsed > timeout:
133
- context_msg = f" ({context_info})" if context_info else ""
134
- raise ValueError(f"Task timed out after {timeout}s for {entity_type} '{entity_name}'{context_msg}")
241
+ raise ValueError(f"Task {task_id} timed out after {timeout}s")
135
242
 
136
- # Check task status
243
+ # Check task status using the get() equivalent for async
137
244
  try:
138
- response = await self.async_client.get(f"{self.base_url}/tasks/{task_id}")
139
- response.raise_for_status()
140
- status = response.json()
141
- except (httpx.HTTPError, httpx.NetworkError, httpx.TimeoutException):
245
+ if self.use_curl_fallback:
246
+ status, _ = await self._make_request_curl_async(f"/tasks/{task_id}", method="GET")
247
+ else:
248
+ try:
249
+ response = await self.async_client.get(f"{self.base_url}/tasks/{task_id}")
250
+ response.raise_for_status()
251
+ status = response.json()
252
+ except (httpx.ConnectError, OSError):
253
+ # Connection failed - switch to curl fallback
254
+ self.use_curl_fallback = True
255
+ status, _ = await self._make_request_curl_async(f"/tasks/{task_id}", method="GET")
256
+ except (httpx.HTTPError, httpx.NetworkError, httpx.TimeoutException, RuntimeError):
142
257
  # Network error while checking status - retry
143
258
  await asyncio.sleep(poll_interval)
144
259
  continue
@@ -147,13 +262,10 @@ class ServiceDataPublisher:
147
262
 
148
263
  # Check if task is complete
149
264
  if status.get("status") == "completed" or state == "SUCCESS":
150
- # Task succeeded
151
265
  return status.get("result", {})
152
266
  elif status.get("status") == "failed" or state == "FAILURE":
153
- # Task failed
154
267
  error = status.get("error", "Unknown error")
155
- context_msg = f" ({context_info})" if context_info else ""
156
- raise ValueError(f"Task failed for {entity_type} '{entity_name}'{context_msg}: {error}")
268
+ raise ValueError(f"Task {task_id} failed: {error}")
157
269
 
158
270
  # Still processing - wait and retry
159
271
  await asyncio.sleep(poll_interval)
@@ -193,70 +305,57 @@ class ServiceDataPublisher:
193
305
  last_exception = None
194
306
  for attempt in range(max_retries):
195
307
  try:
196
- response = await self.async_client.post(
197
- f"{self.base_url}{endpoint}",
198
- json=data,
199
- )
308
+ # Use the public post() method with automatic curl fallback
309
+ response_json, status_code = await self.post(endpoint, data, check_status=False)
200
310
 
201
311
  # Handle task-based response (HTTP 202)
202
- if response.status_code == 202:
312
+ if status_code == 202:
203
313
  # Backend returns task_id - poll for completion
204
- response_data = response.json()
205
- task_id = response_data.get("task_id")
314
+ task_id = response_json.get("task_id")
206
315
 
207
316
  if not task_id:
208
317
  context_msg = f" ({context_info})" if context_info else ""
209
318
  raise ValueError(f"No task_id in response for {entity_type} '{entity_name}'{context_msg}")
210
319
 
211
- # Poll task status until completion
212
- result = await self._poll_task_status(
213
- task_id=task_id,
214
- entity_type=entity_type,
215
- entity_name=entity_name,
216
- context_info=context_info,
217
- )
218
- return result
320
+ # Poll task status until completion using check_task utility
321
+ try:
322
+ result = await self.check_task(task_id)
323
+ return result
324
+ except ValueError as e:
325
+ # Add context to task errors
326
+ context_msg = f" ({context_info})" if context_info else ""
327
+ raise ValueError(f"Task failed for {entity_type} '{entity_name}'{context_msg}: {e}")
219
328
 
220
- # Provide detailed error information if request fails
221
- if not response.is_success:
329
+ # Check for errors
330
+ if status_code >= 400:
222
331
  # Don't retry on 4xx errors (client errors) - they won't succeed on retry
223
- if 400 <= response.status_code < 500:
224
- error_detail = "Unknown error"
225
- try:
226
- error_json = response.json()
227
- error_detail = error_json.get("detail", str(error_json))
228
- except Exception:
229
- error_detail = response.text or f"HTTP {response.status_code}"
230
-
332
+ if 400 <= status_code < 500:
333
+ error_detail = response_json.get("detail", str(response_json))
231
334
  context_msg = f" ({context_info})" if context_info else ""
232
335
  raise ValueError(
233
336
  f"Failed to publish {entity_type} '{entity_name}'{context_msg}: {error_detail}"
234
337
  )
235
338
 
236
- # 5xx errors or network errors - retry with exponential backoff
339
+ # 5xx errors - retry with exponential backoff
237
340
  if attempt < max_retries - 1:
238
341
  wait_time = 2**attempt # Exponential backoff: 1s, 2s, 4s
239
342
  await asyncio.sleep(wait_time)
240
343
  continue
241
344
  else:
242
345
  # Last attempt failed
243
- error_detail = "Unknown error"
244
- try:
245
- error_json = response.json()
246
- error_detail = error_json.get("detail", str(error_json))
247
- except Exception:
248
- error_detail = response.text or f"HTTP {response.status_code}"
249
-
346
+ error_detail = response_json.get("detail", str(response_json))
250
347
  context_msg = f" ({context_info})" if context_info else ""
251
348
  raise ValueError(
252
349
  f"Failed to publish {entity_type} after {max_retries} attempts: "
253
350
  f"'{entity_name}'{context_msg}: {error_detail}"
254
351
  )
255
352
 
256
- # For non-202 success responses, return the body
257
- return response.json()
353
+ # Success response (2xx)
354
+ return response_json
258
355
 
259
- except (httpx.NetworkError, httpx.TimeoutException) as e:
356
+ except (httpx.NetworkError, httpx.TimeoutException, RuntimeError) as e:
357
+ # Network/connection errors - the post() method should have tried curl fallback
358
+ # If we're here, both httpx and curl failed
260
359
  last_exception = e
261
360
  if attempt < max_retries - 1:
262
361
  wait_time = 2**attempt # Exponential backoff: 1s, 2s, 4s
@@ -2,7 +2,9 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import subprocess
5
6
  from typing import Any
7
+ from urllib.parse import urlencode
6
8
 
7
9
  import httpx
8
10
  import typer
@@ -14,7 +16,11 @@ console = Console()
14
16
 
15
17
 
16
18
  class ServiceDataQuery:
17
- """Query service data from UnitySVC backend endpoints."""
19
+ """Query service data from UnitySVC backend endpoints.
20
+
21
+ Uses httpx by default, with automatic fallback to curl for systems
22
+ with network restrictions (e.g., macOS with conda Python).
23
+ """
18
24
 
19
25
  def __init__(self) -> None:
20
26
  """Initialize query client from environment variables.
@@ -31,6 +37,7 @@ class ServiceDataQuery:
31
37
  raise ValueError("UNITYSVC_API_KEY environment variable not set")
32
38
 
33
39
  self.base_url = self.base_url.rstrip("/")
40
+ self.use_curl_fallback = False
34
41
  self.client = httpx.Client(
35
42
  headers={
36
43
  "X-API-Key": self.api_key,
@@ -39,6 +46,80 @@ class ServiceDataQuery:
39
46
  timeout=30.0,
40
47
  )
41
48
 
49
+ def _make_request_curl(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
50
+ """Make HTTP GET request using curl fallback.
51
+
52
+ Args:
53
+ endpoint: API endpoint path (e.g., "/publish/sellers")
54
+ params: Query parameters
55
+
56
+ Returns:
57
+ JSON response as dictionary
58
+
59
+ Raises:
60
+ RuntimeError: If curl command fails or returns non-200 status
61
+ """
62
+ url = f"{self.base_url}{endpoint}"
63
+ if params:
64
+ url = f"{url}?{urlencode(params)}"
65
+
66
+ cmd = [
67
+ "curl",
68
+ "-s", # Silent mode
69
+ "-f", # Fail on HTTP errors
70
+ "-H",
71
+ f"X-API-Key: {self.api_key}",
72
+ "-H",
73
+ "Accept: application/json",
74
+ url,
75
+ ]
76
+
77
+ try:
78
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=30)
79
+ return json.loads(result.stdout)
80
+ except subprocess.CalledProcessError as e:
81
+ error_msg = e.stderr.strip() if e.stderr else "Unknown error"
82
+ raise RuntimeError(f"HTTP request failed: {error_msg}")
83
+ except subprocess.TimeoutExpired:
84
+ raise RuntimeError("Request timed out after 30 seconds")
85
+ except json.JSONDecodeError as e:
86
+ raise RuntimeError(f"Invalid JSON response: {e}")
87
+
88
+ def get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
89
+ """Make a GET request to the backend API with automatic curl fallback.
90
+
91
+ Public utility method for making GET requests. Tries httpx first for performance,
92
+ automatically falls back to curl if network restrictions are detected (e.g., macOS
93
+ with conda Python).
94
+
95
+ Can be used by subclasses and other packages (e.g., unitysvc_admin) that need
96
+ the same fallback behavior.
97
+
98
+ Args:
99
+ endpoint: API endpoint path (e.g., "/publish/sellers", "/admin/documents")
100
+ params: Query parameters
101
+
102
+ Returns:
103
+ JSON response as dictionary
104
+
105
+ Raises:
106
+ RuntimeError: If both httpx and curl fail
107
+ """
108
+ # If we already know curl is needed, use it directly
109
+ if self.use_curl_fallback:
110
+ return self._make_request_curl(endpoint, params)
111
+
112
+ # Try httpx first
113
+ try:
114
+ response = self.client.get(f"{self.base_url}{endpoint}", params=params)
115
+ response.raise_for_status()
116
+ return response.json()
117
+ except (httpx.ConnectError, OSError) as e:
118
+ # Connection failed - likely network restrictions
119
+ # Fall back to curl and remember this for future requests
120
+ self.use_curl_fallback = True
121
+ return self._make_request_curl(endpoint, params)
122
+
42
123
  def list_service_offerings(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
43
124
  """List all service offerings from the backend.
44
125
 
@@ -46,9 +127,7 @@ class ServiceDataQuery:
46
127
  skip: Number of records to skip (for pagination)
47
128
  limit: Maximum number of records to return
48
129
  """
49
- response = self.client.get(f"{self.base_url}/publish/offerings", params={"skip": skip, "limit": limit})
50
- response.raise_for_status()
51
- result = response.json()
130
+ result = self.get("/publish/offerings", {"skip": skip, "limit": limit})
52
131
  return result.get("data", result) if isinstance(result, dict) else result
53
132
 
54
133
  def list_service_listings(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
@@ -58,9 +137,7 @@ class ServiceDataQuery:
58
137
  skip: Number of records to skip (for pagination)
59
138
  limit: Maximum number of records to return
60
139
  """
61
- response = self.client.get(f"{self.base_url}/publish/listings", params={"skip": skip, "limit": limit})
62
- response.raise_for_status()
63
- result = response.json()
140
+ result = self.get("/publish/listings", {"skip": skip, "limit": limit})
64
141
  return result.get("data", result) if isinstance(result, dict) else result
65
142
 
66
143
  def list_providers(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
@@ -70,9 +147,7 @@ class ServiceDataQuery:
70
147
  skip: Number of records to skip (for pagination)
71
148
  limit: Maximum number of records to return
72
149
  """
73
- response = self.client.get(f"{self.base_url}/publish/providers", params={"skip": skip, "limit": limit})
74
- response.raise_for_status()
75
- result = response.json()
150
+ result = self.get("/publish/providers", {"skip": skip, "limit": limit})
76
151
  return result.get("data", result) if isinstance(result, dict) else result
77
152
 
78
153
  def list_sellers(self, skip: int = 0, limit: int = 100) -> list[dict[str, Any]]:
@@ -82,14 +157,12 @@ class ServiceDataQuery:
82
157
  skip: Number of records to skip (for pagination)
83
158
  limit: Maximum number of records to return
84
159
  """
85
- response = self.client.get(f"{self.base_url}/publish/sellers", params={"skip": skip, "limit": limit})
86
- response.raise_for_status()
87
- result = response.json()
160
+ result = self.get("/publish/sellers", {"skip": skip, "limit": limit})
88
161
  return result.get("data", result) if isinstance(result, dict) else result
89
162
 
90
163
  def close(self):
91
- """Close the HTTP client."""
92
- self.client.close()
164
+ """Close the HTTP client (no-op for curl-based implementation)."""
165
+ pass
93
166
 
94
167
  def __enter__(self):
95
168
  """Context manager entry."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-services
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: SDK for digital service providers on UnitySVC
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -3,9 +3,9 @@ unitysvc_services/cli.py,sha256=OK0IZyAckxP15jRWU_W49hl3t7XcNRtd8BoDMyRKqNM,682
3
3
  unitysvc_services/format_data.py,sha256=Jl9Vj3fRX852fHSUa5DzO-oiFQwuQHC3WMCDNIlo1Lc,5460
4
4
  unitysvc_services/list.py,sha256=QDp9BByaoeFeJxXJN9RQ-jU99mH9Guq9ampfXCbpZmI,7033
5
5
  unitysvc_services/populate.py,sha256=zkcjIy8BWuQSO7JwiRNHKgGoxQvc3ujluUQdYixdBvY,6626
6
- unitysvc_services/publisher.py,sha256=sNqGbLQ3QulNCGzyRjBg6ks-I2nTVgip4vGFV4XCUto,52285
6
+ unitysvc_services/publisher.py,sha256=Gt1b3O_ePql-GUt6u6djd9d24FBW6RXaUvv7veqm7N4,56343
7
7
  unitysvc_services/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- unitysvc_services/query.py,sha256=x2VUnfva21-mVd-JgtChajNBgXG1AQJ6c3umCw2FNWU,24089
8
+ unitysvc_services/query.py,sha256=B_AjNKZyOX23qd5zUtF92Qj1gQh5iYkXG9fLB5BkY2s,26734
9
9
  unitysvc_services/scaffold.py,sha256=Y73IX8vskImxSvxDgR0mvEFuAMYnBKfttn3bjcz3jmQ,40331
10
10
  unitysvc_services/update.py,sha256=K9swocTUnqqiSgARo6GmuzTzUySSpyqqPPW4xF7ZU-g,9659
11
11
  unitysvc_services/utils.py,sha256=GN0gkVTU8fOx2G0EbqnWmx8w9eFsoPfRprPjwCyPYkE,11371
@@ -16,9 +16,9 @@ unitysvc_services/models/listing_v1.py,sha256=PPb9hIdWQp80AWKLxFXYBDcWXzNcDrO4v6
16
16
  unitysvc_services/models/provider_v1.py,sha256=76EK1i0hVtdx_awb00-ZMtSj4Oc9Zp4xZ-DeXmG3iTY,2701
17
17
  unitysvc_services/models/seller_v1.py,sha256=oll2ZZBPBDX8wslHrbsCKf_jIqHNte2VEj5RJ9bawR4,3520
18
18
  unitysvc_services/models/service_v1.py,sha256=Xpk-K-95M1LRqYM8nNJcll8t-lsW9Xdi2_bVbYNs8-M,3019
19
- unitysvc_services-0.3.0.dist-info/licenses/LICENSE,sha256=_p8V6A8OMPu2HIztn3O01v0-urZFwk0Dd3Yk_PTIlL8,1065
20
- unitysvc_services-0.3.0.dist-info/METADATA,sha256=QPZzgXqlKCPdsJCuWnbxxcXe7sfk-SBTxCJbq2npEc8,6628
21
- unitysvc_services-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- unitysvc_services-0.3.0.dist-info/entry_points.txt,sha256=-vodnbPmo7QQmFu8jdG6sCyGRVM727w9Nhwp4Vwau_k,64
23
- unitysvc_services-0.3.0.dist-info/top_level.txt,sha256=GIotQj-Ro2ruR7eupM1r58PWqIHTAq647ORL7E2kneo,18
24
- unitysvc_services-0.3.0.dist-info/RECORD,,
19
+ unitysvc_services-0.3.1.dist-info/licenses/LICENSE,sha256=_p8V6A8OMPu2HIztn3O01v0-urZFwk0Dd3Yk_PTIlL8,1065
20
+ unitysvc_services-0.3.1.dist-info/METADATA,sha256=rq18iCpS6qmynawugmEWJIVf2RNlxXmDWYdy2hGS3ZU,6628
21
+ unitysvc_services-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ unitysvc_services-0.3.1.dist-info/entry_points.txt,sha256=-vodnbPmo7QQmFu8jdG6sCyGRVM727w9Nhwp4Vwau_k,64
23
+ unitysvc_services-0.3.1.dist-info/top_level.txt,sha256=GIotQj-Ro2ruR7eupM1r58PWqIHTAq647ORL7E2kneo,18
24
+ unitysvc_services-0.3.1.dist-info/RECORD,,