unitysvc-services 0.1.0__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/__init__.py +4 -0
- unitysvc_services/cli.py +21 -0
- unitysvc_services/format_data.py +145 -0
- unitysvc_services/list.py +245 -0
- unitysvc_services/models/__init__.py +6 -0
- unitysvc_services/models/base.py +352 -0
- unitysvc_services/models/listing_v1.py +72 -0
- unitysvc_services/models/provider_v1.py +53 -0
- unitysvc_services/models/seller_v1.py +110 -0
- unitysvc_services/models/service_v1.py +80 -0
- unitysvc_services/populate.py +186 -0
- unitysvc_services/publisher.py +925 -0
- unitysvc_services/query.py +471 -0
- unitysvc_services/scaffold.py +1039 -0
- unitysvc_services/update.py +293 -0
- unitysvc_services/utils.py +240 -0
- unitysvc_services/validator.py +515 -0
- unitysvc_services-0.1.0.dist-info/METADATA +172 -0
- unitysvc_services-0.1.0.dist-info/RECORD +23 -0
- unitysvc_services-0.1.0.dist-info/WHEEL +5 -0
- unitysvc_services-0.1.0.dist-info/entry_points.txt +2 -0
- unitysvc_services-0.1.0.dist-info/licenses/LICENSE +21 -0
- unitysvc_services-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,293 @@
|
|
1
|
+
"""Update command group - update local data files."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
import typer
|
8
|
+
from rich.console import Console
|
9
|
+
|
10
|
+
from .utils import find_file_by_schema_and_name, find_files_by_schema, write_data_file
|
11
|
+
|
12
|
+
app = typer.Typer(help="Update local data files")
|
13
|
+
console = Console()
|
14
|
+
|
15
|
+
|
16
|
+
@app.command("offering")
|
17
|
+
def update_offering(
|
18
|
+
name: str = typer.Option(
|
19
|
+
...,
|
20
|
+
"--name",
|
21
|
+
"-n",
|
22
|
+
help="Name of the service offering to update (matches 'name' field in service file)",
|
23
|
+
),
|
24
|
+
status: str | None = typer.Option(
|
25
|
+
None,
|
26
|
+
"--status",
|
27
|
+
"-s",
|
28
|
+
help="New upstream_status (uploading, ready, deprecated)",
|
29
|
+
),
|
30
|
+
display_name: str | None = typer.Option(
|
31
|
+
None,
|
32
|
+
"--display-name",
|
33
|
+
help="New display name for the offering",
|
34
|
+
),
|
35
|
+
description: str | None = typer.Option(
|
36
|
+
None,
|
37
|
+
"--description",
|
38
|
+
help="New description for the offering",
|
39
|
+
),
|
40
|
+
version: str | None = typer.Option(
|
41
|
+
None,
|
42
|
+
"--version",
|
43
|
+
help="New version for the offering",
|
44
|
+
),
|
45
|
+
data_dir: Path | None = typer.Option(
|
46
|
+
None,
|
47
|
+
"--data-dir",
|
48
|
+
"-d",
|
49
|
+
help="Directory containing data files (default: ./data or UNITYSVC_DATA_DIR env var)",
|
50
|
+
),
|
51
|
+
):
|
52
|
+
"""
|
53
|
+
Update fields in a service offering's local data file.
|
54
|
+
|
55
|
+
Searches for files with schema 'service_v1' by offering name and updates the specified fields.
|
56
|
+
|
57
|
+
Allowed upstream_status values:
|
58
|
+
- uploading: Service is being uploaded (not ready)
|
59
|
+
- ready: Service is ready to be used
|
60
|
+
- deprecated: Service is deprecated from upstream
|
61
|
+
"""
|
62
|
+
# Validate status if provided
|
63
|
+
if status:
|
64
|
+
valid_statuses = ["uploading", "ready", "deprecated"]
|
65
|
+
if status not in valid_statuses:
|
66
|
+
console.print(
|
67
|
+
f"[red]✗[/red] Invalid status: {status}",
|
68
|
+
style="bold red",
|
69
|
+
)
|
70
|
+
console.print(f"[yellow]Allowed statuses:[/yellow] {', '.join(valid_statuses)}")
|
71
|
+
raise typer.Exit(code=1)
|
72
|
+
|
73
|
+
# Check if any update field is provided
|
74
|
+
if not any([status, display_name, description, version]):
|
75
|
+
console.print(
|
76
|
+
(
|
77
|
+
"[red]✗[/red] No fields to update. Provide at least one of: "
|
78
|
+
"--status, --display-name, --description, --version"
|
79
|
+
),
|
80
|
+
style="bold red",
|
81
|
+
)
|
82
|
+
raise typer.Exit(code=1)
|
83
|
+
|
84
|
+
# Set data directory
|
85
|
+
if data_dir is None:
|
86
|
+
data_dir_str = os.getenv("UNITYSVC_DATA_DIR")
|
87
|
+
if data_dir_str:
|
88
|
+
data_dir = Path(data_dir_str)
|
89
|
+
else:
|
90
|
+
data_dir = Path.cwd() / "data"
|
91
|
+
|
92
|
+
if not data_dir.is_absolute():
|
93
|
+
data_dir = Path.cwd() / data_dir
|
94
|
+
|
95
|
+
if not data_dir.exists():
|
96
|
+
console.print(f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red")
|
97
|
+
raise typer.Exit(code=1)
|
98
|
+
|
99
|
+
console.print(f"[blue]Searching for offering:[/blue] {name}")
|
100
|
+
console.print(f"[blue]In directory:[/blue] {data_dir}\n")
|
101
|
+
|
102
|
+
# Find the matching offering file
|
103
|
+
result = find_file_by_schema_and_name(data_dir, "service_v1", "name", name)
|
104
|
+
|
105
|
+
if not result:
|
106
|
+
console.print(
|
107
|
+
f"[red]✗[/red] No offering found with name: {name}",
|
108
|
+
style="bold red",
|
109
|
+
)
|
110
|
+
raise typer.Exit(code=1)
|
111
|
+
|
112
|
+
matching_file, matching_format, data = result
|
113
|
+
|
114
|
+
# Update the fields
|
115
|
+
try:
|
116
|
+
updates: dict[str, tuple[Any, Any]] = {} # field: (old_value, new_value)
|
117
|
+
|
118
|
+
if status:
|
119
|
+
updates["upstream_status"] = (data.get("upstream_status", "unknown"), status)
|
120
|
+
data["upstream_status"] = status
|
121
|
+
|
122
|
+
if display_name:
|
123
|
+
updates["display_name"] = (data.get("display_name", ""), display_name)
|
124
|
+
data["display_name"] = display_name
|
125
|
+
|
126
|
+
if description:
|
127
|
+
updates["description"] = (data.get("description", ""), description)
|
128
|
+
data["description"] = description
|
129
|
+
|
130
|
+
if version:
|
131
|
+
updates["version"] = (data.get("version", ""), version)
|
132
|
+
data["version"] = version
|
133
|
+
|
134
|
+
# Write back in same format
|
135
|
+
write_data_file(matching_file, data, matching_format)
|
136
|
+
|
137
|
+
console.print("[green]✓[/green] Updated offering successfully!")
|
138
|
+
console.print(f"[cyan]File:[/cyan] {matching_file.relative_to(data_dir)}")
|
139
|
+
console.print(f"[cyan]Format:[/cyan] {matching_format.upper()}\n")
|
140
|
+
|
141
|
+
for field, (old, new) in updates.items():
|
142
|
+
console.print(f"[cyan]{field}:[/cyan]")
|
143
|
+
if len(str(old)) > 60 or len(str(new)) > 60:
|
144
|
+
console.print(f" [dim]Old:[/dim] {str(old)[:60]}...")
|
145
|
+
console.print(f" [dim]New:[/dim] {str(new)[:60]}...")
|
146
|
+
else:
|
147
|
+
console.print(f" [dim]Old:[/dim] {old}")
|
148
|
+
console.print(f" [dim]New:[/dim] {new}")
|
149
|
+
|
150
|
+
except Exception as e:
|
151
|
+
console.print(
|
152
|
+
f"[red]✗[/red] Failed to update offering: {e}",
|
153
|
+
style="bold red",
|
154
|
+
)
|
155
|
+
raise typer.Exit(code=1)
|
156
|
+
|
157
|
+
|
158
|
+
@app.command("listing")
|
159
|
+
def update_listing(
|
160
|
+
service_name: str = typer.Option(
|
161
|
+
...,
|
162
|
+
"--service-name",
|
163
|
+
"-n",
|
164
|
+
help="Name of the service (to search for listing files in service directory)",
|
165
|
+
),
|
166
|
+
status: str | None = typer.Option(
|
167
|
+
None,
|
168
|
+
"--status",
|
169
|
+
"-s",
|
170
|
+
help=(
|
171
|
+
"New listing_status (unknown, upstream_ready, downstream_ready, "
|
172
|
+
"ready, in_service, upstream_deprecated, deprecated)"
|
173
|
+
),
|
174
|
+
),
|
175
|
+
seller_name: str | None = typer.Option(
|
176
|
+
None,
|
177
|
+
"--seller",
|
178
|
+
help="Seller name to filter listings (updates only matching seller's listing)",
|
179
|
+
),
|
180
|
+
data_dir: Path | None = typer.Option(
|
181
|
+
None,
|
182
|
+
"--data-dir",
|
183
|
+
"-d",
|
184
|
+
help="Directory containing data files (default: ./data or UNITYSVC_DATA_DIR env var)",
|
185
|
+
),
|
186
|
+
):
|
187
|
+
"""
|
188
|
+
Update fields in service listing(s) local data files.
|
189
|
+
|
190
|
+
Searches for files with schema 'listing_v1' in the service directory and updates the specified fields.
|
191
|
+
|
192
|
+
Allowed listing_status values:
|
193
|
+
- unknown: Not yet determined
|
194
|
+
- upstream_ready: Upstream is ready to be used
|
195
|
+
- downstream_ready: Downstream is ready with proper routing, logging, and billing
|
196
|
+
- ready: Operationally ready (with docs, metrics, and pricing)
|
197
|
+
- in_service: Service is in service
|
198
|
+
- upstream_deprecated: Service is deprecated from upstream
|
199
|
+
- deprecated: Service is no longer offered to users
|
200
|
+
"""
|
201
|
+
# Validate status if provided
|
202
|
+
if status:
|
203
|
+
valid_statuses = [
|
204
|
+
"unknown",
|
205
|
+
"upstream_ready",
|
206
|
+
"downstream_ready",
|
207
|
+
"ready",
|
208
|
+
"in_service",
|
209
|
+
"upstream_deprecated",
|
210
|
+
"deprecated",
|
211
|
+
]
|
212
|
+
if status not in valid_statuses:
|
213
|
+
console.print(
|
214
|
+
f"[red]✗[/red] Invalid status: {status}",
|
215
|
+
style="bold red",
|
216
|
+
)
|
217
|
+
console.print(f"[yellow]Allowed statuses:[/yellow] {', '.join(valid_statuses)}")
|
218
|
+
raise typer.Exit(code=1)
|
219
|
+
|
220
|
+
# Check if any update field is provided
|
221
|
+
if not status:
|
222
|
+
console.print(
|
223
|
+
"[red]✗[/red] No fields to update. Provide at least one of: --status",
|
224
|
+
style="bold red",
|
225
|
+
)
|
226
|
+
raise typer.Exit(code=1)
|
227
|
+
|
228
|
+
# Set data directory
|
229
|
+
if data_dir is None:
|
230
|
+
data_dir_str = os.getenv("UNITYSVC_DATA_DIR")
|
231
|
+
if data_dir_str:
|
232
|
+
data_dir = Path(data_dir_str)
|
233
|
+
else:
|
234
|
+
data_dir = Path.cwd() / "data"
|
235
|
+
|
236
|
+
if not data_dir.is_absolute():
|
237
|
+
data_dir = Path.cwd() / data_dir
|
238
|
+
|
239
|
+
if not data_dir.exists():
|
240
|
+
console.print(f"[red]✗[/red] Data directory not found: {data_dir}", style="bold red")
|
241
|
+
raise typer.Exit(code=1)
|
242
|
+
|
243
|
+
console.print(f"[blue]Searching for service:[/blue] {service_name}")
|
244
|
+
console.print(f"[blue]In directory:[/blue] {data_dir}")
|
245
|
+
if seller_name:
|
246
|
+
console.print(f"[blue]Filtering by seller:[/blue] {seller_name}")
|
247
|
+
console.print()
|
248
|
+
|
249
|
+
# Build field filter
|
250
|
+
field_filter = {}
|
251
|
+
if seller_name:
|
252
|
+
field_filter["seller_name"] = seller_name
|
253
|
+
|
254
|
+
# Find listing files matching criteria
|
255
|
+
listing_files = find_files_by_schema(data_dir, "listing_v1", path_filter=service_name, field_filter=field_filter)
|
256
|
+
|
257
|
+
if not listing_files:
|
258
|
+
console.print(
|
259
|
+
"[red]✗[/red] No listing files found matching criteria",
|
260
|
+
style="bold red",
|
261
|
+
)
|
262
|
+
raise typer.Exit(code=1)
|
263
|
+
|
264
|
+
# Update all matching listings
|
265
|
+
updated_count = 0
|
266
|
+
for listing_file, file_format, data in listing_files:
|
267
|
+
try:
|
268
|
+
old_status = data.get("listing_status", "unknown")
|
269
|
+
if status:
|
270
|
+
data["listing_status"] = status
|
271
|
+
|
272
|
+
# Write back in same format
|
273
|
+
write_data_file(listing_file, data, file_format)
|
274
|
+
|
275
|
+
console.print(f"[green]✓[/green] Updated: {listing_file.relative_to(data_dir)}")
|
276
|
+
console.print(f" [dim]Seller: {data.get('seller_name', 'N/A')}[/dim]")
|
277
|
+
console.print(f" [dim]Format: {file_format.upper()}[/dim]")
|
278
|
+
if status:
|
279
|
+
console.print(f" [dim]Old status: {old_status} → New status: {status}[/dim]")
|
280
|
+
console.print()
|
281
|
+
updated_count += 1
|
282
|
+
|
283
|
+
except Exception as e:
|
284
|
+
console.print(
|
285
|
+
f"[red]✗[/red] Failed to update {listing_file.relative_to(data_dir)}: {e}",
|
286
|
+
style="bold red",
|
287
|
+
)
|
288
|
+
|
289
|
+
if updated_count > 0:
|
290
|
+
console.print(f"[green]✓[/green] Successfully updated {updated_count} listing(s)")
|
291
|
+
else:
|
292
|
+
console.print("[red]✗[/red] No listings were updated", style="bold red")
|
293
|
+
raise typer.Exit(code=1)
|
@@ -0,0 +1,240 @@
|
|
1
|
+
"""Utility functions for file handling and data operations."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import tomllib
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
import tomli_w
|
9
|
+
|
10
|
+
|
11
|
+
def load_data_file(file_path: Path) -> tuple[dict[str, Any], str]:
|
12
|
+
"""
|
13
|
+
Load a data file (JSON or TOML) and return (data, format).
|
14
|
+
|
15
|
+
Args:
|
16
|
+
file_path: Path to the data file
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
Tuple of (data dict, format string "json" or "toml")
|
20
|
+
|
21
|
+
Raises:
|
22
|
+
ValueError: If file format is not supported
|
23
|
+
"""
|
24
|
+
if file_path.suffix == ".json":
|
25
|
+
with open(file_path, encoding="utf-8") as f:
|
26
|
+
return json.load(f), "json"
|
27
|
+
elif file_path.suffix == ".toml":
|
28
|
+
with open(file_path, "rb") as f:
|
29
|
+
return tomllib.load(f), "toml"
|
30
|
+
else:
|
31
|
+
raise ValueError(f"Unsupported file format: {file_path.suffix}")
|
32
|
+
|
33
|
+
|
34
|
+
def write_data_file(file_path: Path, data: dict[str, Any], format: str) -> None:
|
35
|
+
"""
|
36
|
+
Write data back to file in the specified format.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
file_path: Path to the data file
|
40
|
+
data: Data dictionary to write
|
41
|
+
format: Format string ("json" or "toml")
|
42
|
+
|
43
|
+
Raises:
|
44
|
+
ValueError: If format is not supported
|
45
|
+
"""
|
46
|
+
if format == "json":
|
47
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
48
|
+
json.dump(data, f, indent=2, sort_keys=True)
|
49
|
+
f.write("\n")
|
50
|
+
elif format == "toml":
|
51
|
+
with open(file_path, "wb") as f:
|
52
|
+
tomli_w.dump(data, f)
|
53
|
+
else:
|
54
|
+
raise ValueError(f"Unsupported format: {format}")
|
55
|
+
|
56
|
+
|
57
|
+
def find_data_files(data_dir: Path, extensions: list[str] | None = None) -> list[Path]:
|
58
|
+
"""
|
59
|
+
Find all data files in a directory with specified extensions.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
data_dir: Directory to search
|
63
|
+
extensions: List of extensions to search for (default: ["json", "toml"])
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
List of Path objects for matching files
|
67
|
+
"""
|
68
|
+
if extensions is None:
|
69
|
+
extensions = ["json", "toml"]
|
70
|
+
|
71
|
+
data_files: list[Path] = []
|
72
|
+
for ext in extensions:
|
73
|
+
data_files.extend(data_dir.rglob(f"*.{ext}"))
|
74
|
+
return data_files
|
75
|
+
|
76
|
+
|
77
|
+
def find_file_by_schema_and_name(
|
78
|
+
data_dir: Path, schema: str, name_field: str, name_value: str
|
79
|
+
) -> tuple[Path, str, dict[str, Any]] | None:
|
80
|
+
"""
|
81
|
+
Find a data file by schema type and name field value.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
data_dir: Directory to search
|
85
|
+
schema: Schema identifier (e.g., "service_v1", "listing_v1")
|
86
|
+
name_field: Field name to match (e.g., "name", "seller_name")
|
87
|
+
name_value: Value to match in the name field
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
Tuple of (file_path, format, data) if found, None otherwise
|
91
|
+
"""
|
92
|
+
data_files = find_data_files(data_dir)
|
93
|
+
|
94
|
+
for data_file in data_files:
|
95
|
+
try:
|
96
|
+
data, file_format = load_data_file(data_file)
|
97
|
+
if data.get("schema") == schema and data.get(name_field) == name_value:
|
98
|
+
return data_file, file_format, data
|
99
|
+
except Exception:
|
100
|
+
# Skip files that can't be loaded
|
101
|
+
continue
|
102
|
+
|
103
|
+
return None
|
104
|
+
|
105
|
+
|
106
|
+
def find_files_by_schema(
|
107
|
+
data_dir: Path,
|
108
|
+
schema: str,
|
109
|
+
path_filter: str | None = None,
|
110
|
+
field_filter: dict[str, Any] | None = None,
|
111
|
+
) -> list[tuple[Path, str, dict[str, Any]]]:
|
112
|
+
"""
|
113
|
+
Find all data files matching a schema with optional filters.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
data_dir: Directory to search
|
117
|
+
schema: Schema identifier (e.g., "service_v1", "listing_v1")
|
118
|
+
path_filter: Optional string that must be in the file path
|
119
|
+
field_filter: Optional dict of field:value pairs to filter by
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
List of tuples (file_path, format, data) for matching files
|
123
|
+
"""
|
124
|
+
data_files = find_data_files(data_dir)
|
125
|
+
matching_files: list[tuple[Path, str, dict[str, Any]]] = []
|
126
|
+
|
127
|
+
for data_file in data_files:
|
128
|
+
try:
|
129
|
+
# Apply path filter
|
130
|
+
if path_filter and path_filter not in str(data_file):
|
131
|
+
continue
|
132
|
+
|
133
|
+
data, file_format = load_data_file(data_file)
|
134
|
+
|
135
|
+
# Check schema
|
136
|
+
if data.get("schema") != schema:
|
137
|
+
continue
|
138
|
+
|
139
|
+
# Apply field filters
|
140
|
+
if field_filter:
|
141
|
+
if not all(data.get(k) == v for k, v in field_filter.items()):
|
142
|
+
continue
|
143
|
+
|
144
|
+
matching_files.append((data_file, file_format, data))
|
145
|
+
|
146
|
+
except Exception:
|
147
|
+
# Skip files that can't be loaded
|
148
|
+
continue
|
149
|
+
|
150
|
+
return matching_files
|
151
|
+
|
152
|
+
|
153
|
+
def resolve_provider_name(file_path: Path) -> str | None:
|
154
|
+
"""
|
155
|
+
Resolve the provider name from the file path.
|
156
|
+
|
157
|
+
The provider name is determined by the directory structure:
|
158
|
+
- For service offerings: <provider_name>/services/<service_name>/service.{json,toml}
|
159
|
+
- For service listings: <provider_name>/services/<service_name>/listing-*.{json,toml}
|
160
|
+
|
161
|
+
Args:
|
162
|
+
file_path: Path to the service offering or listing file
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
Provider name if found in directory structure, None otherwise
|
166
|
+
"""
|
167
|
+
# Check if file is under a "services" directory
|
168
|
+
parts = file_path.parts
|
169
|
+
|
170
|
+
try:
|
171
|
+
# Find the "services" directory in the path
|
172
|
+
services_idx = parts.index("services")
|
173
|
+
|
174
|
+
# Provider name is the directory before "services"
|
175
|
+
if services_idx > 0:
|
176
|
+
provider_dir = parts[services_idx - 1]
|
177
|
+
|
178
|
+
# The provider directory should contain a provider data file
|
179
|
+
# Get the full path to the provider directory
|
180
|
+
provider_path = Path(*parts[:services_idx])
|
181
|
+
|
182
|
+
# Look for provider data file to validate and get the actual provider name
|
183
|
+
for data_file in find_data_files(provider_path):
|
184
|
+
try:
|
185
|
+
# Only check files in the provider directory itself, not subdirectories
|
186
|
+
if data_file.parent == provider_path:
|
187
|
+
data, _file_format = load_data_file(data_file)
|
188
|
+
if data.get("schema") == "provider_v1":
|
189
|
+
return data.get("name")
|
190
|
+
except Exception:
|
191
|
+
continue
|
192
|
+
|
193
|
+
# Fallback to directory name if no provider file found
|
194
|
+
return provider_dir
|
195
|
+
except (ValueError, IndexError):
|
196
|
+
# "services" not in path or invalid structure
|
197
|
+
pass
|
198
|
+
|
199
|
+
return None
|
200
|
+
|
201
|
+
|
202
|
+
def resolve_service_name_for_listing(listing_file: Path, listing_data: dict[str, Any]) -> str | None:
|
203
|
+
"""
|
204
|
+
Resolve the service name for a listing file.
|
205
|
+
|
206
|
+
Rules:
|
207
|
+
1. If service_name is defined in listing_data, return it
|
208
|
+
2. Otherwise, find the only service offering in the same directory and return its name
|
209
|
+
|
210
|
+
Args:
|
211
|
+
listing_file: Path to the listing file
|
212
|
+
listing_data: Listing data dictionary
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
Service name if found, None otherwise
|
216
|
+
"""
|
217
|
+
# Rule 1: If service_name is already defined, use it
|
218
|
+
if "service_name" in listing_data and listing_data["service_name"]:
|
219
|
+
return listing_data["service_name"]
|
220
|
+
|
221
|
+
# Rule 2: Find the only service offering in the same directory
|
222
|
+
listing_dir = listing_file.parent
|
223
|
+
|
224
|
+
# Find all service offering files in the same directory
|
225
|
+
service_files: list[tuple[Path, str, dict[str, Any]]] = []
|
226
|
+
for data_file in find_data_files(listing_dir):
|
227
|
+
try:
|
228
|
+
data, file_format = load_data_file(data_file)
|
229
|
+
if data.get("schema") == "service_v1":
|
230
|
+
service_files.append((data_file, file_format, data))
|
231
|
+
except Exception:
|
232
|
+
continue
|
233
|
+
|
234
|
+
# If there's exactly one service file, use its name
|
235
|
+
if len(service_files) == 1:
|
236
|
+
_service_file, _service_format, service_data = service_files[0]
|
237
|
+
return service_data.get("name")
|
238
|
+
|
239
|
+
# Otherwise, return None (either no service files or multiple service files)
|
240
|
+
return None
|