unitysvc-services 0.1.1__py3-none-any.whl → 0.1.5__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.
- unitysvc_services/api.py +321 -0
- unitysvc_services/cli.py +2 -1
- unitysvc_services/format_data.py +2 -7
- unitysvc_services/list.py +14 -43
- unitysvc_services/models/base.py +169 -102
- unitysvc_services/models/listing_v1.py +25 -9
- unitysvc_services/models/provider_v1.py +19 -8
- unitysvc_services/models/seller_v1.py +10 -8
- unitysvc_services/models/service_v1.py +8 -1
- unitysvc_services/populate.py +20 -6
- unitysvc_services/publisher.py +897 -462
- unitysvc_services/py.typed +0 -0
- unitysvc_services/query.py +577 -384
- unitysvc_services/test.py +769 -0
- unitysvc_services/update.py +4 -13
- unitysvc_services/utils.py +55 -6
- unitysvc_services/validator.py +117 -86
- unitysvc_services-0.1.5.dist-info/METADATA +182 -0
- unitysvc_services-0.1.5.dist-info/RECORD +26 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/entry_points.txt +1 -0
- unitysvc_services-0.1.1.dist-info/METADATA +0 -173
- unitysvc_services-0.1.1.dist-info/RECORD +0 -23
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/WHEEL +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/top_level.txt +0 -0
unitysvc_services/api.py
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
"""Base API client for UnitySVC with automatic curl fallback.
|
2
|
+
|
3
|
+
This module provides the base class for all UnitySVC API clients with
|
4
|
+
automatic network fallback from httpx to curl for systems with network
|
5
|
+
restrictions (e.g., macOS with conda Python).
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import json
|
10
|
+
import os
|
11
|
+
from typing import Any
|
12
|
+
from urllib.parse import urlencode
|
13
|
+
|
14
|
+
import httpx
|
15
|
+
|
16
|
+
|
17
|
+
class UnitySvcAPI:
|
18
|
+
"""Base class for UnitySVC API clients with automatic curl fallback.
|
19
|
+
|
20
|
+
Provides async HTTP GET/POST methods that try httpx first for performance,
|
21
|
+
then automatically fall back to curl if network restrictions are detected
|
22
|
+
(e.g., macOS with conda Python).
|
23
|
+
|
24
|
+
This base class can be used by:
|
25
|
+
- ServiceDataQuery (query/read operations)
|
26
|
+
- ServiceDataPublisher (publish/write operations)
|
27
|
+
- AdminQuery (administrative operations)
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self) -> None:
|
31
|
+
"""Initialize API client from environment variables.
|
32
|
+
|
33
|
+
Raises:
|
34
|
+
ValueError: If required environment variables are not set
|
35
|
+
"""
|
36
|
+
self.base_url = os.environ.get("UNITYSVC_BASE_URL")
|
37
|
+
if not self.base_url:
|
38
|
+
raise ValueError("UNITYSVC_BASE_URL environment variable not set")
|
39
|
+
|
40
|
+
self.api_key = os.environ.get("UNITYSVC_API_KEY")
|
41
|
+
if not self.api_key:
|
42
|
+
raise ValueError("UNITYSVC_API_KEY environment variable not set")
|
43
|
+
|
44
|
+
self.base_url = self.base_url.rstrip("/")
|
45
|
+
self.use_curl_fallback = False
|
46
|
+
self.client = httpx.AsyncClient(
|
47
|
+
headers={
|
48
|
+
"X-API-Key": self.api_key,
|
49
|
+
"Content-Type": "application/json",
|
50
|
+
},
|
51
|
+
timeout=30.0,
|
52
|
+
)
|
53
|
+
|
54
|
+
async def _make_request_curl(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
55
|
+
"""Make HTTP GET request using curl fallback (async).
|
56
|
+
|
57
|
+
Args:
|
58
|
+
endpoint: API endpoint path (e.g., "/publish/sellers")
|
59
|
+
params: Query parameters
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
JSON response as dictionary
|
63
|
+
|
64
|
+
Raises:
|
65
|
+
httpx.HTTPStatusError: If HTTP status code indicates error (with response details)
|
66
|
+
RuntimeError: If curl command fails or times out
|
67
|
+
"""
|
68
|
+
url = f"{self.base_url}{endpoint}"
|
69
|
+
if params:
|
70
|
+
url = f"{url}?{urlencode(params)}"
|
71
|
+
|
72
|
+
cmd = [
|
73
|
+
"curl",
|
74
|
+
"-s", # Silent mode
|
75
|
+
"-w",
|
76
|
+
"\n%{http_code}", # Write status code on new line
|
77
|
+
"-H",
|
78
|
+
f"X-API-Key: {self.api_key}",
|
79
|
+
"-H",
|
80
|
+
"Accept: application/json",
|
81
|
+
url,
|
82
|
+
]
|
83
|
+
|
84
|
+
try:
|
85
|
+
proc = await asyncio.create_subprocess_exec(
|
86
|
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
87
|
+
)
|
88
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
|
89
|
+
|
90
|
+
if proc.returncode != 0:
|
91
|
+
error_msg = stderr.decode().strip() if stderr else "Curl command failed"
|
92
|
+
raise RuntimeError(f"Curl error: {error_msg}")
|
93
|
+
|
94
|
+
# Parse response: last line is status code, rest is body
|
95
|
+
output = stdout.decode().strip()
|
96
|
+
lines = output.split("\n")
|
97
|
+
status_code = int(lines[-1])
|
98
|
+
body = "\n".join(lines[:-1])
|
99
|
+
|
100
|
+
# Parse JSON response
|
101
|
+
try:
|
102
|
+
response_data = json.loads(body) if body else {}
|
103
|
+
except json.JSONDecodeError:
|
104
|
+
response_data = {"error": body}
|
105
|
+
|
106
|
+
# Raise exception for non-2xx status codes (mimics httpx behavior)
|
107
|
+
if status_code < 200 or status_code >= 300:
|
108
|
+
# Create a mock response object to raise HTTPStatusError
|
109
|
+
mock_request = httpx.Request("GET", url)
|
110
|
+
mock_response = httpx.Response(status_code=status_code, content=body.encode(), request=mock_request)
|
111
|
+
raise httpx.HTTPStatusError(f"HTTP {status_code}", request=mock_request, response=mock_response)
|
112
|
+
|
113
|
+
return response_data
|
114
|
+
except TimeoutError:
|
115
|
+
raise RuntimeError("Request timed out after 30 seconds")
|
116
|
+
except httpx.HTTPStatusError:
|
117
|
+
# Re-raise HTTP errors as-is
|
118
|
+
raise
|
119
|
+
|
120
|
+
async def _make_post_request_curl(
|
121
|
+
self, endpoint: str, json_data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
|
122
|
+
) -> dict[str, Any]:
|
123
|
+
"""Make HTTP POST request using curl fallback (async).
|
124
|
+
|
125
|
+
Args:
|
126
|
+
endpoint: API endpoint path (e.g., "/admin/subscriptions")
|
127
|
+
json_data: JSON body data
|
128
|
+
params: Query parameters
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
JSON response as dictionary
|
132
|
+
|
133
|
+
Raises:
|
134
|
+
httpx.HTTPStatusError: If HTTP status code indicates error (with response details)
|
135
|
+
RuntimeError: If curl command fails or times out
|
136
|
+
"""
|
137
|
+
url = f"{self.base_url}{endpoint}"
|
138
|
+
if params:
|
139
|
+
url = f"{url}?{urlencode(params)}"
|
140
|
+
|
141
|
+
cmd = [
|
142
|
+
"curl",
|
143
|
+
"-s", # Silent mode
|
144
|
+
"-w",
|
145
|
+
"\n%{http_code}", # Write status code on new line
|
146
|
+
"-X",
|
147
|
+
"POST",
|
148
|
+
"-H",
|
149
|
+
f"X-API-Key: {self.api_key}",
|
150
|
+
"-H",
|
151
|
+
"Content-Type: application/json",
|
152
|
+
"-H",
|
153
|
+
"Accept: application/json",
|
154
|
+
]
|
155
|
+
|
156
|
+
if json_data:
|
157
|
+
cmd.extend(["-d", json.dumps(json_data)])
|
158
|
+
|
159
|
+
cmd.append(url)
|
160
|
+
|
161
|
+
try:
|
162
|
+
proc = await asyncio.create_subprocess_exec(
|
163
|
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
164
|
+
)
|
165
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
|
166
|
+
|
167
|
+
if proc.returncode != 0:
|
168
|
+
error_msg = stderr.decode().strip() if stderr else "Curl command failed"
|
169
|
+
raise RuntimeError(f"Curl error: {error_msg}")
|
170
|
+
|
171
|
+
# Parse response: last line is status code, rest is body
|
172
|
+
output = stdout.decode().strip()
|
173
|
+
lines = output.split("\n")
|
174
|
+
status_code = int(lines[-1])
|
175
|
+
body = "\n".join(lines[:-1])
|
176
|
+
|
177
|
+
# Parse JSON response
|
178
|
+
try:
|
179
|
+
response_data = json.loads(body) if body else {}
|
180
|
+
except json.JSONDecodeError:
|
181
|
+
response_data = {"error": body}
|
182
|
+
|
183
|
+
# Raise exception for non-2xx status codes (mimics httpx behavior)
|
184
|
+
if status_code < 200 or status_code >= 300:
|
185
|
+
# Create a mock response object to raise HTTPStatusError
|
186
|
+
mock_request = httpx.Request("POST", url)
|
187
|
+
mock_response = httpx.Response(status_code=status_code, content=body.encode(), request=mock_request)
|
188
|
+
raise httpx.HTTPStatusError(f"HTTP {status_code}", request=mock_request, response=mock_response)
|
189
|
+
|
190
|
+
return response_data
|
191
|
+
except TimeoutError:
|
192
|
+
raise RuntimeError("Request timed out after 30 seconds")
|
193
|
+
except httpx.HTTPStatusError:
|
194
|
+
# Re-raise HTTP errors as-is
|
195
|
+
raise
|
196
|
+
|
197
|
+
async def get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
198
|
+
"""Make a GET request to the backend API with automatic curl fallback.
|
199
|
+
|
200
|
+
Public async utility method for making GET requests. Tries httpx first for performance,
|
201
|
+
automatically falls back to curl if network restrictions are detected (e.g., macOS
|
202
|
+
with conda Python).
|
203
|
+
|
204
|
+
Args:
|
205
|
+
endpoint: API endpoint path (e.g., "/publish/sellers", "/admin/documents")
|
206
|
+
params: Query parameters
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
JSON response as dictionary
|
210
|
+
|
211
|
+
Raises:
|
212
|
+
RuntimeError: If both httpx and curl fail
|
213
|
+
"""
|
214
|
+
# If we already know curl is needed, use it directly
|
215
|
+
if self.use_curl_fallback:
|
216
|
+
return await self._make_request_curl(endpoint, params)
|
217
|
+
|
218
|
+
# Try httpx first
|
219
|
+
try:
|
220
|
+
response = await self.client.get(f"{self.base_url}{endpoint}", params=params)
|
221
|
+
response.raise_for_status()
|
222
|
+
return response.json()
|
223
|
+
except (httpx.ConnectError, OSError):
|
224
|
+
# Connection failed - likely network restrictions
|
225
|
+
# Fall back to curl and remember this for future requests
|
226
|
+
self.use_curl_fallback = True
|
227
|
+
return await self._make_request_curl(endpoint, params)
|
228
|
+
|
229
|
+
async def post(
|
230
|
+
self, endpoint: str, json_data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
|
231
|
+
) -> dict[str, Any]:
|
232
|
+
"""Make a POST request to the backend API with automatic curl fallback.
|
233
|
+
|
234
|
+
Public async utility method for making POST requests. Tries httpx first for performance,
|
235
|
+
automatically falls back to curl if network restrictions are detected (e.g., macOS
|
236
|
+
with conda Python).
|
237
|
+
|
238
|
+
Args:
|
239
|
+
endpoint: API endpoint path (e.g., "/admin/subscriptions")
|
240
|
+
json_data: JSON body data
|
241
|
+
params: Query parameters
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
JSON response as dictionary
|
245
|
+
|
246
|
+
Raises:
|
247
|
+
RuntimeError: If both httpx and curl fail
|
248
|
+
"""
|
249
|
+
# If we already know curl is needed, use it directly
|
250
|
+
if self.use_curl_fallback:
|
251
|
+
return await self._make_post_request_curl(endpoint, json_data, params)
|
252
|
+
|
253
|
+
# Try httpx first
|
254
|
+
try:
|
255
|
+
response = await self.client.post(f"{self.base_url}{endpoint}", json=json_data, params=params)
|
256
|
+
response.raise_for_status()
|
257
|
+
return response.json()
|
258
|
+
except (httpx.ConnectError, OSError):
|
259
|
+
# Connection failed - likely network restrictions
|
260
|
+
# Fall back to curl and remember this for future requests
|
261
|
+
self.use_curl_fallback = True
|
262
|
+
return await self._make_post_request_curl(endpoint, json_data, params)
|
263
|
+
|
264
|
+
async def check_task(self, task_id: str, poll_interval: float = 2.0, timeout: float = 300.0) -> dict[str, Any]:
|
265
|
+
"""Check and wait for task completion (async version).
|
266
|
+
|
267
|
+
Utility function to poll a Celery task until it completes or times out.
|
268
|
+
Uses the async HTTP client with curl fallback.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
task_id: Celery task ID to poll
|
272
|
+
poll_interval: Seconds between status checks (default: 2.0)
|
273
|
+
timeout: Maximum seconds to wait (default: 300.0)
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
Task result dictionary
|
277
|
+
|
278
|
+
Raises:
|
279
|
+
ValueError: If task fails or times out
|
280
|
+
"""
|
281
|
+
import time
|
282
|
+
|
283
|
+
start_time = time.time()
|
284
|
+
|
285
|
+
while True:
|
286
|
+
elapsed = time.time() - start_time
|
287
|
+
if elapsed > timeout:
|
288
|
+
raise ValueError(f"Task {task_id} timed out after {timeout}s")
|
289
|
+
|
290
|
+
# Check task status using get() with automatic curl fallback
|
291
|
+
# Use UnitySvcAPI.get to ensure we call the async version, not sync wrapper
|
292
|
+
try:
|
293
|
+
status = await UnitySvcAPI.get(self, f"/tasks/{task_id}")
|
294
|
+
except Exception:
|
295
|
+
# Network error while checking status - retry
|
296
|
+
await asyncio.sleep(poll_interval)
|
297
|
+
continue
|
298
|
+
|
299
|
+
state = status.get("state", "PENDING")
|
300
|
+
|
301
|
+
# Check if task is complete
|
302
|
+
if status.get("status") == "completed" or state == "SUCCESS":
|
303
|
+
return status.get("result", {})
|
304
|
+
elif status.get("status") == "failed" or state == "FAILURE":
|
305
|
+
error = status.get("error", "Unknown error")
|
306
|
+
raise ValueError(f"Task {task_id} failed: {error}")
|
307
|
+
|
308
|
+
# Still processing - wait and retry
|
309
|
+
await asyncio.sleep(poll_interval)
|
310
|
+
|
311
|
+
async def aclose(self):
|
312
|
+
"""Close the HTTP client."""
|
313
|
+
await self.client.aclose()
|
314
|
+
|
315
|
+
async def __aenter__(self):
|
316
|
+
"""Async context manager entry."""
|
317
|
+
return self
|
318
|
+
|
319
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
320
|
+
"""Async context manager exit."""
|
321
|
+
await self.aclose()
|
unitysvc_services/cli.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
import typer
|
4
4
|
|
5
|
-
from . import format_data, populate, publisher, query, scaffold, update, validator
|
5
|
+
from . import format_data, populate, publisher, query, scaffold, test, update, validator
|
6
6
|
from . import list as list_cmd
|
7
7
|
|
8
8
|
app = typer.Typer()
|
@@ -14,6 +14,7 @@ app.add_typer(list_cmd.app, name="list")
|
|
14
14
|
app.add_typer(query.app, name="query")
|
15
15
|
app.add_typer(publisher.app, name="publish")
|
16
16
|
app.add_typer(update.app, name="update")
|
17
|
+
app.add_typer(test.app, name="test")
|
17
18
|
|
18
19
|
# Register standalone commands at root level
|
19
20
|
app.command("format")(format_data.format_data)
|
unitysvc_services/format_data.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
"""Format command - format data files."""
|
2
2
|
|
3
|
-
import os
|
4
3
|
from pathlib import Path
|
5
4
|
|
6
5
|
import typer
|
@@ -14,7 +13,7 @@ console = Console()
|
|
14
13
|
def format_data(
|
15
14
|
data_dir: Path | None = typer.Argument(
|
16
15
|
None,
|
17
|
-
help="Directory containing data files to format (default:
|
16
|
+
help="Directory containing data files to format (default: current directory)",
|
18
17
|
),
|
19
18
|
check_only: bool = typer.Option(
|
20
19
|
False,
|
@@ -35,11 +34,7 @@ def format_data(
|
|
35
34
|
|
36
35
|
# Set data directory
|
37
36
|
if data_dir is None:
|
38
|
-
|
39
|
-
if data_dir_str:
|
40
|
-
data_dir = Path(data_dir_str)
|
41
|
-
else:
|
42
|
-
data_dir = Path.cwd() / "data"
|
37
|
+
data_dir = Path.cwd()
|
43
38
|
|
44
39
|
if not data_dir.is_absolute():
|
45
40
|
data_dir = Path.cwd() / data_dir
|
unitysvc_services/list.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
"""List command group - list local data files."""
|
2
2
|
|
3
|
-
import os
|
4
3
|
from pathlib import Path
|
5
4
|
|
6
5
|
import typer
|
@@ -21,25 +20,19 @@ console = Console()
|
|
21
20
|
def list_providers(
|
22
21
|
data_dir: Path | None = typer.Argument(
|
23
22
|
None,
|
24
|
-
help="Directory containing provider files (default:
|
23
|
+
help="Directory containing provider files (default: current directory)",
|
25
24
|
),
|
26
25
|
):
|
27
26
|
"""List all provider files found in the data directory."""
|
28
27
|
# Set data directory
|
29
28
|
if data_dir is None:
|
30
|
-
|
31
|
-
if data_dir_str:
|
32
|
-
data_dir = Path(data_dir_str)
|
33
|
-
else:
|
34
|
-
data_dir = Path.cwd() / "data"
|
29
|
+
data_dir = Path.cwd()
|
35
30
|
|
36
31
|
if not data_dir.is_absolute():
|
37
32
|
data_dir = Path.cwd() / data_dir
|
38
33
|
|
39
34
|
if not data_dir.exists():
|
40
|
-
console.print(
|
41
|
-
f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red"
|
42
|
-
)
|
35
|
+
console.print(f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red")
|
43
36
|
raise typer.Exit(code=1)
|
44
37
|
|
45
38
|
console.print(f"[blue]Searching for providers in:[/blue] {data_dir}\n")
|
@@ -72,25 +65,19 @@ def list_providers(
|
|
72
65
|
def list_sellers(
|
73
66
|
data_dir: Path | None = typer.Argument(
|
74
67
|
None,
|
75
|
-
help="Directory containing seller files (default:
|
68
|
+
help="Directory containing seller files (default: current directory)",
|
76
69
|
),
|
77
70
|
):
|
78
71
|
"""List all seller files found in the data directory."""
|
79
72
|
# Set data directory
|
80
73
|
if data_dir is None:
|
81
|
-
|
82
|
-
if data_dir_str:
|
83
|
-
data_dir = Path(data_dir_str)
|
84
|
-
else:
|
85
|
-
data_dir = Path.cwd() / "data"
|
74
|
+
data_dir = Path.cwd()
|
86
75
|
|
87
76
|
if not data_dir.is_absolute():
|
88
77
|
data_dir = Path.cwd() / data_dir
|
89
78
|
|
90
79
|
if not data_dir.exists():
|
91
|
-
console.print(
|
92
|
-
f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red"
|
93
|
-
)
|
80
|
+
console.print(f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red")
|
94
81
|
raise typer.Exit(code=1)
|
95
82
|
|
96
83
|
console.print(f"[blue]Searching for sellers in:[/blue] {data_dir}\n")
|
@@ -123,25 +110,19 @@ def list_sellers(
|
|
123
110
|
def list_offerings(
|
124
111
|
data_dir: Path | None = typer.Argument(
|
125
112
|
None,
|
126
|
-
help="Directory containing service files (default:
|
113
|
+
help="Directory containing service files (default: current directory)",
|
127
114
|
),
|
128
115
|
):
|
129
116
|
"""List all service offering files found in the data directory."""
|
130
117
|
# Set data directory
|
131
118
|
if data_dir is None:
|
132
|
-
|
133
|
-
if data_dir_str:
|
134
|
-
data_dir = Path(data_dir_str)
|
135
|
-
else:
|
136
|
-
data_dir = Path.cwd() / "data"
|
119
|
+
data_dir = Path.cwd()
|
137
120
|
|
138
121
|
if not data_dir.is_absolute():
|
139
122
|
data_dir = Path.cwd() / data_dir
|
140
123
|
|
141
124
|
if not data_dir.exists():
|
142
|
-
console.print(
|
143
|
-
f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red"
|
144
|
-
)
|
125
|
+
console.print(f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red")
|
145
126
|
raise typer.Exit(code=1)
|
146
127
|
|
147
128
|
console.print(f"[blue]Searching for service offerings in:[/blue] {data_dir}\n")
|
@@ -172,34 +153,26 @@ def list_offerings(
|
|
172
153
|
)
|
173
154
|
|
174
155
|
console.print(table)
|
175
|
-
console.print(
|
176
|
-
f"\n[green]Total:[/green] {len(service_files)} service offering file(s)"
|
177
|
-
)
|
156
|
+
console.print(f"\n[green]Total:[/green] {len(service_files)} service offering file(s)")
|
178
157
|
|
179
158
|
|
180
159
|
@app.command("listings")
|
181
160
|
def list_listings(
|
182
161
|
data_dir: Path | None = typer.Argument(
|
183
162
|
None,
|
184
|
-
help="Directory containing listing files (default:
|
163
|
+
help="Directory containing listing files (default: current directory)",
|
185
164
|
),
|
186
165
|
):
|
187
166
|
"""List all service listing files found in the data directory."""
|
188
167
|
# Set data directory
|
189
168
|
if data_dir is None:
|
190
|
-
|
191
|
-
if data_dir_str:
|
192
|
-
data_dir = Path(data_dir_str)
|
193
|
-
else:
|
194
|
-
data_dir = Path.cwd() / "data"
|
169
|
+
data_dir = Path.cwd()
|
195
170
|
|
196
171
|
if not data_dir.is_absolute():
|
197
172
|
data_dir = Path.cwd() / data_dir
|
198
173
|
|
199
174
|
if not data_dir.exists():
|
200
|
-
console.print(
|
201
|
-
f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red"
|
202
|
-
)
|
175
|
+
console.print(f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red")
|
203
176
|
raise typer.Exit(code=1)
|
204
177
|
|
205
178
|
console.print(f"[blue]Searching for service listings in:[/blue] {data_dir}\n")
|
@@ -240,6 +213,4 @@ def list_listings(
|
|
240
213
|
)
|
241
214
|
|
242
215
|
console.print(table)
|
243
|
-
console.print(
|
244
|
-
f"\n[green]Total:[/green] {len(listing_files)} service listing file(s)"
|
245
|
-
)
|
216
|
+
console.print(f"\n[green]Total:[/green] {len(listing_files)} service listing file(s)")
|